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.3.1 (a) (25D771280a) [Darwin 25.3.0]
Apple M4, 1 CPU, 10 logical and 10 physical cores
.NET SDK 10.0.201
[Host] : .NET 10.0.5 (10.0.5, 10.0.526.15411), Arm64 RyuJIT armv8.0-a
.NET 10.0 : .NET 10.0.5 (10.0.5, 10.0.526.15411), 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.4435 ns | 0.059 | - |
| Lock · Interlocked.Add · System | 0.4573 ns | 0.060 | - |
| Lock · Interlocked.Inc · System | 0.4583 ns | 0.061 | - |
| Lock · Interlocked.CmpX · System | 2.4091 ns | 0.318 | - |
| Lock · Lock · System | 2.6142 ns | 0.345 | - |
| Lock · Lock.EnterScope · System | 2.6513 ns | 0.350 | - |
| SpinLock · SpinLock · CryptoHives | 2.7340 ns | 0.361 | - |
| Lock · lock() · System | 3.2074 ns | 0.423 | - |
| LockAsync · AsyncLock · ProtoPromise | 6.6302 ns | 0.875 | - |
| LockAsync · AsyncLock · Pooled | 7.5745 ns | 1.000 | - |
| LockAsync · AsyncSemaphore · VS.Threading | 11.6188 ns | 1.534 | - |
| LockAsync · AsyncLock · RefImpl | 11.7976 ns | 1.558 | - |
| LockAsync · SemaphoreSlim · System | 12.7817 ns | 1.688 | - |
| LockAsync · AsyncLock · NonKeyed | 16.6770 ns | 2.202 | - |
| LockAsync · AsyncLock · Nito.AsyncEx | 40.3838 ns | 5.332 | 320 B |
| SpinWait · SpinOnce · System | 47.4733 ns | 6.268 | - |
| LockAsync · AsyncLock · NeoSmart | 49.8415 ns | 6.580 | 208 B |
| SpinLock · SpinLock · System | 53.7720 ns | 7.099 | - |
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 | 7.891 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 0 | None | 9.628 ns | 1.22 | - |
| Multiple · AsyncLock · Pooled (Task) | 0 | None | 9.871 ns | 1.25 | - |
| Multiple · AsyncSemaphore · VS.Threading | 0 | None | 12.949 ns | 1.64 | - |
| Multiple · AsyncLock · RefImpl | 0 | None | 13.427 ns | 1.70 | - |
| Multiple · SemaphoreSlim · System | 0 | None | 14.463 ns | 1.83 | - |
| Multiple · AsyncLock · NonKeyed | 0 | None | 18.020 ns | 2.28 | - |
| Multiple · AsyncLock · Nito | 0 | None | 41.402 ns | 5.25 | 320 B |
| Multiple · AsyncLock · NeoSmart | 0 | None | 52.997 ns | 6.72 | 208 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 0 | NotCancelled | 8.057 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 0 | NotCancelled | 10.232 ns | 1.27 | - |
| Multiple · AsyncLock · Pooled (Task) | 0 | NotCancelled | 10.256 ns | 1.27 | - |
| Multiple · AsyncSemaphore · VS.Threading | 0 | NotCancelled | 12.621 ns | 1.57 | - |
| Multiple · SemaphoreSlim · System | 0 | NotCancelled | 14.560 ns | 1.81 | - |
| Multiple · AsyncLock · NonKeyed | 0 | NotCancelled | 18.759 ns | 2.33 | - |
| Multiple · AsyncLock · Nito | 0 | NotCancelled | 41.284 ns | 5.12 | 320 B |
| Multiple · AsyncLock · NeoSmart | 0 | NotCancelled | 52.493 ns | 6.52 | 208 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 1 | None | 23.490 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 1 | None | 28.130 ns | 1.20 | - |
| Multiple · SemaphoreSlim · System | 1 | None | 37.045 ns | 1.58 | 88 B |
| Multiple · AsyncSemaphore · VS.Threading | 1 | None | 54.599 ns | 2.32 | 168 B |
| Multiple · AsyncLock · RefImpl | 1 | None | 68.697 ns | 2.92 | 216 B |
| Multiple · AsyncLock · Nito | 1 | None | 94.316 ns | 4.02 | 728 B |
| Multiple · AsyncLock · NeoSmart | 1 | None | 109.366 ns | 4.66 | 416 B |
| Multiple · AsyncLock · NonKeyed | 1 | None | 1,216.363 ns | 51.79 | 350 B |
| Multiple · AsyncLock · Pooled (Task) | 1 | None | 1,381.198 ns | 58.80 | 271 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 1 | NotCancelled | 36.277 ns | 1.00 | - |
| Multiple · AsyncLock · ProtoPromise | 1 | NotCancelled | 44.412 ns | 1.22 | - |
| Multiple · AsyncSemaphore · VS.Threading | 1 | NotCancelled | 63.203 ns | 1.74 | 168 B |
| Multiple · AsyncLock · NeoSmart | 1 | NotCancelled | 107.268 ns | 2.96 | 416 B |
| Multiple · AsyncLock · Nito | 1 | NotCancelled | 646.678 ns | 17.83 | 968 B |
| Multiple · AsyncLock · Pooled (Task) | 1 | NotCancelled | 1,484.811 ns | 40.93 | 272 B |
| Multiple · AsyncLock · NonKeyed | 1 | NotCancelled | 1,498.509 ns | 41.31 | 640 B |
| Multiple · SemaphoreSlim · System | 1 | NotCancelled | 1,598.035 ns | 44.05 | 504 B |
| Multiple · AsyncLock · ProtoPromise | 10 | None | 197.073 ns | 0.82 | - |
| Multiple · AsyncLock · Pooled (ValueTask) | 10 | None | 240.851 ns | 1.00 | - |
| Multiple · SemaphoreSlim · System | 10 | None | 253.671 ns | 1.05 | 880 B |
| Multiple · AsyncSemaphore · VS.Threading | 10 | None | 451.128 ns | 1.87 | 1680 B |
| Multiple · AsyncLock · Nito | 10 | None | 565.469 ns | 2.35 | 4400 B |
| Multiple · AsyncLock · RefImpl | 10 | None | 567.840 ns | 2.36 | 2160 B |
| Multiple · AsyncLock · NeoSmart | 10 | None | 578.067 ns | 2.40 | 2288 B |
| Multiple · AsyncLock · NonKeyed | 10 | None | 7,081.010 ns | 29.40 | 2296 B |
| Multiple · AsyncLock · Pooled (Task) | 10 | None | 8,263.349 ns | 34.31 | 1352 B |
| Multiple · AsyncLock · ProtoPromise | 10 | NotCancelled | 339.757 ns | 0.90 | - |
| Multiple · AsyncLock · Pooled (ValueTask) | 10 | NotCancelled | 377.674 ns | 1.00 | - |
| Multiple · AsyncSemaphore · VS.Threading | 10 | NotCancelled | 552.628 ns | 1.46 | 1680 B |
| Multiple · AsyncLock · NeoSmart | 10 | NotCancelled | 572.761 ns | 1.52 | 2288 B |
| Multiple · AsyncLock · Nito | 10 | NotCancelled | 5,022.693 ns | 13.30 | 6800 B |
| Multiple · AsyncLock · Pooled (Task) | 10 | NotCancelled | 8,764.494 ns | 23.21 | 1352 B |
| Multiple · SemaphoreSlim · System | 10 | NotCancelled | 10,892.329 ns | 28.84 | 3888 B |
| Multiple · AsyncLock · NonKeyed | 10 | NotCancelled | 10,994.378 ns | 29.11 | 5176 B |
| Multiple · AsyncLock · ProtoPromise | 100 | None | 1,824.031 ns | 0.81 | - |
| Multiple · SemaphoreSlim · System | 100 | None | 2,212.582 ns | 0.98 | 8800 B |
| Multiple · AsyncLock · Pooled (ValueTask) | 100 | None | 2,252.599 ns | 1.00 | - |
| Multiple · AsyncSemaphore · VS.Threading | 100 | None | 4,467.492 ns | 1.98 | 21120 B |
| Multiple · AsyncLock · NeoSmart | 100 | None | 4,974.090 ns | 2.21 | 21008 B |
| Multiple · AsyncLock · RefImpl | 100 | None | 5,289.120 ns | 2.35 | 21600 B |
| Multiple · AsyncLock · Nito | 100 | None | 5,310.252 ns | 2.36 | 41120 B |
| Multiple · AsyncLock · NonKeyed | 100 | None | 51,740.124 ns | 22.97 | 21738 B |
| Multiple · AsyncLock · Pooled (Task) | 100 | None | 52,126.799 ns | 23.14 | 12158 B |
| Multiple · AsyncLock · ProtoPromise | 100 | NotCancelled | 3,179.227 ns | 0.90 | - |
| Multiple · AsyncLock · Pooled (ValueTask) | 100 | NotCancelled | 3,519.580 ns | 1.00 | - |
| Multiple · AsyncLock · NeoSmart | 100 | NotCancelled | 5,035.855 ns | 1.43 | 21008 B |
| Multiple · AsyncSemaphore · VS.Threading | 100 | NotCancelled | 5,525.962 ns | 1.57 | 21120 B |
| Multiple · AsyncLock · Pooled (Task) | 100 | NotCancelled | 60,399.318 ns | 17.16 | 12155 B |
| Multiple · AsyncLock · Nito | 100 | NotCancelled | 62,085.556 ns | 17.64 | 65120 B |
| Multiple · SemaphoreSlim · System | 100 | NotCancelled | 81,425.204 ns | 23.14 | 37736 B |
| Multiple · AsyncLock · NonKeyed | 100 | NotCancelled | 93,239.964 ns | 26.50 | 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 · ProtoPromise | 0.3773 ns | 0.83 | - |
| Set · AsyncAutoReset · Pooled | 0.4524 ns | 1.00 | - |
| Set · AsyncAutoReset · RefImpl | 2.9948 ns | 6.62 | - |
| Set · AsyncAutoReset · Nito.AsyncEx | 3.3905 ns | 7.49 | - |
| Set · AutoResetEvent · System | 55.3979 ns | 122.45 | - |
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.628 ns | 0.77 | - |
| SetThenWait · AsyncAutoReset · Pooled (ValueTask) | 4.714 ns | 1.00 | - |
| SetThenWait · AsyncAutoReset · Pooled (AsTask) | 5.894 ns | 1.25 | - |
| SetThenWait · AsyncAutoReset · RefImpl | 9.885 ns | 2.10 | - |
| SetThenWait · AsyncAutoReset · Nito.AsyncEx | 10.082 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 | 17.96 ns | 0.84 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 1 | None | 18.43 ns | 0.86 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 1 | None | 18.49 ns | 0.86 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 1 | None | 21.41 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 1 | None | 21.58 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 1 | None | 24.44 ns | 1.14 | 96 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 1 | None | 29.64 ns | 1.38 | 80 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 1 | None | 31.51 ns | 1.47 | 160 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 1 | None | 1,230.34 ns | 57.47 | 229 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 1 | NotCancelled | 29.64 ns | 0.88 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 1 | NotCancelled | 29.84 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 1 | NotCancelled | 30.59 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 1 | NotCancelled | 32.65 ns | 0.97 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 1 | NotCancelled | 33.57 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 1 | NotCancelled | 41.08 ns | 1.22 | 80 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 1 | NotCancelled | 628.48 ns | 18.72 | 400 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 1 | NotCancelled | 1,369.58 ns | 40.80 | 232 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 2 | None | 34.51 ns | 0.71 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 2 | None | 43.38 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 2 | None | 45.39 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 2 | None | 47.53 ns | 0.97 | 192 B |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 2 | None | 48.84 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 2 | None | 49.37 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 2 | None | 60.79 ns | 1.24 | 320 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 2 | None | 72.14 ns | 1.48 | 160 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 2 | None | 1,814.28 ns | 37.15 | 342 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 2 | NotCancelled | 61.65 ns | 0.86 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 2 | NotCancelled | 67.33 ns | 0.94 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 2 | NotCancelled | 68.59 ns | 0.95 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 2 | NotCancelled | 71.95 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 2 | NotCancelled | 72.46 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 2 | NotCancelled | 94.77 ns | 1.32 | 160 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 2 | NotCancelled | 1,071.33 ns | 14.89 | 800 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 2 | NotCancelled | 1,935.44 ns | 26.90 | 344 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 10 | None | 165.84 ns | 0.64 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 10 | None | 230.77 ns | 0.89 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 10 | None | 235.56 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 10 | None | 237.92 ns | 0.92 | 960 B |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 10 | None | 258.24 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 10 | None | 265.52 ns | 1.03 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 10 | None | 298.09 ns | 1.15 | 1600 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 10 | None | 364.88 ns | 1.41 | 800 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 10 | None | 5,112.86 ns | 19.80 | 1239 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 10 | NotCancelled | 313.55 ns | 0.82 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 10 | NotCancelled | 348.47 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 10 | NotCancelled | 355.19 ns | 0.93 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 10 | NotCancelled | 376.28 ns | 0.99 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 10 | NotCancelled | 381.33 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 10 | NotCancelled | 475.57 ns | 1.25 | 800 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 10 | NotCancelled | 5,104.58 ns | 13.39 | 4000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 10 | NotCancelled | 6,877.33 ns | 18.04 | 1240 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 100 | None | 1,606.21 ns | 0.64 | - |
| WaitThenSet · AsyncAutoReset · RefImpl | 100 | None | 2,147.13 ns | 0.86 | 9600 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 100 | None | 2,157.25 ns | 0.86 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 100 | None | 2,184.42 ns | 0.87 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 100 | None | 2,501.68 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 100 | None | 2,519.45 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 100 | None | 2,873.12 ns | 1.15 | 16000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 100 | None | 3,552.41 ns | 1.42 | 8000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 100 | None | 33,795.11 ns | 13.51 | 11319 B |
| WaitThenSet · AsyncAutoReset · ProtoPromise | 100 | NotCancelled | 3,062.41 ns | 0.83 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask SyncCont) | 100 | NotCancelled | 3,339.76 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsValueTask) | 100 | NotCancelled | 3,360.55 ns | 0.91 | - |
| WaitThenSet · AsyncAutoReset · Pooled (ValueTask) | 100 | NotCancelled | 3,679.41 ns | 1.00 | - |
| WaitThenSet · AsyncAutoReset · Pooled (SyncCont) | 100 | NotCancelled | 3,722.32 ns | 1.01 | - |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask SyncCont) | 100 | NotCancelled | 4,558.07 ns | 1.24 | 8000 B |
| WaitThenSet · AsyncAutoReset · Nito.AsyncEx | 100 | NotCancelled | 61,744.88 ns | 16.78 | 40000 B |
| WaitThenSet · AsyncAutoReset · Pooled (AsTask) | 100 | NotCancelled | 112,610.58 ns | 30.61 | 11326 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 | 1.009 ns | 0.62 | - |
| SetReset · AsyncManualReset · Pooled | 1.616 ns | 1.00 | - |
| SetReset · ManualResetEventSlim · System | 6.662 ns | 4.12 | - |
| SetReset · AsyncManualReset · RefImpl | 9.369 ns | 5.80 | 96 B |
| SetReset · AsyncManualReset · Nito.AsyncEx | 15.199 ns | 9.40 | 96 B |
| SetReset · ManualResetEvent · System | 108.067 ns | 66.87 | - |
SetThenWait
| Description | Mean | Ratio | Allocated |
|---|---|---|---|
| SetThenWait · AsyncManualReset · ProtoPromise | 4.203 ns | 0.65 | - |
| SetThenWait · AsyncManualReset · Pooled (AsTask) | 5.999 ns | 0.92 | - |
| SetThenWait · AsyncManualReset · Pooled (ValueTask) | 6.505 ns | 1.00 | - |
| SetThenWait · AsyncManualReset · RefImpl | 12.832 ns | 1.97 | 96 B |
| SetThenWait · AsyncManualReset · Nito.AsyncEx | 21.813 ns | 3.35 | 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.47 ns | 0.81 | 96 B |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 1 | None | 17.19 ns | 0.84 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 1 | None | 17.54 ns | 0.86 | - |
| WaitThenSet · AsyncManualReset · ProtoPromise | 1 | None | 18.54 ns | 0.91 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 1 | None | 20.44 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 1 | None | 20.75 ns | 1.02 | - |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 1 | None | 26.62 ns | 1.30 | 96 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 1 | None | 28.74 ns | 1.41 | 80 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 1 | None | 1,291.26 ns | 63.19 | 230 B |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 1 | NotCancelled | 28.78 ns | 0.91 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 1 | NotCancelled | 29.50 ns | 0.93 | - |
| WaitThenSet · AsyncManualReset · ProtoPromise | 1 | NotCancelled | 31.40 ns | 0.99 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 1 | NotCancelled | 31.65 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 1 | NotCancelled | 31.71 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 1 | NotCancelled | 41.95 ns | 1.32 | 80 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 1 | NotCancelled | 1,443.13 ns | 45.52 | 808 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 1 | NotCancelled | 1,496.60 ns | 47.21 | 232 B |
| WaitThenSet · AsyncManualReset · RefImpl | 2 | None | 20.27 ns | 0.44 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 2 | None | 33.62 ns | 0.73 | - |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 2 | None | 34.65 ns | 0.76 | 96 B |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 2 | None | 42.09 ns | 0.92 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 2 | None | 43.47 ns | 0.95 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 2 | None | 43.78 ns | 0.95 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 2 | None | 45.88 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 2 | None | 65.24 ns | 1.42 | 160 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 2 | None | 1,777.86 ns | 38.76 | 343 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 2 | NotCancelled | 62.17 ns | 0.92 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 2 | NotCancelled | 65.48 ns | 0.97 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 2 | NotCancelled | 66.38 ns | 0.98 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 2 | NotCancelled | 67.42 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 2 | NotCancelled | 68.71 ns | 1.02 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 2 | NotCancelled | 103.73 ns | 1.54 | 160 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 2 | NotCancelled | 2,228.65 ns | 33.06 | 344 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 2 | NotCancelled | 2,316.48 ns | 34.36 | 1488 B |
| WaitThenSet · AsyncManualReset · RefImpl | 10 | None | 58.16 ns | 0.23 | 96 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 10 | None | 100.26 ns | 0.40 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 10 | None | 159.08 ns | 0.64 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 10 | None | 212.14 ns | 0.85 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 10 | None | 217.21 ns | 0.87 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 10 | None | 248.87 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 10 | None | 250.88 ns | 1.01 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 10 | None | 338.66 ns | 1.36 | 800 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 10 | None | 6,269.22 ns | 25.19 | 1240 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 10 | NotCancelled | 303.05 ns | 0.80 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 10 | NotCancelled | 344.64 ns | 0.91 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 10 | NotCancelled | 347.10 ns | 0.92 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 10 | NotCancelled | 376.76 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 10 | NotCancelled | 377.14 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 10 | NotCancelled | 477.58 ns | 1.27 | 800 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 10 | NotCancelled | 6,880.06 ns | 18.24 | 6464 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 10 | NotCancelled | 7,417.03 ns | 19.67 | 1240 B |
| WaitThenSet · AsyncManualReset · RefImpl | 100 | None | 460.79 ns | 0.19 | 96 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 100 | None | 806.83 ns | 0.33 | 96 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 100 | None | 1,457.80 ns | 0.60 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 100 | None | 2,082.27 ns | 0.86 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 100 | None | 2,088.38 ns | 0.86 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 100 | None | 2,413.12 ns | 0.99 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 100 | None | 2,428.53 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 100 | None | 3,272.00 ns | 1.35 | 8000 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 100 | None | 32,830.61 ns | 13.52 | 11320 B |
| WaitThenSet · AsyncManualReset · ProtoPromise | 100 | NotCancelled | 2,838.95 ns | 0.78 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask) | 100 | NotCancelled | 3,284.09 ns | 0.90 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsValueTask SyncCont) | 100 | NotCancelled | 3,321.15 ns | 0.91 | - |
| WaitThenSet · AsyncManualReset · Pooled (SyncCont) | 100 | NotCancelled | 3,575.24 ns | 0.98 | - |
| WaitThenSet · AsyncManualReset · Pooled (ValueTask) | 100 | NotCancelled | 3,642.58 ns | 1.00 | - |
| WaitThenSet · AsyncManualReset · Pooled (AsTask SyncCont) | 100 | NotCancelled | 4,450.96 ns | 1.22 | 8000 B |
| WaitThenSet · AsyncManualReset · Pooled (AsTask) | 100 | NotCancelled | 103,311.74 ns | 28.36 | 11327 B |
| WaitThenSet · AsyncManualReset · Nito.AsyncEx | 100 | NotCancelled | 150,459.74 ns | 41.31 | 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.945 ns | 0.77 | - |
| WaitRelease · AsyncSemaphore · Pooled | 6.412 ns | 1.00 | - |
| WaitRelease · AsyncSemaphore · Nito.AsyncEx | 11.604 ns | 1.81 | - |
| WaitRelease · AsyncSemaphore · RefImpl | 11.697 ns | 1.82 | - |
| WaitRelease · SemaphoreSlim · System | 12.635 ns | 1.97 | - |
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.295 ns | 0.74 | - |
| SignalAndWait · AsyncCountdownEv · Pooled | 1 | 5.787 ns | 1.00 | - |
| SignalAndWait · CountdownEvent · System | 1 | 6.401 ns | 1.11 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 1 | 13.402 ns | 2.32 | 96 B |
| WaitAndSignal · AsyncCountdownEv · ProtoPromise | 1 | 14.133 ns | 2.44 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 1 | 34.716 ns | 6.00 | - |
| SignalAndWait · AsyncCountdownEv · ProtoPromise | 10 | 15.277 ns | 0.69 | - |
| SignalAndWait · CountdownEvent · System | 10 | 16.036 ns | 0.72 | - |
| SignalAndWait · AsyncCountdownEv · RefImpl | 10 | 21.003 ns | 0.95 | 96 B |
| SignalAndWait · AsyncCountdownEv · Pooled | 10 | 22.133 ns | 1.00 | - |
| WaitAndSignal · AsyncCountdownEv · ProtoPromise | 10 | 22.524 ns | 1.02 | - |
| WaitAndSignal · AsyncCountdownEv · Pooled | 10 | 45.687 ns | 2.06 | - |
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 | 6.663 ns | 1.00 | - |
| SignalAndWait · Barrier · System | 1 | 1,080.414 ns | 162.15 | 239 B |
| SignalAndWait · AsyncBarrier · RefImpl | 1 | 1,664.424 ns | 249.80 | 8473 B |
| SignalAndWait · AsyncBarrier · Pooled | 10 | 195.938 ns | 1.00 | - |
| SignalAndWait · AsyncBarrier · RefImpl | 10 | 1,848.135 ns | 9.43 | 8691 B |
| SignalAndWait · Barrier · System | 10 | 14,392.134 ns | 73.46 | 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.605 ns | 0.67 | - |
| WriterLock · AsyncRWLock · Proto.Promises | 6.874 ns | 1.00 | - |
| WriterLock · AsyncRWLock · Pooled | 6.881 ns | 1.00 | - |
| WriterLock · AsyncRWLock · RefImpl | 12.124 ns | 1.76 | - |
| WriterLock · AsyncRWLock · Nito.AsyncEx | 58.594 ns | 8.51 | 496 B |
| WriterLock · AsyncRWLock · VS.Threading | 1,481.982 ns | 215.36 | 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 | 6.172 ns | 0.71 | - |
| ReaderLock · AsyncRWLock · Pooled | 0 | None | 8.639 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 11.373 ns | 1.32 | - |
| ReaderLock · AsyncRWLock · RefImpl | 0 | None | 13.955 ns | 1.62 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | None | 42.188 ns | 4.89 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | None | 169.288 ns | 19.60 | 208 B |
| ReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 8.813 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 11.912 ns | 1.35 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 0 | NotCancelled | 41.956 ns | 4.77 | 320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 173.597 ns | 19.72 | 208 B |
| ReaderLock · RWLockSlim · System | 1 | None | 10.560 ns | 0.23 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 16.098 ns | 0.35 | - |
| ReaderLock · AsyncRWLock · RefImpl | 1 | None | 23.725 ns | 0.52 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | None | 45.892 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | None | 84.073 ns | 1.83 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | None | 403.626 ns | 8.80 | 416 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 16.784 ns | 0.37 | - |
| ReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 45.025 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 1 | NotCancelled | 93.791 ns | 2.08 | 640 B |
| ReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 405.231 ns | 9.00 | 416 B |
| ReaderLock · RWLockSlim · System | 10 | None | 53.209 ns | 0.16 | - |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | None | 54.324 ns | 0.16 | - |
| ReaderLock · AsyncRWLock · RefImpl | 10 | None | 114.815 ns | 0.34 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | None | 341.559 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | None | 480.115 ns | 1.41 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | None | 2,924.869 ns | 8.56 | 2288 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 10 | NotCancelled | 54.705 ns | 0.16 | - |
| ReaderLock · AsyncRWLock · Pooled | 10 | NotCancelled | 348.494 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 10 | NotCancelled | 466.506 ns | 1.34 | 3520 B |
| ReaderLock · AsyncRWLock · VS.Threading | 10 | NotCancelled | 2,913.725 ns | 8.36 | 2288 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | None | 452.161 ns | 0.13 | - |
| ReaderLock · RWLockSlim · System | 100 | None | 489.782 ns | 0.14 | - |
| ReaderLock · AsyncRWLock · RefImpl | 100 | None | 991.034 ns | 0.29 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | None | 3,419.210 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | None | 4,250.900 ns | 1.24 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | None | 73,780.822 ns | 21.58 | 21008 B |
| ReaderLock · AsyncRWLock · Proto.Promises | 100 | NotCancelled | 473.818 ns | 0.14 | - |
| ReaderLock · AsyncRWLock · Pooled | 100 | NotCancelled | 3,412.131 ns | 1.00 | - |
| ReaderLock · AsyncRWLock · Nito.AsyncEx | 100 | NotCancelled | 4,264.565 ns | 1.25 | 32320 B |
| ReaderLock · AsyncRWLock · VS.Threading | 100 | NotCancelled | 73,570.658 ns | 21.56 | 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.970 ns | 0.59 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | None | 8.437 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | None | 12.288 ns | 1.46 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | None | 1,509.803 ns | 178.98 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 0 | NotCancelled | 8.484 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 12.314 ns | 1.45 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,552.237 ns | 182.97 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 1 | None | 4.977 ns | 0.14 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | None | 11.034 ns | 0.30 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | None | 36.772 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | None | 1,431.211 ns | 38.93 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 11.031 ns | 0.30 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 1 | NotCancelled | 36.280 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 1,497.189 ns | 41.27 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 2 | None | 4.963 ns | 0.14 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | None | 11.068 ns | 0.30 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | None | 36.723 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | None | 1,524.603 ns | 41.52 | 616 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 11.024 ns | 0.30 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 2 | NotCancelled | 36.582 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 1,537.993 ns | 42.04 | 616 B |
| UpgradeableReaderLock · RWLockSlim · System | 5 | None | 19.204 ns | 0.14 | - |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | None | 22.802 ns | 0.17 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | None | 136.771 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | None | 2,051.705 ns | 15.00 | 1240 B |
| UpgradeableReaderLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 23.691 ns | 0.17 | - |
| UpgradeableReaderLock · AsyncRWLock · Pooled | 5 | NotCancelled | 135.748 ns | 1.00 | - |
| UpgradeableReaderLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 2,151.423 ns | 15.85 | 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.34 ns | 0.67 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | None | 15.45 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | None | 21.60 ns | 1.40 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | None | 1,601.02 ns | 103.64 | 824 B |
| UpgradedWriterLock · AsyncRWLock · Pooled | 0 | NotCancelled | 15.31 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 0 | NotCancelled | 18.73 ns | 1.22 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 0 | NotCancelled | 1,580.17 ns | 103.18 | 824 B |
| UpgradedWriterLock · RWLockSlim · System | 1 | None | 16.29 ns | 0.23 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | None | 31.34 ns | 0.45 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | None | 70.25 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | None | 1,865.61 ns | 26.56 | 1032 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 1 | NotCancelled | 42.41 ns | 0.60 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 1 | NotCancelled | 70.82 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 1 | NotCancelled | 1,903.70 ns | 26.89 | 1032 B |
| UpgradedWriterLock · RWLockSlim · System | 2 | None | 20.74 ns | 0.17 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | None | 35.07 ns | 0.28 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | None | 125.18 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | None | 2,172.35 ns | 17.36 | 1240 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 2 | NotCancelled | 46.73 ns | 0.36 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 2 | NotCancelled | 128.91 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 2 | NotCancelled | 2,287.06 ns | 17.74 | 1240 B |
| UpgradedWriterLock · RWLockSlim · System | 5 | None | 33.92 ns | 0.11 | - |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | None | 46.46 ns | 0.15 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | None | 305.75 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | None | 3,287.80 ns | 10.75 | 1864 B |
| UpgradedWriterLock · AsyncRWLock · Proto.Promises | 5 | NotCancelled | 58.86 ns | 0.19 | - |
| UpgradedWriterLock · AsyncRWLock · Pooled | 5 | NotCancelled | 312.35 ns | 1.00 | - |
| UpgradedWriterLock · AsyncRWLock · VS.Threading | 5 | NotCancelled | 3,500.68 ns | 11.21 | 1864 B |