Skip to content

perf(semaphore): avoid the queue lock for positive-balance releases - #316

Draft
orthur2 wants to merge 2 commits into
apache:mainfrom
orthur2:perf/semaphore-positive-balance-release
Draft

orthur2 wants to merge 2 commits into
apache:mainfrom
orthur2:perf/semaphore-positive-balance-release

Conversation

@orthur2

@orthur2 orthur2 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Try one checked CAS in Semaphore::release when permits > 0, falling back to the waiters lock when the balance is zero or the CAS fails. Mutex guards use the internal release_all_held path to skip the probe.

This is a draft for discussion and to preserve the experiment. The change improves the measured Linux workloads, but macOS shows regressions in tight two-thread acquire/release loops. I am not proposing to merge it in its current form.

Design Notes

The existing queue protocol drains the balance before linking a waiter and returns permits to the balance only after the queue is empty. The CAS validates that the observed positive balance has not changed before adding permits.

Fast releases can also update a positive balance while the queue lock is held, so the locked path uses a checked CAS there to preserve overflow detection. Balance updates remain read-modify-write operations. Tests cover fast and locked releases, waiter handoff, concurrent acquisition/release, overflow, and release-sequence ordering.

Falling back after one failed CAS does not stop other threads from using the fast paths. Earlier macOS diagnostic runs found substantially more failed acquire and locked-release CAS attempts; restoring locked releases largely recovered baseline performance. Bounded backoff experiments did not preserve the gains across the control workloads and are not included in this diff.

Benchmarks

Historical measurements: baseline da30f7a versus 3768717, Rust 1.96.0. The branch now merges upstream main at 87017f9 in 9c09ca1; these benchmarks have not been rerun after that merge. An out-of-tree probe links the variants into one executable and runs seven randomized, interleaved rounds of one million iterations per worker, with startup barriers. The semaphore has 64 permits and acquires/releases two per iteration; the read-lock loop polls read(). Each loop drops the guard immediately without added computation.

Values are medians across rounds of elapsed wall time divided by iterations per worker, in ns; lower is better. Percentages compare against the baseline on the same machine, not across platforms.

Workload Threads Linux baseline → PR macOS baseline → PR
Semaphore 1 50.27 → 28.81 (-43%) 7.36 → 4.70 (-36%)
Semaphore 2 266.44 → 142.53 (-47%) 32.41 → 52.64 (+62%)
Semaphore 4 765.56 → 326.99 (-57%) 85.62 → 46.11 (-46%)
Semaphore 8 2000.60 → 746.70 (-63%) 243.27 → 129.04 (-47%)
RwLock read 1 75.14 → 40.17 (-47%) 12.18 → 8.00 (-34%)
RwLock read 2 237.47 → 171.88 (-28%) 46.04 → 92.86 (+102%)
RwLock read 4 862.19 → 344.42 (-60%) 121.91 → 120.61 (-1%)
RwLock read 8 2423.15 → 835.18 (-66%) 313.95 → 190.16 (-39%)

Linux: Hygon C86 7360 (x86-64), 24 physical cores / 48 hardware threads; benchmark workers pinned to up to 8 distinct physical cores. Two threads share an L3; four stay within one NUMA node; eight span two nodes. Memory placement is fixed. Pre/post checks, including SMT siblings, showed at least 97% idle for the main matrix.

macOS: Apple M5, 10 cores (4 performance cores + 6 efficiency cores), with up to 8 benchmark workers and no core pinning. This was a repeat under background load, with CPU activity and swap-ins recorded; it is not an idle-machine validation. These results reproduce the two-thread regression but do not isolate how much background load affects its magnitude.

The open question is whether the fast-path gains can be retained without the macOS tight-contention regression or changes to permit limits and fairness.

Validation

After merging upstream main: cargo x test (607 passed), cargo x check, and cargo x lint. The merge preserves upstream wake-panic handling and adds a regression test for overflow in the locked release path during unwinding.

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.

1 participant