1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
//! What WFE is, what it costs, and the one rule for when to emit it.
//!
//! # Why this file exists
//!
//! `benches/locks.rs` prices five locks. This asserts the platform facts that
//! benchmark rests on, so a change in the hardware, the toolchain or the OS
//! surfaces here as a named failure rather than as a benchmark that quietly
//! means something different.
//!
//! Everything below was measured on aarch64-apple-darwin, 16 cores, and every
//! bound is deliberately loose: these tests exist to catch a property
//! disappearing, not to pin a number.
//!
//! # The instruction
//!
//! `WFE` puts a core in a low-power state until its event register is set.
//! `SEV` sets it on every core. ARM documents this as *the* intended spinlock
//! construction: a waiter executes WFE to request a low-power state, and the
//! releaser executes SEV to wake it.
//!
//! Available on every ARMv7-and-later core, so every ARM64 chip: Apple
//! Silicon, AWS Graviton 2/3/4 (Neoverse N1/V1/V2), Ampere Altra, Raspberry Pi
//! 4/5. x86 has the same idea split by privilege: `MONITOR`/`MWAIT` is ring 0,
//! `UMONITOR`/`UMWAIT` is ring 3 from Intel Tremont (2020) onward, and AMD has
//! `MONITORX`/`MWAITX`.
//!
//! # Rust never emits it
//!
//! `core::hint::spin_loop()` on aarch64 is `__isb(SY)`, an instruction
//! barrier. Disassembled:
//!
//! ```text
//! __RNvCs8LfLpYhzmc_7hintasm1s:
//! isb
//! ret
//! ```
//!
//! There is no WFE anywhere in `core::hint`, and no wrapper in
//! `core::arch::aarch64`. That is structural rather than an oversight: WFE is
//! only useful if the *releaser* pairs it with SEV, and that is a protocol
//! between both sides of a lock. A one-sided hint cannot express one, so it has
//! to be written in the lock, where both sides are known.
//!
//! # What it costs here
//!
//! ```text
//! nop 0.3 ns
//! isb (spin_loop) 8.6 ns
//! wfe, event pending 1336.7 ns
//! ```
//!
//! So WFE is not a userspace no-op on this machine, and it does not block
//! forever either: it idles for roughly 1.3 us and returns unprompted. A
//! waiter therefore needs no SEV to make progress, and burns about 150x less
//! CPU per unit of wall time waited than an `isb` spin.
//!
//! # The rule, which is the useful part
//!
//! **WFE idles the core. It does not yield to the operating system.**
//!
//! That is why it lost here. With 128 threads on 16 cores a WFE-waiting lock
//! cost 5584 ms of CPU against 1241 ms for a yielding spinner: a waiter idling
//! in WFE is still a *scheduled* thread, so the lock holder still cannot get a
//! core. Below the core count it is level with plain spinning and no better.
//!
//! An earlier version of that lock restarted its spin schedule after every
//! WFE, so it yielded between idles. That accident is the only reason it ever
//! looked competitive, and finding it is why this rule is written down.
//!
//! So emit WFE when:
//!
//! * threads are at most cores, so idling one costs nothing that is wanted;
//! * there is no operating system to yield to, which is exactly when every
//! other option reduces to burning the core;
//! * waits are long enough that 1.3 us of idle beats 8.6 ns of `isb` repeated
//! until the holder finishes.
//!
//! Bare metal and pinned threads. Not an oversubscribed host.
//!
//! # The result above is macOS-specific, and probably inverts on AWS
//!
//! Under KVM, WFE is trapped by the hypervisor (the `TWE` bit) and KVM yields
//! the vCPU. The Linux commit is literally "arm64: KVM: Yield CPU when vcpu
//! executes a WFE", written for the same pathology measured here, where
//! spinning vCPUs hold the cores a lock holder needs and hackbench slows by
//! 40x. That trap supplies the missing half: on Graviton, WFE *does* reach a
//! scheduler.
//!
//! Graviton also maps one vCPU to one physical core with no SMT, so a guest is
//! not oversubscribed the way this machine was at 128 threads on 16 cores,
//! which is the regime WFE is for in the first place.
//!
//! **So the negative result here is not portable and must be re-measured on
//! Graviton before anyone concludes WFE is worthless.**
//!
//! # Two findings from the same work, kept so they are not re-derived
//!
//! **`lock_api` is free.** `parking_lot::Mutex` and
//! `lock_api::Mutex<parking_lot::RawMutex, _>` measure the same, so making
//! `concurrent` generic over `R: RawMutex` costs nothing and lets a consumer
//! choose. `lock_arc` and `ArcMutexGuard<R, T>` are `lock_api`'s, not
//! `parking_lot`'s, so `Ref` keeps working either way.
//!
//! **The node lock is not this crate's bottleneck.** With the index `RwLock`
//! held constant, every node lock lands inside the null. An earlier benchmark
//! varied both at once and reported the index lock's behaviour as a node-lock
//! result. The index `RwLock` is what deserves the next look.
//!
//! # Why a compiler should care
//!
//! EKOPathRS is a compiler. A compiler that recognises a spin loop can emit
//! WFE for it, which LLVM does not do. The measurement above prices that at
//! 150x less CPU per unit waited, and the rule above says when it would be
//! wrong, which is the half that makes it safe to automate.
//!
//! # Related work, recent
//!
//! * HTLL, "Latency-Aware Scalable Blocking Mutex", IEEE TPDS, January 2025:
//! throughput and latency together under oversubscription, up to 97% latency
//! reduction for about 5% throughput.
//! * Fissile Locks, arXiv 2003.05025: compact, NUMA-aware, preemption tolerant.
//! * Asymmetry-aware Scalable Locking, arXiv 2108.03355. Directly relevant
//! here, because this is a P-core/E-core machine and neither the benchmark
//! nor these tests separate them.
use black_box;
use Instant;
/// Iterations per timing loop. Large enough that a nanosecond-scale
/// instruction is measurable over timer noise.
const ITERS: u64 = 200_000;
/// WFE must be reachable at all from userspace, or none of this applies.
///
/// If this ever traps or is emulated away, every other claim in this file is
/// about a different machine.
/// WFE is not a no-op here, which is the whole reason it is worth emitting.
///
/// An implementation is free to make WFE a `nop`, and on such a machine a
/// WFE-based lock is a plain spinlock wearing a costume. This separates the
/// two: `nop` measured 0.3 ns and WFE measured 1336.7 ns, so anything within
/// an order of magnitude of `nop` means WFE is not idling.
/// `core::hint::spin_loop()` is not WFE, and the gap is why the lock has to
/// write the instruction itself.
///
/// `spin_loop()` is `__isb(SY)`, measured at 8.6 ns against WFE's 1336.7. If
/// these ever converge, either Rust started emitting WFE, in which case a lock
/// should stop hand-writing it, or WFE stopped idling.
/// WFE returns on its own, so a waiter cannot deadlock if SEV is missed.
///
/// This is what makes a WFE lock safe to write: the event register may already
/// be clear, or a SEV may land before the waiter reaches its WFE, and neither
/// wedges. Measured at roughly 1.3 us per WFE, so a hundred of them is
/// bounded well under a second on any machine where WFE idles at all.
/// The rule, as an executable statement: WFE does not yield to the scheduler.
///
/// Two threads per core, one holding a lock for a long stretch. If WFE reached
/// the scheduler, waiters would stand aside and this would finish in about the
/// time the holders need. It does not, which is why the rule is "threads at
/// most cores".
///
/// **Expected to be different under KVM**, where WFE traps and the hypervisor
/// yields the vCPU. On Graviton this test is the one to watch: if it starts
/// passing comfortably there, WFE became viable for oversubscribed hosts and
/// the guidance in this file needs revisiting.