perf(sync): defer shared reader wake-state allocation - #1067
Conversation
There was a problem hiding this comment.
Pull request overview
Optimizes no-token shared-reader acquisition by deferring wake-state allocation until contention requires suspension.
Changes:
- Adds lazy wake-state allocation while preserving cancellation and writer preference.
- Adds regression tests and a shared-mutex benchmark.
- Documents the exception-specification and performance changes.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
include/elio/sync/shared_mutex.hpp |
Implements deferred allocation. |
tests/unit/test_sync_cancellation.cpp |
Adds allocation and race regressions. |
examples/shared_mutex_reader_benchmark.cpp |
Adds performance workloads. |
examples/CMakeLists.txt |
Builds the new benchmark. |
wiki/API-Reference.md |
Documents allocation and exception behavior. |
wiki/Performance-Tuning.md |
Documents benchmark methodology. |
CHANGELOG.md |
Records the behavioral change. |
Suppressed comments (2)
examples/shared_mutex_reader_benchmark.cpp:97
- This forced-handoff row schedules
std::noop_coroutine()and then callsawait_resume()manually, so it never measures wakeup and resumption of a parked reader's long-lived coroutine frame as required by #1040. Please perform each handoff through an actual reader task and wait for that task to resume before recording completion.
auto waiter = mutex.lock_shared();
if (waiter.await_ready() ||
!waiter.await_suspend(std::noop_coroutine())) {
std::abort();
}
mutex.unlock();
waiter.await_resume();
examples/shared_mutex_reader_benchmark.cpp:119
- The concurrent-reader rows also invoke the awaiter methods directly on
std::threads rather than running long-lived coroutine reader loops. As a result, the documented 1/2/4/8-reader benchmark does not exercise the coroutine path required by #1040. Schedule one persistent reader coroutine per worker and useco_awaitwithin each timed loop.
auto waiter = mutex.lock_shared();
if (!waiter.await_ready()) std::abort();
waiter.await_resume();
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
98fa0e2 to
57ebab3
Compare
|
@copilot please re-review the updated head only. The two prior findings have been addressed; please review without making code changes. |
Both prior findings are addressed in the updated head. Benchmark coroutine frames — CI smoke coverage — The deferred-allocation logic is correct: No issues found in the updated head. |
Description
Defer the independent wake-state allocation used by no-token
shared_mutex::lock_shared()until suspension entry has confirmed that thereader still needs the writer/saturation slow path.
Ready admission and the suspension-entry lock-free retry now allocate nothing.
Token-aware readers retain their eager cancellation state and behavior. The
slow path allocates before taking the waiter-queue mutex, so allocation failure
cannot publish or grant a waiter.
This implements the workload-weighted contract accepted after the earlier
prototype was closed: the common reader-ready path receives the optimization,
while the uncommon contended path is allowed a small absolute cost. The issue
records both the superseded percentage-based gate and the final prospective
starvation-focused policy; this PR does not relabel the earlier result as a
pass.
Type of Change
Related Issues
Closes #1040
Changes Made
Core Changes
lock-free admission attempt.
locking and preserve the existing dequeue/grant lifetime rules.
rollback, and the documented reader-count representation boundary.
token cancellation semantics.
and packed-state race regressions plus a same-source persistent-coroutine
benchmark.
--smokemode in Release CI without atiming threshold.
API Changes
Before: the no-token
lock_shared()awaiter'sawait_suspend()wasnoexcept, because wake state was allocated eagerly while constructing theawaiter.
After: ready readers do not allocate. A no-token reader that still reaches
the writer/saturation slow path allocates in
await_suspend()and may propagatestd::bad_allocbefore any queue or packed-state mutation. Consequently, thatlow-level
await_suspend()is no longernoexcept.The
lock_shared()call shape, awaiter layout, successful result, ownership,writer-preference behavior, and token-aware API are unchanged.
Migration Guide
Most coroutine call sites require no change. Code that directly depends on the
no-token awaiter's
await_suspend()beingnoexceptmust remove thatassumption or handle allocation failure. Applications should continue to obey
the documented 62-bit outstanding-reader precondition.
Testing
Unit Tests
Integration Tests
Sanitizer Testing
Test Results
[sync][shared_mutex]: 695 assertions / 18 cases.elio_tests: 13,666 assertions / 844 cases.[sync][shared_mutex]: 82/1 and 695/18.[sync][shared_mutex]: 82/1 and 695/18.exited with status 2.
The first PR review correctly found that the original benchmark drove several
awaiters directly instead of using long-lived coroutine frames. Those earlier
numbers are not used below. The corrected Release/Werror evidence used the
exact same final persistent-coroutine benchmark source and flags for baseline
and candidate, changing only the Elio include root. Core, reader-only, and
mixed suites used 30 balanced interleaved pairs; pressure used a fresh 60
pairs. Analysis used a 200,000-resample order-stratified paired-log bootstrap:
allocations.
C/B=0.63366, 95% CI[0.62532, 0.64292]).[-2.8036, +3.8947], within thepreregistered +3 ns point / +4 ns upper bounds.
0.63381,0.79064,0.88708; all confidence intervals passed.0.99091; writer p95/p990.99403/0.99610.0.99220; writer p95/p990.99893/1.00399.0.96736; writer p950.99063CI[0.98494, 0.99642], p990.99587CI[0.97381, 1.01800].timeout or zero-progress sample. The observed maximum remains a diagnostic:
ratio
0.95847, CI[0.85501, 1.07436].The process-level CPU mask supplied the allowed set
1-6rather than individualthread-role pinning; those six distinct physical cores covered the four readers,
driver, and scheduler without oversubscription.
Checklist
Code Quality
Documentation
Testing
Compatibility
Performance
Screenshots / Diagrams
Not applicable.
Additional Notes
The representation-limit fallback remains subject to the caller precondition
documented by #1062. This PR does not change the correctness fix from #1063.
Independent current-head and corrected-method reviews found no state-machine,
API-documentation, test, or remaining benchmark-method blocker. The initial
review's long-lived-frame and CI-smoke findings are addressed in the updated
head. GitHub CI is pending.
Reviewer Guidance
Areas requiring special attention:
publication, so
std::bad_allocpreserves the strong guarantee.writer flag observed on either attempt must preserve writer preference.
await_suspend()exception-specification change mustmatch the API reference and changelog.
Thank you for contributing to Elio! 🎉