macOS ARM64 Apple M4 Threading Benchmarks
Machine Profile
Machine Specification
The benchmarks were run on the following machine:
BenchmarkDotNet v0.15.8, macOS Tahoe 26.4.1 (25E253) [Darwin 25.4.0]
Apple M4, 1 CPU, 10 logical and 10 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a
.NET 10.0 : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a
Job=.NET 10.0 Runtime=.NET 10.0 Toolchain=net10.0
Alloc Ratio=NA
Note: Results are machine-specific and may vary between systems. Run benchmarks locally for your specific hardware.
BenchmarkDotNet microbenchmarks for all async synchronization primitives in CryptoHives.Foundation.Threading. Benchmarks cover uncontested fast paths (0 other waiters), single-waiter async round trips (1 waiter), and high-contention scenarios (10–100 waiters). All implementations are tested with both a default CancellationToken.None and a non-cancelled CancellationToken to isolate the registration overhead.
Implementations are compared against:
- Pooled — CryptoHives pooled
ValueTask-returning implementation (the baseline, ratio = 1.00) - ProtoPromise — ProtoPromise zero-allocation async primitives
- RefImpl — Simple reference implementation using
TaskCompletionSource<bool>(demonstrates heap-allocating baselines) - Nito.AsyncEx — Nito.AsyncEx async synchronization primitives (Stephen Cleary)
- NeoSmart — NeoSmart.AsyncLock
- VS.Threading / NonKeyed — Microsoft.VisualStudio.Threading / non-keyed async lock variants
- System — .NET built-in synchronization primitives (
SemaphoreSlim,ReaderWriterLockSlim,CountdownEvent,Barrier,ManualResetEventSlim,ManualResetEvent,AutoResetEvent)
Highlights
| Primitive | vs Windows | Key Insight |
|---|---|---|
| AsyncAutoResetEvent (Set) | ~34% faster | ARM64 fast path; System.AutoResetEvent ~4× cheaper than on Windows |
| AsyncBarrier (1 participant) | ~39% faster | System.Barrier ~4× cheaper on macOS than Windows at P=1 |
| AsyncCountdownEvent (1 participant) | ~28% faster | Pooled near-parity with System.CountdownEvent |
| AsyncLock (single) | ~9% faster | CryptoHives SpinLock ~20× faster than System.SpinLock |
| AsyncManualResetEvent (SetReset) | ~21% faster | ManualResetEvent kernel ~4× cheaper on macOS than Windows |
| AsyncRWLock (reader, uncontested) | ~2× faster | Strongest uncontested advantage; inverts under high contention |
| AsyncRWLock (writer, uncontested) | ~34% faster | Near-parity with ProtoPromise |
| AsyncSemaphore | ~30% faster | System.SemaphoreSlim ~½ the cost of Windows |
Note
On Apple M4, the Pooled AsTask() path is 2–3× slower than on Windows x64 for low-contention scenarios (1–2 waiters). This reflects Windows ThreadPool's faster inline Task continuation scheduling. However, at 100 waiters the macOS AsTask path becomes faster — the Windows ThreadPool saturates first. For latency-sensitive code, prefer AsValueTask() or direct ValueTask consumption.
AsyncLock
AsyncLock provides exclusive mutual exclusion with cancellation support. The Single benchmark measures the uncontested acquire+release cycle with no other waiters. The Multiple benchmark adds a configurable number of concurrent contending waiters.
Single (uncontested)
The uncontested benchmark is a proxy for the underlying lock and ValueTask state machine cost when the fast path does not need to suspend. The CryptoHives SpinLock matches the speed of .NET's Lock.EnterScope — both roughly 20× faster than System.SpinLock due to the absence of OS kernel yield in the non-contended case. The async Pooled path is the fastest async lock on this platform.
Key observations:
- Pooled: Fastest async lock; ~9% faster than Windows x64
- ProtoPromise: ~12–15% faster than Pooled at this baseline (zero-allocation, no cancellation support)
- System.Lock /
lock(): both ~3–4× cheaper than async paths (no await overhead) - CryptoHives SpinLock: matches System.Lock; suitable only for very short critical sections
- Nito and NeoSmart: each allocate per-lock (320 B / 208 B respectively); in the order-of-magnitude slower range
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| Lock · Interlocked.Exchange · System | 0.0000 ns | 0.000 | - |
| Lock · Increment · System | 0.4601 ns | 0.067 | - |
| Lock · Interlocked.Inc · System | 0.4692 ns | 0.068 | - |
| Lock · Interlocked.Add · System | 0.4771 ns | 0.069 | - |
| Lock · Lock · System | 1.7588 ns | 0.255 | - |
| Lock · Lock.EnterScope · System | 1.7817 ns | 0.258 | - |
| Lock · Interlocked.CmpX · System | 2.4232 ns | 0.351 | - |
| SpinLock · SpinLock · CryptoHives | 2.6420 ns | 0.382 | - |
| Lock · lock() · System | 2.9387 ns | 0.425 | - |
| LockAsync · AsyncLock · ProtoPromise | 6.6327 ns | 0.960 | - |
| LockAsync · AsyncLock · Pooled | 6.9102 ns | 1.000 | - |
| LockAsync · AsyncSemaphore · VS.Threading | 11.4114 ns | 1.651 | - |
| LockAsync · AsyncLock · RefImpl | 11.7925 ns | 1.707 | - |
| LockAsync · SemaphoreSlim · System | 12.4404 ns | 1.800 | - |
| LockAsync · AsyncLock · NonKeyed | 16.8037 ns | 2.432 | - |
| LockAsync · AsyncLock · Nito.AsyncEx | 40.1211 ns | 5.806 | 320 B |
| SpinWait · SpinOnce · System | 44.1741 ns | 6.393 | - |
| SpinLock · SpinLock · System | 52.1762 ns | 7.551 | - |
| LockAsync · AsyncLock · NeoSmart | 60.4453 ns | 8.748 | 208 B |
Multiple (contended)
The Multiple benchmark drives Iterations concurrent waiters through a single lock. Iteration = 0 is the uncontested baseline; Iteration = 1 introduces one additional waiter triggering an actual async suspension and resume; 10 and 100 measure contention scaling.
Key observations:
- Pooled (ValueTask): Fastest async lock path at all contention levels
- At 1 contender: Pooled (VT) leads; ProtoPromise ~20% slower; SemaphoreSlim ~55% slower
- Pooled (AsTask) at 1 contender: ~2.9× slower than Windows. Task continuation scheduling via macOS ThreadPool is slower for a single resume. Prefer
AsValueTask(). - At 100 contenders: Pooled (VT) ~29% faster than Windows for the ValueTask path
- Nito and VS.Threading scale poorly under
NotCancelledcancellation due toCancellationToken.Registeroverhead
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| Multiple · AsyncLock · Pooled (ValueTask) | 0 | None | 8.425 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 0 | None | 9.284 ns | 1.10 | - |
| Multiple · AsyncLock · Pooled (Task) | 0 | None | 10.440 ns | 1.24 | - |
| Multiple · AsyncSemaphore · VS.Threading | 0 | None | 12.675 ns | 1.50 | - |
| Multiple · AsyncLock · RefImpl | 0 | None | 13.141 ns | 1.56 | - |
| Multiple · SemaphoreSlim · System | 0 | None | 14.235 ns | 1.69 | - |
| Multiple · AsyncLock · NonKeyed | 0 | None | 18.142 ns | 2.15 | - |
| Multiple · AsyncLock · Nito | 0 | None | 41.584 ns | 4.94 | 320 B |
| Multiple · AsyncLock · NeoSmart | 0 | None | 50.834 ns | 6.03 | 208 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 0 | NotCancelled | 8.514 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 0 | NotCancelled | 9.266 ns | 1.09 | - |
| Multiple · AsyncLock · Pooled (Task) | 0 | NotCancelled | 10.493 ns | 1.23 | - |
| Multiple · AsyncSemaphore · VS.Threading | 0 | NotCancelled | 12.882 ns | 1.51 | - |
| Multiple · SemaphoreSlim · System | 0 | NotCancelled | 14.293 ns | 1.68 | - |
| Multiple · AsyncLock · NonKeyed | 0 | NotCancelled | 18.182 ns | 2.14 | - |
| Multiple · AsyncLock · Nito | 0 | NotCancelled | 41.084 ns | 4.83 | 320 B |
| Multiple · AsyncLock · NeoSmart | 0 | NotCancelled | 51.570 ns | 6.06 | 208 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 1 | None | 25.441 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 1 | None | 27.270 ns | 1.07 | - |
| Multiple · SemaphoreSlim · System | 1 | None | 36.967 ns | 1.45 | 88 B |
| Multiple · AsyncSemaphore · VS.Threading | 1 | None | 52.469 ns | 2.06 | 168 B |
| Multiple · AsyncLock · RefImpl | 1 | None | 65.429 ns | 2.57 | 216 B |
| Multiple · AsyncLock · Nito | 1 | None | 94.398 ns | 3.71 | 728 B |
| Multiple · AsyncLock · NeoSmart | 1 | None | 104.984 ns | 4.13 | 416 B |
| Multiple · AsyncLock · NonKeyed | 1 | None | 1,274.175 ns | 50.09 | 351 B |
| Multiple · AsyncLock · Pooled (Task) | 1 | None | 1,452.753 ns | 57.10 | 271 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 1 | NotCancelled | 38.623 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 1 | NotCancelled | 42.165 ns | 1.09 | - |
| Multiple · AsyncSemaphore · VS.Threading | 1 | NotCancelled | 62.016 ns | 1.61 | 168 B |
| Multiple · AsyncLock · NeoSmart | 1 | NotCancelled | 108.661 ns | 2.81 | 416 B |
| Multiple · AsyncLock · Nito | 1 | NotCancelled | 649.216 ns | 16.81 | 968 B |
| Multiple · SemaphoreSlim · System | 1 | NotCancelled | 1,424.487 ns | 36.88 | 504 B |
| Multiple · AsyncLock · Pooled (Task) | 1 | NotCancelled | 1,465.472 ns | 37.94 | 272 B |
| Multiple · AsyncLock · NonKeyed | 1 | NotCancelled | 1,480.866 ns | 38.34 | 640 B |
| Multiple · AsyncLock · ProtoPromise | 10 | None | 194.709 ns | 0.72 | - |
| Multiple · SemaphoreSlim · System | 10 | None | 245.409 ns | 0.91 | 880 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 10 | None | 270.181 ns | 1.00 | - |
| Multiple · AsyncSemaphore · VS.Threading | 10 | None | 453.778 ns | 1.68 | 1680 B |
| Multiple · AsyncLock · RefImpl | 10 | None | 556.398 ns | 2.06 | 2160 B |
| Multiple · AsyncLock · NeoSmart | 10 | None | 559.989 ns | 2.07 | 2288 B |
| Multiple · AsyncLock · Nito | 10 | None | 562.881 ns | 2.08 | 4400 B |
| Multiple · AsyncLock · NonKeyed | 10 | None | 7,555.876 ns | 27.97 | 2296 B |
| Multiple · AsyncLock · Pooled (Task) | 10 | None | 8,143.090 ns | 30.14 | 1352 B |
| Multiple · AsyncLock · ProtoPromise | 10 | NotCancelled | 334.922 ns | 0.86 | - |
| Multiple · AsyncLock · Pooled (ValueTask) | 10 | NotCancelled | 389.866 ns | 1.00 | - |
| Multiple · AsyncSemaphore · VS.Threading | 10 | NotCancelled | 551.252 ns | 1.41 | 1680 B |
| Multiple · AsyncLock · NeoSmart | 10 | NotCancelled | 563.921 ns | 1.45 | 2288 B |
| Multiple · AsyncLock · Nito | 10 | NotCancelled | 4,833.723 ns | 12.40 | 6800 B |
| Multiple · AsyncLock · Pooled (Task) | 10 | NotCancelled | 8,736.692 ns | 22.41 | 1352 B |
| Multiple · SemaphoreSlim · System | 10 | NotCancelled | 10,615.816 ns | 27.23 | 3888 B |
| Multiple · AsyncLock · NonKeyed | 10 | NotCancelled | 10,925.889 ns | 28.02 | 5176 B |
| Multiple · AsyncLock · ProtoPromise | 100 | None | 1,807.087 ns | 0.78 | - |
| Multiple · SemaphoreSlim · System | 100 | None | 2,229.703 ns | 0.96 | 8800 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 100 | None | 2,324.326 ns | 1.00 | - |
| Multiple · AsyncSemaphore · VS.Threading | 100 | None | 4,462.744 ns | 1.92 | 21120 B |
| Multiple · AsyncLock · NeoSmart | 100 | None | 4,946.453 ns | 2.13 | 21008 B |
| Multiple · AsyncLock · Nito | 100 | None | 5,257.998 ns | 2.26 | 41120 B |
| Multiple · AsyncLock · RefImpl | 100 | None | 5,321.844 ns | 2.29 | 21600 B |
| Multiple · AsyncLock · NonKeyed | 100 | None | 50,706.763 ns | 21.82 | 21739 B |
| Multiple · AsyncLock · Pooled (Task) | 100 | None | 52,025.798 ns | 22.38 | 12155 B |
| Multiple · AsyncLock · ProtoPromise | 100 | NotCancelled | 3,207.920 ns | 0.88 | - |
| Multiple · AsyncLock · Pooled (ValueTask) | 100 | NotCancelled | 3,639.009 ns | 1.00 | - |
| Multiple · AsyncLock · NeoSmart | 100 | NotCancelled | 4,930.770 ns | 1.36 | 21008 B |
| Multiple · AsyncSemaphore · VS.Threading | 100 | NotCancelled | 5,529.917 ns | 1.52 | 21120 B |
| Multiple · AsyncLock · Nito | 100 | NotCancelled | 59,235.902 ns | 16.28 | 65120 B |
| Multiple · AsyncLock · Pooled (Task) | 100 | NotCancelled | 60,318.879 ns | 16.58 | 12190 B |
| Multiple · SemaphoreSlim · System | 100 | NotCancelled | 83,624.877 ns | 22.98 | 37738 B |
| Multiple · AsyncLock · NonKeyed | 100 | NotCancelled | 94,018.415 ns | 25.84 | 50542 B |
AsyncAutoResetEvent
AsyncAutoResetEvent releases exactly one waiter per Set() call. Three benchmarks cover: Set (no waiters — measures pure signal cost), SetThenWait (signal then immediately wait on the newly-reset event), and WaitThenSet (N waiters await then are unblocked one by one).
Set (no waiters)
The Set benchmark captures the cost of signaling an event that no one is waiting on — effectively a conditional interlocked operation plus any bookkeeping.
Key observations:
- Pooled and ProtoPromise: sub-nanosecond pure-signal cost
- System.AutoResetEvent (kernel): ~120× slower than Pooled; ~4× cheaper than on Windows
- Apple Silicon
mach_semaphoresignaling is significantly cheaper than Windows kernel event objects
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| Set · AsyncAutoReset · Pooled | 0.4561 ns | 1.00 | - |
| Set · AsyncAutoReset · ProtoPromise | 0.5768 ns | 1.26 | - |
| Set · AsyncAutoReset · RefImpl | 2.9920 ns | 6.56 | - |
| Set · AsyncAutoReset · Nito.AsyncEx | 3.2573 ns | 7.14 | - |
| Set · AutoResetEvent · System | 55.2256 ns | 121.09 | - |
SetThenWait
Signals then immediately calls WaitAsync. Because the event was just set, WaitAsync returns synchronously — this measures the combined signal + synchronous check cost.
Key observations:
- Pooled (ValueTask) and ProtoPromise: sub-10 ns combined signal + synchronous check
- RefImpl and Nito: ~2× slower than Pooled
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| SetThenWait · AsyncAutoReset · ProtoPromise | 3.590 ns | 0.77 | - |
| SetThenWait · AsyncAutoReset · Pooled (ValueTask) | 4.632 ns | 1.00 | - |
| SetThenWait · AsyncAutoReset · Pooled (AsTask) | 5.578 ns | 1.20 | - |
| SetThenWait · AsyncAutoReset · Nito.AsyncEx | 9.310 ns | 2.01 | - |
| SetThenWait · AsyncAutoReset · RefImpl | 9.921 ns | 2.14 | - |
WaitThenSet
N waiters call WaitAsync, then Set() is called N times from another context. Each Set/Wait round trip involves a full async scheduling cycle (suspend + resume).
Key observations:
- Pooled (AsValueTask) at 1 waiter: ~14% faster than default
Pooled (ValueTask)when using theAsValueTask()overload - Pooled (AsValueTask SyncCont): synchronized continuation (inline resume) saves a context switch; similar result
- At 100 waiters: Pooled (VT) ~25% faster than Windows for the ValueTask path
- Nito at 100 waiters with
NotCancelled: hundreds of microseconds — 100 per-waiter CancellationToken registrations dominate
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| WaitThenSet · AsyncAutoReset · ProtoPromise | 1 | None | 18.04 ns | 0.81 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 1 | None | 19.83 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 1 | None | 20.61 ns | 0.92 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 1 | None | 22.35 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 1 | None | 22.66 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 1 | None | 24.99 ns | 1.12 | 96 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 1 | None | 31.33 ns | 1.40 | 80 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 1 | None | 31.88 ns | 1.43 | 160 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 1 | None | 1,168.48 ns | 52.28 | 230 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 1 | NotCancelled | 29.92 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 1 | NotCancelled | 30.96 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 1 | NotCancelled | 31.23 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 1 | NotCancelled | 33.47 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 1 | NotCancelled | 33.66 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 1 | NotCancelled | 43.40 ns | 1.30 | 80 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 1 | NotCancelled | 678.27 ns | 20.27 | 400 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 1 | NotCancelled | 1,338.75 ns | 40.01 | 232 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 2 | None | 34.21 ns | 0.59 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 2 | None | 47.47 ns | 0.82 | 192 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 2 | None | 51.41 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 2 | None | 52.59 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 2 | None | 56.66 ns | 0.98 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 2 | None | 57.91 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 2 | None | 60.65 ns | 1.05 | 320 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 2 | None | 73.16 ns | 1.26 | 160 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 2 | None | 1,733.49 ns | 29.94 | 342 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 2 | NotCancelled | 63.36 ns | 0.85 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 2 | NotCancelled | 69.03 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 2 | NotCancelled | 70.77 ns | 0.95 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 2 | NotCancelled | 74.56 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 2 | NotCancelled | 75.09 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 2 | NotCancelled | 98.72 ns | 1.32 | 160 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 2 | NotCancelled | 1,071.75 ns | 14.38 | 800 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 2 | NotCancelled | 1,962.88 ns | 26.33 | 344 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 10 | None | 165.33 ns | 0.57 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 10 | None | 231.48 ns | 0.80 | 960 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 10 | None | 264.91 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 10 | None | 267.93 ns | 0.92 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 10 | None | 291.13 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 10 | None | 297.34 ns | 1.02 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 10 | None | 298.07 ns | 1.02 | 1600 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 10 | None | 387.58 ns | 1.33 | 800 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 10 | None | 6,006.54 ns | 20.63 | 1239 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 10 | NotCancelled | 312.24 ns | 0.78 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 10 | NotCancelled | 373.70 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 10 | NotCancelled | 377.26 ns | 0.94 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 10 | NotCancelled | 394.53 ns | 0.98 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 10 | NotCancelled | 401.78 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 10 | NotCancelled | 509.86 ns | 1.27 | 800 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 10 | NotCancelled | 5,167.63 ns | 12.86 | 4000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 10 | NotCancelled | 7,401.84 ns | 18.42 | 1240 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 100 | None | 1,604.52 ns | 0.58 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 100 | None | 2,153.78 ns | 0.79 | 9600 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 100 | None | 2,358.28 ns | 0.86 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 100 | None | 2,405.40 ns | 0.88 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 100 | None | 2,713.10 ns | 0.99 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 100 | None | 2,742.91 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 100 | None | 2,919.10 ns | 1.06 | 16000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 100 | None | 3,604.36 ns | 1.31 | 8000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 100 | None | 34,738.25 ns | 12.66 | 11319 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 100 | NotCancelled | 3,082.76 ns | 0.81 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 100 | NotCancelled | 3,488.57 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 100 | NotCancelled | 3,516.90 ns | 0.92 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 100 | NotCancelled | 3,827.37 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 100 | NotCancelled | 3,862.02 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 100 | NotCancelled | 4,699.28 ns | 1.23 | 8000 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 100 | NotCancelled | 55,269.09 ns | 14.44 | 40000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 100 | NotCancelled | 159,550.73 ns | 41.69 | 11330 B |
AsyncManualResetEvent
AsyncManualResetEvent releases all waiters when set and stays set until explicitly reset. The SetReset benchmark measures the rapid set/reset cycle. SetThenWait measures a set followed by a synchronous wait (which completes immediately since the event is already set). WaitThenSet drives N concurrent waiters.
SetReset
Combined set-then-reset cycle — captures the cost of toggling the event state with no waiters.
Key observations:
- Pooled: ~4× faster than
ManualResetEventSlim.Set+Resetand ~65× faster thanManualResetEvent - ProtoPromise: fastest overall; ManualResetEvent (kernel): ~4× cheaper on macOS than Windows
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| SetReset · AsyncManualReset · ProtoPromise | 0.9164 ns | 0.60 | - |
| SetReset · AsyncManualReset · Pooled | 1.5384 ns | 1.00 | - |
| SetReset · ManualResetEventSlim · System | 6.8272 ns | 4.44 | - |
| SetReset · AsyncManualReset · RefImpl | 9.2011 ns | 5.98 | 96 B |
| SetReset · AsyncManualReset · Nito.AsyncEx | 14.8942 ns | 9.68 | 96 B |
| SetReset · ManualResetEvent · System | 112.0806 ns | 72.86 | - |
SetThenWait
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| SetThenWait · AsyncManualReset · ProtoPromise | 4.114 ns | 0.72 | - |
| SetThenWait · AsyncManualReset · Pooled (AsTask) | 5.260 ns | 0.93 | - |
| SetThenWait · AsyncManualReset · Pooled (ValueTask) | 5.684 ns | 1.00 | - |
| SetThenWait · AsyncManualReset · RefImpl | 12.428 ns | 2.19 | 96 B |
| SetThenWait · AsyncManualReset · Nito.AsyncEx | 21.419 ns | 3.77 | 96 B |
WaitThenSet
Unlike AsyncAutoResetEvent, the AsyncManualResetEvent WaitThenSet benchmark releases all N waiters in a single Set() call (broadcast semantics). This means the allocation for RefImpl stays constant regardless of waiter count (single TaskCompletionSource shared by all).
Key observations:
- Pooled (AsValueTask) at 1 waiter: ~15% faster than Windows
NotCancelledadds roughly 50–60% overhead per waiter at 1 iteration for the CancellationToken registration- Nito
NotCancelledat 100 waiters: hundreds of microseconds — 100 per-waiter registrations dominate
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| WaitThenSet · AsyncManualReset · RefImpl | 1 | None | 16.18 ns | 0.73 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 1 | None | 18.27 ns | 0.83 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 1 | None | 20.78 ns | 0.94 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 1 | None | 21.81 ns | 0.99 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 1 | None | 22.09 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 1 | None | 23.22 ns | 1.05 | - |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 1 | None | 25.92 ns | 1.17 | 96 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 1 | None | 31.37 ns | 1.42 | 80 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 1 | None | 1,227.92 ns | 55.60 | 231 B |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 1 | NotCancelled | 29.69 ns | 0.88 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 1 | NotCancelled | 31.02 ns | 0.92 | - |
| WaitThenSet · AsyncManualReset · ProtoPromise | 1 | NotCancelled | 32.12 ns | 0.95 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 1 | NotCancelled | 33.48 ns | 0.99 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 1 | NotCancelled | 33.76 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 1 | NotCancelled | 43.49 ns | 1.29 | 80 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 1 | NotCancelled | 1,346.74 ns | 39.89 | 232 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 1 | NotCancelled | 1,425.12 ns | 42.21 | 808 B |
| WaitThenSet · AsyncManualReset · RefImpl | 2 | None | 20.50 ns | 0.36 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 2 | None | 33.28 ns | 0.59 | - |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 2 | None | 35.64 ns | 0.63 | 96 B |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 2 | None | 48.81 ns | 0.86 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 2 | None | 55.93 ns | 0.98 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 2 | None | 56.42 ns | 0.99 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 2 | None | 56.80 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 2 | None | 71.25 ns | 1.25 | 160 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 2 | None | 1,875.99 ns | 33.03 | 344 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 2 | NotCancelled | 63.15 ns | 0.88 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 2 | NotCancelled | 68.56 ns | 0.96 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 2 | NotCancelled | 68.65 ns | 0.96 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 2 | NotCancelled | 71.49 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 2 | NotCancelled | 71.53 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 2 | NotCancelled | 95.36 ns | 1.33 | 160 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 2 | NotCancelled | 2,046.09 ns | 28.60 | 344 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 2 | NotCancelled | 2,311.93 ns | 32.32 | 1488 B |
| WaitThenSet · AsyncManualReset · RefImpl | 10 | None | 57.53 ns | 0.20 | 96 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 10 | None | 99.67 ns | 0.34 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 10 | None | 157.39 ns | 0.54 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 10 | None | 258.76 ns | 0.89 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 10 | None | 259.12 ns | 0.89 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 10 | None | 291.94 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 10 | None | 325.39 ns | 1.11 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 10 | None | 383.58 ns | 1.31 | 800 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 10 | None | 5,473.62 ns | 18.75 | 1240 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 10 | NotCancelled | 299.56 ns | 0.77 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 10 | NotCancelled | 368.54 ns | 0.95 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 10 | NotCancelled | 371.09 ns | 0.95 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 10 | NotCancelled | 389.45 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 10 | NotCancelled | 394.69 ns | 1.01 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 10 | NotCancelled | 504.84 ns | 1.30 | 800 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 10 | NotCancelled | 7,428.64 ns | 19.07 | 1240 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 10 | NotCancelled | 7,450.48 ns | 19.13 | 6464 B |
| WaitThenSet · AsyncManualReset · RefImpl | 100 | None | 451.90 ns | 0.16 | 96 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 100 | None | 921.42 ns | 0.33 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 100 | None | 1,445.45 ns | 0.52 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 100 | None | 2,404.66 ns | 0.87 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 100 | None | 2,415.38 ns | 0.87 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 100 | None | 2,611.25 ns | 0.94 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 100 | None | 2,766.80 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 100 | None | 3,496.55 ns | 1.26 | 8000 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 100 | None | 31,479.01 ns | 11.38 | 11320 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 100 | NotCancelled | 2,860.88 ns | 0.77 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 100 | NotCancelled | 3,445.33 ns | 0.93 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 100 | NotCancelled | 3,482.04 ns | 0.94 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 100 | NotCancelled | 3,710.89 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 100 | NotCancelled | 3,816.98 ns | 1.03 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 100 | NotCancelled | 4,672.33 ns | 1.26 | 8000 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 100 | NotCancelled | 99,611.44 ns | 26.84 | 11325 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 100 | NotCancelled | 151,291.92 ns | 40.77 | 61615 B |
AsyncSemaphore
AsyncSemaphore manages a counted resource permit. The single-permit benchmark measures the wait+release cycle uncontested (the permit is available, so WaitAsync takes the fast path).
Key observations:
- Pooled: ~30% faster than Windows — the largest relative advantage across all uncontested benchmarks
- ProtoPromise: ~25% faster than Pooled
- System.SemaphoreSlim: ~2× slower than Pooled; ~25% cheaper than on Windows (lighter macOS kernel semaphore)
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| WaitRelease · AsyncSemaphore · ProtoPromise | 4.777 ns | 0.77 | - |
| WaitRelease · AsyncSemaphore · Pooled | 6.203 ns | 1.00 | - |
| WaitRelease · AsyncSemaphore · RefImpl | 11.378 ns | 1.83 | - |
| WaitRelease · AsyncSemaphore · Nito.AsyncEx | 11.417 ns | 1.84 | - |
| WaitRelease · SemaphoreSlim · System | 12.665 ns | 2.04 | - |
AsyncCountdownEvent
AsyncCountdownEvent completes when signaled N times. Two scenarios: SignalAndWait (last signal also unblocks the single waiter atomically), and WaitAndSignal (the waiter is already blocking when signals arrive).
Key observations:
- Pooled (SignalAndWait, P=1): ~28% faster than Windows
- System.CountdownEvent: near-identical on both platforms — kernel countdown objects normalize across architectures
- At P=10, all implementations are within 2× of each other; Pooled matches System.CountdownEvent
| Description | ParticipantCount | Mean | Ratio | Allocated |
|---|---|---|---|---|
| SignalAndWait · AsyncCountdownEv · ProtoPromise | 1 | 4.281 ns | 0.77 | - |
| SignalAndWait · AsyncCountdownEv · Pooled | 1 | 5.573 ns | 1.00 | - |
| SignalAndWait · CountdownEvent · System | 1 | 6.662 ns | 1.20 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 1 | 13.377 ns | 2.40 | 96 B |
| WaitAndSignal · AsyncCountdownEv · ProtoPromise | 1 | 13.752 ns | 2.47 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 1 | 35.327 ns | 6.34 | - |
| SignalAndWait · AsyncCountdownEv · ProtoPromise | 10 | 15.227 ns | 0.70 | - |
| SignalAndWait · CountdownEvent · System | 10 | 16.373 ns | 0.75 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 10 | 20.918 ns | 0.96 | 96 B |
| SignalAndWait · AsyncCountdownEv · Pooled | 10 | 21.765 ns | 1.00 | - |
| WaitAndSignal · AsyncCountdownEv · ProtoPromise | 10 | 22.452 ns | 1.03 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 10 | 46.098 ns | 2.12 | - |
AsyncBarrier
AsyncBarrier blocks all N participants until all have signaled, then releases them simultaneously. The SignalAndWait benchmark measures the end-to-end barrier phase cycle.
Key observations:
- Pooled at P=1: ~39% faster than Windows
- System.Barrier at P=1: ~160× slower than Pooled; the ratio vs Pooled is much worse here than on Windows (~42×);
System.Barrierperforms full OS thread synchronization internally - At P=10: Pooled ~23% faster than Windows; System.Barrier ~2.8× cheaper than on Windows at P=10
| Description | ParticipantCount | Mean | Ratio | Allocated |
|---|---|---|---|---|
| SignalAndWait · AsyncBarrier · Pooled | 1 | 7.495 ns | 1.00 | - |
| SignalAndWait · Barrier · System | 1 | 1,049.935 ns | 140.09 | 238 B |
| SignalAndWait · AsyncBarrier · RefImpl | 1 | 1,674.585 ns | 223.44 | 8470 B |
| SignalAndWait · AsyncBarrier · Pooled | 10 | 201.242 ns | 1.00 | - |
| SignalAndWait · AsyncBarrier · RefImpl | 10 | 1,852.928 ns | 9.21 | 8693 B |
| SignalAndWait · Barrier · System | 10 | 18,894.387 ns | 93.89 | 1392 B |
AsyncReaderWriterLock
AsyncReaderWriterLock supports multiple concurrent readers or a single exclusive writer, plus an upgradeable reader that can atomically promote to writer. Benchmarks cover four access patterns:
- WriterLock — exclusive write acquire+release (uncontested)
- ReaderLock — shared read acquire+release at N concurrent readers
- UpgradeableReaderLock — upgradeable acquire at N concurrent upgradeables
- UpgradedWriterLock — upgrade from upgradeable to writer at N concurrent upgradeables
WriterLock (uncontested)
Key observations:
- Pooled and ProtoPromise: near parity — fastest async writer lock on this platform
- Both are ~34% faster than Windows for the Pooled path
- System.ReaderWriterLockSlim: ~35% faster than Pooled (synchronous acquire path); ~33% faster than Windows equivalent
- VS.Threading: async overhead ~215× vs Pooled due to its queuing model
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| WriterLock · RWLockSlim · System | 4.635 ns | 0.61 | - |
| WriterLock · AsyncRWLock · Proto.Promises | 6.804 ns | 0.89 | - |
| WriterLock · AsyncRWLock · Pooled | 7.614 ns | 1.00 | - |
| WriterLock · AsyncRWLock · RefImpl | 12.101 ns | 1.59 | - |
| WriterLock · AsyncRWLock · Nito.AsyncEx | 57.229 ns | 7.52 | 496 B |
| WriterLock · AsyncRWLock · VS.Threading | 1,382.823 ns | 181.62 | 584 B |
ReaderLock
The reader lock benchmark reveals a notable contention inversion between platforms:
- Uncontested (0 concurrent readers): M4 ~2× faster than Windows — the strongest single uncontested advantage across all threading benchmarks
- 1 concurrent reader: Windows ~32% faster with a single contending reader; scheduling a resume on macOS ThreadPool costs more for the RWLock resumption path
- 100 concurrent readers: Windows ~2× faster at high contention
- ProtoPromise at 100 readers: macOS dramatically outperforms Windows (~0.13 vs 0.73 ratio to Pooled) — ProtoPromise's lock-free reader promotion is particularly effective at broadcast-style reader release on M4
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| ReaderLock · RWLockSlim · System | 0 | None | 5.888 ns | 0.64 | - |
| ReaderLock · AsyncRWLock · Pooled | 0 | None | 9.213 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 11.173 ns | 1.21 | - |
| ReaderLock · AsyncRWLock · RefImpl | 0 | None | 14.129 ns | 1.53 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | None | 41.797 ns | 4.54 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | None | 168.990 ns | 18.34 | 208 B |
| ReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 9.321 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 11.602 ns | 1.24 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | NotCancelled | 41.122 ns | 4.41 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 169.890 ns | 18.23 | 208 B |
| ReaderLock · RWLockSlim · System | 1 | None | 10.176 ns | 0.22 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 15.947 ns | 0.35 | - |
| ReaderLock · AsyncRWLock · RefImpl | 1 | None | 23.278 ns | 0.51 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | None | 46.083 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | None | 82.708 ns | 1.79 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | None | 393.761 ns | 8.54 | 416 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 16.695 ns | 0.37 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 45.499 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | NotCancelled | 82.226 ns | 1.81 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 392.648 ns | 8.63 | 416 B |
| ReaderLock · RWLockSlim · System | 10 | None | 51.441 ns | 0.16 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | None | 53.245 ns | 0.17 | - |
| ReaderLock · AsyncRWLock · RefImpl | 10 | None | 113.986 ns | 0.35 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | None | 321.143 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | None | 465.151 ns | 1.45 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | None | 2,890.460 ns | 9.00 | 2288 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | NotCancelled | 54.657 ns | 0.18 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | NotCancelled | 310.092 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | NotCancelled | 469.412 ns | 1.51 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | NotCancelled | 2,874.892 ns | 9.27 | 2288 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | None | 458.458 ns | 0.16 | - |
| ReaderLock · RWLockSlim · System | 100 | None | 482.223 ns | 0.17 | - |
| ReaderLock · AsyncRWLock · RefImpl | 100 | None | 985.528 ns | 0.35 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | None | 2,828.670 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | None | 4,126.229 ns | 1.46 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | None | 74,059.595 ns | 26.20 | 21008 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | NotCancelled | 450.691 ns | 0.17 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | NotCancelled | 2,713.733 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | NotCancelled | 4,114.507 ns | 1.52 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | NotCancelled | 74,958.026 ns | 27.62 | 21008 B |
UpgradeableReaderLock
Same contention inversion pattern as reader lock:
- Uncontested: M4 ~2× faster than Windows
- System.ReaderWriterLockSlim upgradeable: ~40% faster than Pooled uncontested; ~30% cheaper than on Windows
- VS.Threading upgradeable: ~1.4× slower than Windows uncontested due to lock-free upgradeable state tracking differences; allocates 616 B per acquire
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| UpgradeableReaderLock · RWLockSlim · System | 0 | None | 4.973 ns | 0.57 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | None | 8.719 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 12.361 ns | 1.42 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | None | 1,389.283 ns | 159.35 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 8.703 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 12.282 ns | 1.41 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,457.782 ns | 167.50 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 1 | None | 4.973 ns | 0.12 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 10.892 ns | 0.26 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | None | 41.941 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | None | 1,452.258 ns | 34.66 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 10.977 ns | 0.26 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 41.459 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 1,451.141 ns | 35.01 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 2 | None | 4.969 ns | 0.12 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | None | 10.851 ns | 0.26 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | None | 41.628 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | None | 1,363.634 ns | 32.76 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 10.840 ns | 0.27 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | NotCancelled | 40.608 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 1,411.681 ns | 34.78 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 5 | None | 19.236 ns | 0.14 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | None | 22.858 ns | 0.17 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | None | 137.638 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | None | 2,130.834 ns | 15.48 | 1240 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 23.752 ns | 0.18 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | NotCancelled | 130.160 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 2,157.943 ns | 16.58 | 1240 B |
UpgradedWriterLock
The upgraded writer lock holds an upgradeable reader then atomically promotes to exclusive writer, requiring all active readers to drain first.
- Uncontested: M4 ~32% faster than Windows
- 1 concurrent upgraded writer: Windows ~30% faster (same inversion as reader lock)
- 5 concurrent upgraded writers: Windows ~2.5× faster — writer upgrade queuing is the scenario where Windows ThreadPool's Task scheduling advantage is most visible
| Description | Iterations | cancellationType | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| UpgradedWriterLock · RWLockSlim · System | 0 | None | 10.52 ns | 0.62 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | None | 16.94 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | None | 21.61 ns | 1.28 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | None | 1,552.73 ns | 91.69 | 824 B |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | NotCancelled | 16.89 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 18.75 ns | 1.11 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,651.64 ns | 97.78 | 824 B |
| UpgradedWriterLock · RWLockSlim · System | 1 | None | 16.72 ns | 0.23 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | None | 30.60 ns | 0.41 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | None | 74.10 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | None | 1,779.19 ns | 24.01 | 1032 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 42.98 ns | 0.62 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | NotCancelled | 68.81 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 1,945.10 ns | 28.27 | 1032 B |
| UpgradedWriterLock · RWLockSlim · System | 2 | None | 21.38 ns | 0.16 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | None | 34.28 ns | 0.25 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | None | 135.45 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | None | 2,147.03 ns | 15.85 | 1240 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 48.11 ns | 0.37 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | NotCancelled | 129.10 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 2,360.20 ns | 18.28 | 1240 B |
| UpgradedWriterLock · RWLockSlim · System | 5 | None | 34.02 ns | 0.11 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | None | 47.45 ns | 0.15 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | None | 311.51 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | None | 3,378.35 ns | 10.85 | 1864 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 58.82 ns | 0.19 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | NotCancelled | 304.04 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 3,474.43 ns | 11.43 | 1864 B |