You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Proposal: Explore a CAS fast path for Semaphore with native atomics #1118
Would you be open to a CAS fast path for Concurrent::Semaphore on CRuby with the existing C extension?
The current MutexSemaphore takes a mutex for every acquisition and release, even when permits are available. Using AtomicFixnum#compare_and_set could reduce this overhead.
Related design in Java
OpenJDK's Semaphore uses CAS to update permits, with AbstractQueuedSynchronizer handling queuing and blocking when acquisition cannot succeed. This provides a design precedent; performance on CRuby still needs its own evaluation.
Proposed approach
Use CAS when permits can be acquired immediately, and a mutex and condition variable when waiting is necessary. Enable this with the existing CRuby C extension, retaining the current implementations elsewhere. Preserve existing Ruby behavior, including large permit counts through a mutex-protected Integer fallback.
A prototype PR in my fork contains the implementation and tests; no new C code or runtime dependency is required.
Initial measurements
Apple M5 MacBook Pro, macOS 26.2, CRuby 4.0.2, YJIT disabled, CAtomicFixnum. The measured prototype was compared with the unchanged Mutex implementation. Results are median throughput over 7 samples after warmup, in millions of acquire/release pairs per second. Both implementations ran in the same process with alternating sample order.
Workload
Existing Mutex
Prototype
Prototype / Mutex
One thread, one permit, acquire/release
2.153
3.130
1.45x
Eight threads, one permit, Thread.pass while acquired
0.253
0.241
0.95x
The waiting case was slower in this run and varied substantially; these results do not establish a waiting-path improvement. The prototype also changes waiter bookkeeping and notification behavior, so the gain cannot be attributed to CAS alone. Full samples and reproduction commands are in the fork PR.
Would this direction be of interest? I would appreciate feedback on the approach and any additional validation needed before preparing an upstream PR.
Thanks for the detailed proposal and measurements!
We're going to pass on this one. It adds a third Semaphore implementation that only applies to CRuby with concurrent-ruby-ext and only speeds up the uncontended path. The waiter-counting, notification and large-count fallback logic is subtle concurrency code to maintain for that gain.
The mutex also doesn't look like the main cost. In a quick local test (Ruby 4.0.7, YJIT disabled), a bare Mutex + ConditionVariable semaphore did ~3.7M acquire/release pairs/s, versus ~1.2M for MutexSemaphore. That simplified version leaves out timeouts and reentrancy, but it suggests most of the overhead is our Ruby-level wrapping rather than the lock.
Closing this, but thanks again for the careful write-up.
Activity
joshuay03 commentedon Oct 2, 2026
Thanks for the detailed proposal and measurements!
We're going to pass on this one. It adds a third Semaphore implementation that only applies to CRuby with
concurrent-ruby-extand only speeds up the uncontended path. The waiter-counting, notification and large-count fallback logic is subtle concurrency code to maintain for that gain.The mutex also doesn't look like the main cost. In a quick local test (Ruby 4.0.7, YJIT disabled), a bare
Mutex+ConditionVariablesemaphore did ~3.7M acquire/release pairs/s, versus ~1.2M forMutexSemaphore. That simplified version leaves out timeouts and reentrancy, but it suggests most of the overhead is our Ruby-level wrapping rather than the lock.Closing this, but thanks again for the careful write-up.