Skip to content

Add opt-in thread safety and FREE_PAGE_IF_EMPTY to PAGED_SPARSE_ARRAY - #701

Open
samrach9 wants to merge 2 commits into
masterfrom
users/samrach9/thread_safe_paged_sparse_array
Open

samrach9 wants to merge 2 commits into
masterfrom
users/samrach9/thread_safe_paged_sparse_array

Conversation

@samrach9

@samrach9 samrach9 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a new module, THREAD_SAFE_PAGED_SPARSE_ARRAY(T): a paged sparse array whose ALLOCATE, RELEASE, GET and FREE_EMPTY_PAGES can be called concurrently from multiple threads. The existing PAGED_SPARSE_ARRAY is unchanged; the new module reuses its result codes (PAGED_SPARSE_ARRAY_ALLOCATE_RESULT, PAGED_SPARSE_ARRAY_GET_RESULT) and bitmap helpers.

This is the c-util part of PBI 40316555, which replaces the fixed, preallocated sequence-indexed arrays in EBS bs_ordered_op_writer (~40 MB per primary) and bsi_sequence_tracker (~4 MB) with a structure that sizes itself to the operations in flight. Moving the writer and the tracker to it will be separate EBS PRs.

  • Lock-free hot path (PBI requirement): ALLOCATE, RELEASE and GET on an allocated page take no lock. They only use interlocked operations on the page's use count and allocation bitmap.
  • Per-page locks: each page has its own lock, taken only when that page is created (first ALLOCATE into it) or freed. Calls on other pages never wait for it.
  • Safe page freeing: a page's use count counts its allocated elements plus the calls currently using it. A page is freed only after its use count is atomically changed from 0 to "freeing", and the memory is freed after its lock is released. A pointer returned by ALLOCATE/GET stays valid until that index is released.
  • FREE_EMPTY_PAGES(start_index, end_index): frees the empty, unused pages that contain any index in the range, and skips the others.
  • Compile-time options, selected by name: THREAD_SAFE_PAGED_SPARSE_ARRAY_TYPE_DEFINE(T) uses AUTO_FREE_EMPTY_PAGES and DO_NOT_ZERO_NEW_PAGES. THREAD_SAFE_PAGED_SPARSE_ARRAY_TYPE_DEFINE_WITH_OPTIONS(T, KEEP_EMPTY_PAGES, ZERO_NEW_PAGES) selects the others. KEEP_EMPTY_PAGES is for ring buffers: the caller frees pages when its tail leaves them, instead of a page being freed and re-allocated every time it becomes empty. A misspelled option, a number, or swapped options do not compile.
  • Tests: 55 unit tests, including tests that assert no lock is taken on the hot path, and 3 multi-threaded int tests (chaos with auto-free, chaos with concurrent FREE_EMPTY_PAGES, and a producer/consumer ring buffer). Passing on Windows and Linux; traceability and repo validation pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@samrach9

samrach9 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

Comment thread inc/c_util/paged_sparse_array_ll.h Resolved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@samrach9

samrach9 commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

- Allocate an index for usage (fails if already allocated)
- Release an index (by default, when all indexes in a page are released, the page is freed)
- Get an index (fails if not allocated)
- Free the pages that contain the indexes of a range, if no element in those pages is allocated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Free the pages that contain the indexes

I don't recall, but why is this needed? Shouldn't pages be freed as soon as no allocated index exists in them?

- `page`: a pointer to the page (see [Page Structure](#page-structure)), `NULL` when the page is not allocated
- `use_count`: the count of allocated elements in the page plus the count of calls that use the page at that moment. While the page is being freed, `THREAD_SAFE_PAGED_SPARSE_ARRAY_PAGE_FREEING` (2<sup>62</sup>) is added to it.
- `lock`: an `SRW_LOCK_LL` that is acquired in exclusive mode only to allocate or to free the page

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these explanations just duplicating the underlyiong PAGED_SPARSE_ARRAY? I don't think we should duplicate but rather explain that this is the thread safe one that wraps the underlying non thread safe thingie.
I hope that's what we want, right?


| Option | Value | Meaning |
|---|---|---|
| `empty_pages` | `AUTO_FREE_EMPTY_PAGES` (default) | A page is freed when its use count becomes 0: no element in it is allocated and no call uses it. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AUTO_FREE_EMPTY_PAGES

Do we ever want to not auto free?

|---|---|---|
| `empty_pages` | `AUTO_FREE_EMPTY_PAGES` (default) | A page is freed when its use count becomes 0: no element in it is allocated and no call uses it. |
| `empty_pages` | `KEEP_EMPTY_PAGES` | Empty pages are kept until `FREE_EMPTY_PAGES` is called or the array is disposed. This avoids freeing and re-allocating a page when its elements are released and allocated again in quick succession (e.g. when the array is used as a ring buffer). |
| `new_pages` | `DO_NOT_ZERO_NEW_PAGES` (default) | The items of a newly allocated page are left uninitialized. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these 2 mutually exclusive?

The rules for callers are:
- Only the user that allocated an `index` shall release it, and an `index` shall not be released more than once per allocation.
- The array only protects its own bookkeeping. Synchronizing access to the contents of an element (e.g., one thread writing an element while another thread reads it after `GET`) is the responsibility of the caller.
- `item_dispose_func` is called without holding any lock.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

samrach9 Did you read all of this document?

**SRS_THREAD_SAFE_PAGED_SPARSE_ARRAY_09_010: [** `THREAD_SAFE_PAGED_SPARSE_ARRAY_CREATE(T)` shall succeed and return a non-`NULL` value. **]**

**SRS_THREAD_SAFE_PAGED_SPARSE_ARRAY_09_011: [** If there are any other errors, `THREAD_SAFE_PAGED_SPARSE_ARRAY_CREATE(T)` shall fail and return `NULL`. **]**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is not using the existing implementation underneath?

I think Dan's point about SRP in the last iteration was that PAGED_SPARSE_ARRAY should be used as is and the thread safety should be provided on top rather than in the same module.

@parth21999

Copy link
Copy Markdown
Member
}                                                                                                                                                                      \

bot likes else if.

We usually use nested if else


Refers to: inc/c_util/thread_safe_paged_sparse_array_ll.h:289 in 7863b58. [](commit_id = 7863b58, deletion_comment = False)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants