Repository navigation
perf(semaphore): register the first waiter without allocating - #419
Open
nanami-0713 wants to merge 2 commits into
Open
nanami-0713 wants to merge 2 commits into
nanami-0713 wants to merge 2 commits into
Conversation
tisonkun
requested review from
orthur2 and
tisonkun
and
a balanced review from Copilot
November 9, 2026 07:06
There was a problem hiding this comment.
🟡 Changes recommended
The new tests lack regression assertions for allocation-free first registration, lone-waiter reuse, and spill behavior that preserves slot 0, so they would also pass with the previous vector-only implementation.
1 open issue
What changed in this PR
This PR reduces first-waiter allocation overhead in queue-backed synchronization primitives by storing the arena’s first waiter inline, addressing #400.
Changes:
- Keep the first arena slot inline and spill to vector storage when a second concurrent waiter registers.
- Update arena access, removal, and draining logic, with tests for slot reuse and ID stability.
- Document the performance improvement in the changelog.
| File | Description |
|---|---|
CHANGELOG.md |
Records reduced first-waiter allocation overhead for affected primitives. |
asyncband/src/internal/arena.rs |
Adds inline storage, spill handling, and arena regression tests. |
Review effort: Balanced
Comment on lines
+270
to
+280
| assert_eq!(id, SlotId::from_index(0)); | ||
| assert_eq!(arena.get(id), Some(&"only")); | ||
| assert_eq!(arena.len(), 1); | ||
|
|
||
| assert_eq!(arena.remove(id), "only"); | ||
| assert!(arena.is_empty()); | ||
| assert_eq!(arena.get(id), None); | ||
|
|
||
| // Slot 0 is handed out again while the vector is still empty. | ||
| let again = arena.insert("again"); | ||
| assert_eq!(again, id); |
Contributor
Author
|
The initial tests were invariant tests — they pin ID stability, reuse, and take-all-restart semantics, and they intentionally pass on the previous vector-only implementation too, since those invariants hold there. What was missing was a test that distinguishes the change itself. 5fa4ec1 adds both layers:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
The waiter arena keeps its first slot inline, so the first registration on any queue-backed primitive no longer allocates. The slot vector — with today's growth behavior — is only created by the second concurrent waiter. On the fresh-semaphore permit handoff from #400 this removes one of the two allocations per handoff and cuts the median time by about a quarter. Fixes #400.
Design notes
firstfield until the vector first becomes non-empty; the spill moves it intoslots[0], so every slot ID stays stable across the spill and the vacant list only ever spans the vector.take_alldrains both storages and restarts at slot 0, preserving the existing ID-restart semantics (the existing arena test is unchanged).WaitListand every consumer — semaphore, condvar, both reset events, broadcast MPMC — are untouched, so multi-permit ordering, cancellation after a partial or full grant, and wakeup delivery keep their current behavior, covered by the unchanged queue logic and the existing suites.Option<T>— a semaphore's wait list grows by 56 bytes on 64-bit targets. Uncontended primitives pay this in size in exchange for an allocation-free first waiter.Measurements
Apple M5 (10 cores), macOS 27.0.1, rustc 1.99.1 stable; this branch versus main at
cf7ec96, run in one session.cargo x bench --bench primitives -- semaphore::acquire::handoff_permit --repeat 10:Allocation counting (
GlobalAllocprobe, 1,000,000 rounds): a fresh handoff drops from two allocations per round (64 B + 192 B) to one (64 B), and a reused semaphore's warm-up registration drops from two allocations to one. The remaining 64 B isstd::sync::Mutex's lazy pthread boxing underacquired_or_enqueue, which Tokio's batch semaphore pays on the same path on this platform; futex-backed targets box nothing.Verification
cargo x testpasses, including new arena tests for the inline slot, spill ID stability, and thetake_allrestart.