subetha_cxc/monitor_wait.rs
1//! Monitor-based wait tier: hardware MONITOR/MWAIT-class waiting
2//! between the spin tier and the kernel-park tier.
3//!
4//! The wait ladder this slots into:
5//!
6//! | Tier | Mechanism | Wait scale | Core while waiting | Producer wake cost |
7//! |---|---|---|---|---|
8//! | spin | `PAUSE` loop | ns | busy | free (the store) |
9//! | **monitor (this module)** | `MONITORX`/`MWAITX` (AMD) or `UMONITOR`/`UMWAIT` (WAITPKG) | us, bounded | light sleep (C0.1) | free (the store) |
10//! | park | futex / `_umtx_op` / `WaitOnAddress` | unbounded | released to the OS | one syscall |
11//!
12//! The monitor tier's two properties the other tiers lack:
13//!
14//! - **The producer's wake is free.** The waiter arms a hardware
15//! monitor on the slot's cache line; ANY store to that line trips
16//! it. The producer's existing state-CAS IS the wake - no syscall
17//! on the wake side, unlike every kernel-park mechanism.
18//! - **Monitors are physical-address based** (AMD APM / Intel SDM
19//! MONITOR semantics), so a store from ANOTHER PROCESS that
20//! mapped the same MMF page wakes the waiter. On Windows - where
21//! `WaitOnAddress` is intra-process only - this is the first
22//! non-polling cross-process wake the substrate has.
23//!
24//! What it is NOT: a park. `MWAITX` / `UMWAIT` hold the core in a
25//! shallow sleep state with a hardware deadline; the OS cannot
26//! schedule other work there. The tier therefore takes a bounded
27//! cycle budget and reports `false` on expiry so the caller
28//! escalates to the kernel park.
29//!
30//! # Instruction facts (verified against the Linux kernel's
31//! `arch/x86/include/asm/mwait.h` and the Intel SDM UMWAIT page)
32//!
33//! - `MONITORX`: address in `rAX`, `ECX` = extensions (0),
34//! `EDX` = hints (0). Both extension registers MUST be zero -
35//! nonzero raises #GP, and the Windows x64 ABI happily leaves
36//! argument garbage in `RCX` if the wrapper does not pin it.
37//! - `MWAITX`: `EAX` = hints (0), `EBX` = max wait "expressed in SW
38//! P0 clocks; the software P0 frequency is the same as the TSC
39//! frequency", `ECX` bit 1 = enable the timer.
40//! - `UMONITOR r64`: address operand.
41//! - `UMWAIT r32`: register operand = control (bit 0: 1 = C0.1
42//! shallow/fast wake, 0 = C0.2 deeper; other bits #GP); implicit
43//! `EDX:EAX` = ABSOLUTE TSC deadline; wakes on monitored store,
44//! deadline, or the OS's `IA32_UMWAIT_CONTROL` cap (CF set).
45//! - Detection: MWAITX = CPUID `0x8000_0001` ECX bit 29 (AMD);
46//! WAITPKG = CPUID `7.0` ECX bit 5 (Intel Tiger Lake+ / Sapphire
47//! Rapids+, AMD Zen 5+).
48//!
49//! Both waits can wake spuriously (interrupts trip monitors), so
50//! the loop re-arms until the value changes or the budget expires.
51//!
52//! # Tuning
53//!
54//! - `SUBETHA_NO_MONITOR_WAIT=1` disables the tier (callers fall
55//! straight from spin to park).
56//! - `SUBETHA_MONITOR_WAIT_CYCLES=<n>` overrides the default
57//! per-wait budget ([`DEFAULT_MONITOR_BUDGET_CYCLES`]).
58
59use std::sync::OnceLock;
60use std::sync::atomic::{AtomicU32, Ordering};
61
62use crate::ordering::read_tsc;
63
64/// Default monitor-tier budget in TSC cycles before escalating to
65/// the kernel park: ~25-30 us on contemporary 3-3.5 GHz parts.
66/// Sized to dominate a kernel park+wake round trip (single-digit
67/// us) so waits that resolve quickly never pay the syscall, while
68/// a genuinely idle waiter escalates to the zero-CPU park within
69/// tens of microseconds.
70pub const DEFAULT_MONITOR_BUDGET_CYCLES: u64 = 90_000;
71
72/// Which monitor-wait instruction family this host runs.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum MonitorWaitKind {
75 /// Intel WAITPKG: `UMONITOR` / `UMWAIT` (also AMD Zen 5+).
76 /// Preferred when both families exist: the deadline is an
77 /// absolute TSC value (no u32 clamp) and the C-state hint is
78 /// explicit.
79 Waitpkg,
80 /// AMD `MONITORX` / `MWAITX` (Excavator+, all Zen).
81 Mwaitx,
82 /// AArch64 `LDAXR` + `WFE`: load-exclusive arms the exclusive
83 /// monitor on the line; the global monitor's Exclusive->Open
84 /// transition - ANY store to the line, including from another
85 /// core or process - generates the wake event with no explicit
86 /// `SEV` (ARM barrier-litmus appendix). Base-ISA instructions,
87 /// so every aarch64 host takes this arm; wait granularity is
88 /// bounded by interrupts and the kernel's timer event stream
89 /// rather than a per-wait hardware deadline, and the loop
90 /// enforces the cycle budget on `CNTVCT_EL0`.
91 ArmWfe,
92}
93
94struct MonitorConfig {
95 kind: Option<MonitorWaitKind>,
96 budget_cycles: u64,
97}
98
99fn config() -> &'static MonitorConfig {
100 static CONFIG: OnceLock<MonitorConfig> = OnceLock::new();
101 CONFIG.get_or_init(|| {
102 let disabled = std::env::var_os("SUBETHA_NO_MONITOR_WAIT")
103 .is_some_and(|v| v == "1");
104 let budget_cycles = std::env::var("SUBETHA_MONITOR_WAIT_CYCLES")
105 .ok()
106 .and_then(|v| v.parse().ok())
107 .unwrap_or_else(default_budget_cycles);
108 MonitorConfig {
109 kind: if disabled { None } else { detect_kind() },
110 budget_cycles,
111 }
112 })
113}
114
115/// The monitor-wait family available on this host (`None` when the
116/// CPU exposes neither, when the build target is not x86_64, or
117/// when `SUBETHA_NO_MONITOR_WAIT=1`). Cached after the first call.
118///
119/// Detection is CPUID-trusting: a hypervisor that cannot virtualize
120/// the instructions hides the feature bit, and one that advertises
121/// it must back it. The env kill switch is the escape hatch for a
122/// host that lies.
123pub fn monitor_wait_kind() -> Option<MonitorWaitKind> {
124 config().kind
125}
126
127/// The active per-wait budget in TSC cycles.
128pub fn monitor_wait_budget_cycles() -> u64 {
129 config().budget_cycles
130}
131
132#[cfg(target_arch = "x86_64")]
133fn detect_kind() -> Option<MonitorWaitKind> {
134 use core::arch::x86_64::__cpuid;
135 // WAITPKG: CPUID 7.0 ECX bit 5. Preferred over MWAITX (see
136 // MonitorWaitKind docs). The max-basic-leaf check guards the
137 // leaf-7 read on ancient parts.
138 let max_basic = core::arch::x86_64::__cpuid_count(0, 0).eax;
139 if max_basic >= 7 {
140 let leaf7 = core::arch::x86_64::__cpuid_count(7, 0);
141 if leaf7.ecx & (1 << 5) != 0 {
142 return Some(MonitorWaitKind::Waitpkg);
143 }
144 }
145 // MWAITX: CPUID 0x8000_0001 ECX bit 29, behind the max
146 // extended leaf.
147 let max_extended = __cpuid(0x8000_0000).eax;
148 if max_extended >= 0x8000_0001 {
149 let ext1 = __cpuid(0x8000_0001);
150 if ext1.ecx & (1 << 29) != 0 {
151 return Some(MonitorWaitKind::Mwaitx);
152 }
153 }
154 None
155}
156
157#[cfg(target_arch = "aarch64")]
158fn detect_kind() -> Option<MonitorWaitKind> {
159 // WFE / LDAXR are base A64; no probe needed. A hint-as-NOP
160 // implementation degrades the wait to a budget-bounded spin -
161 // correct, just warmer.
162 Some(MonitorWaitKind::ArmWfe)
163}
164
165#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
166fn detect_kind() -> Option<MonitorWaitKind> {
167 None
168}
169
170/// Per-arch default budget targeting the same ~28 us window:
171/// x86 TSCs tick at GHz rates so the constant works directly;
172/// aarch64's generic timer ticks at `CNTFRQ_EL0` (24 MHz - 1 GHz),
173/// so the budget derives from the reported frequency.
174#[cfg(not(target_arch = "aarch64"))]
175fn default_budget_cycles() -> u64 {
176 DEFAULT_MONITOR_BUDGET_CYCLES
177}
178
179#[cfg(target_arch = "aarch64")]
180fn default_budget_cycles() -> u64 {
181 // 28 us worth of counter ticks; floor of 64 keeps a sane
182 // budget even if the register reads 0 on a broken emulator.
183 (crate::ordering::counter_frequency_hz() * 28 / 1_000_000).max(64)
184}
185
186/// Wait on the monitor tier until `*atomic != expected` or the
187/// cycle budget expires.
188///
189/// Returns `true` when the value changed (the caller's condition
190/// fired) and `false` when the budget expired or the tier is
191/// unavailable - in both `false` cases the caller escalates to its
192/// kernel park, which re-checks the value itself, so a race here
193/// costs one tier transition, never a lost wake.
194///
195/// Lost-wake freedom within the tier comes from the hardware
196/// monitor protocol: arm the monitor FIRST, re-check the value,
197/// then wait. A store that lands between the re-check and the wait
198/// instruction trips the already-armed monitor and the wait
199/// returns immediately.
200#[inline]
201pub fn monitor_wait_u32(atomic: &AtomicU32, expected: u32, budget_cycles: u64) -> bool {
202 let Some(kind) = monitor_wait_kind() else {
203 return false;
204 };
205 monitor_wait_u32_with(kind, atomic, expected, budget_cycles)
206}
207
208/// As [`monitor_wait_u32`] with the family chosen explicitly
209/// (bench harnesses A/B the families; production callers use the
210/// probed default).
211#[cfg(target_arch = "x86_64")]
212pub fn monitor_wait_u32_with(
213 kind: MonitorWaitKind,
214 atomic: &AtomicU32,
215 expected: u32,
216 budget_cycles: u64,
217) -> bool {
218 let deadline = read_tsc().wrapping_add(budget_cycles);
219 let addr = atomic.as_ptr() as *const u8;
220 loop {
221 // Arm, THEN check, THEN wait - the order the hardware
222 // protocol requires for lost-wake freedom.
223 unsafe {
224 match kind {
225 MonitorWaitKind::Waitpkg => umonitor(addr),
226 MonitorWaitKind::Mwaitx => monitorx(addr),
227 // The aarch64 family never reaches the x86_64 body.
228 MonitorWaitKind::ArmWfe => return false,
229 }
230 }
231 if atomic.load(Ordering::Acquire) != expected {
232 return true;
233 }
234 let now = read_tsc();
235 let remaining = deadline.wrapping_sub(now);
236 // wrapping_sub > i64::MAX as u64 means `now` passed the
237 // deadline (the subtraction wrapped negative). remaining of
238 // exactly 0 is also expiry: MWAITX with EBX = 0 and the
239 // timer enabled is not a defined "wait zero cycles", so it
240 // never reaches the instruction.
241 if remaining == 0 || remaining > i64::MAX as u64 {
242 return atomic.load(Ordering::Acquire) != expected;
243 }
244 unsafe {
245 match kind {
246 MonitorWaitKind::Waitpkg => umwait(deadline),
247 MonitorWaitKind::Mwaitx => {
248 mwaitx(remaining.min(u32::MAX as u64) as u32)
249 }
250 MonitorWaitKind::ArmWfe => return false,
251 }
252 }
253 if atomic.load(Ordering::Acquire) != expected {
254 return true;
255 }
256 if read_tsc().wrapping_sub(deadline) <= i64::MAX as u64 {
257 // Deadline reached or passed.
258 return atomic.load(Ordering::Acquire) != expected;
259 }
260 // Spurious wake (interrupt tripped the monitor): re-arm.
261 }
262}
263
264/// AArch64 body: `LDAXR` arms the exclusive monitor with acquire
265/// semantics, the value re-check happens on the loaded result, and
266/// `WFE` light-sleeps until an event - which includes ANY store to
267/// the armed line (the global monitor's Exclusive->Open transition
268/// generates the event; no `SEV` needed from the storer), an
269/// interrupt, or the kernel's timer event stream tick. Spurious
270/// wakes re-arm; the budget is enforced on `CNTVCT_EL0` ticks.
271#[cfg(target_arch = "aarch64")]
272pub fn monitor_wait_u32_with(
273 kind: MonitorWaitKind,
274 atomic: &AtomicU32,
275 expected: u32,
276 budget_cycles: u64,
277) -> bool {
278 if kind != MonitorWaitKind::ArmWfe {
279 return false;
280 }
281 let deadline = read_tsc().wrapping_add(budget_cycles);
282 let addr = atomic.as_ptr();
283 loop {
284 let cur: u32;
285 unsafe {
286 // Load-exclusive-acquire: arms the monitor AND is the
287 // value check, collapsing the x86 arm-then-check pair
288 // into one instruction.
289 core::arch::asm!(
290 "ldaxr {v:w}, [{a}]",
291 v = out(reg) cur,
292 a = in(reg) addr,
293 options(nostack, preserves_flags),
294 );
295 }
296 if cur != expected {
297 unsafe {
298 // Hygiene: drop the exclusive reservation.
299 core::arch::asm!("clrex", options(nomem, nostack, preserves_flags));
300 }
301 return true;
302 }
303 let now = read_tsc();
304 if deadline.wrapping_sub(now) > i64::MAX as u64
305 || deadline == now
306 {
307 unsafe {
308 core::arch::asm!("clrex", options(nomem, nostack, preserves_flags));
309 }
310 return atomic.load(Ordering::Acquire) != expected;
311 }
312 unsafe {
313 core::arch::asm!("wfe", options(nomem, nostack, preserves_flags));
314 }
315 if atomic.load(Ordering::Acquire) != expected {
316 return true;
317 }
318 // Event-stream tick or interrupt: re-arm and loop.
319 }
320}
321
322#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
323pub fn monitor_wait_u32_with(
324 _kind: MonitorWaitKind,
325 _atomic: &AtomicU32,
326 _expected: u32,
327 _budget_cycles: u64,
328) -> bool {
329 false
330}
331
332/// As [`monitor_wait_u32`] for a 64-bit atom (ring head counters
333/// and slot sequences are `AtomicU64`). Same protocol, same
334/// guarantees: the monitor watches the LINE, the width only
335/// affects the value re-check.
336#[inline]
337pub fn monitor_wait_u64(
338 atomic: &std::sync::atomic::AtomicU64,
339 expected: u64,
340 budget_cycles: u64,
341) -> bool {
342 let Some(kind) = monitor_wait_kind() else {
343 return false;
344 };
345 monitor_wait_u64_with(kind, atomic, expected, budget_cycles)
346}
347
348#[cfg(target_arch = "x86_64")]
349pub fn monitor_wait_u64_with(
350 kind: MonitorWaitKind,
351 atomic: &std::sync::atomic::AtomicU64,
352 expected: u64,
353 budget_cycles: u64,
354) -> bool {
355 let deadline = read_tsc().wrapping_add(budget_cycles);
356 let addr = atomic.as_ptr() as *const u8;
357 loop {
358 unsafe {
359 match kind {
360 MonitorWaitKind::Waitpkg => umonitor(addr),
361 MonitorWaitKind::Mwaitx => monitorx(addr),
362 MonitorWaitKind::ArmWfe => return false,
363 }
364 }
365 if atomic.load(Ordering::Acquire) != expected {
366 return true;
367 }
368 let now = read_tsc();
369 let remaining = deadline.wrapping_sub(now);
370 if remaining == 0 || remaining > i64::MAX as u64 {
371 return atomic.load(Ordering::Acquire) != expected;
372 }
373 unsafe {
374 match kind {
375 MonitorWaitKind::Waitpkg => umwait(deadline),
376 MonitorWaitKind::Mwaitx => {
377 mwaitx(remaining.min(u32::MAX as u64) as u32)
378 }
379 MonitorWaitKind::ArmWfe => return false,
380 }
381 }
382 if atomic.load(Ordering::Acquire) != expected {
383 return true;
384 }
385 if read_tsc().wrapping_sub(deadline) <= i64::MAX as u64 {
386 return atomic.load(Ordering::Acquire) != expected;
387 }
388 }
389}
390
391#[cfg(target_arch = "aarch64")]
392pub fn monitor_wait_u64_with(
393 kind: MonitorWaitKind,
394 atomic: &std::sync::atomic::AtomicU64,
395 expected: u64,
396 budget_cycles: u64,
397) -> bool {
398 if kind != MonitorWaitKind::ArmWfe {
399 return false;
400 }
401 let deadline = read_tsc().wrapping_add(budget_cycles);
402 let addr = atomic.as_ptr();
403 loop {
404 let cur: u64;
405 unsafe {
406 core::arch::asm!(
407 "ldaxr {v}, [{a}]",
408 v = out(reg) cur,
409 a = in(reg) addr,
410 options(nostack, preserves_flags),
411 );
412 }
413 if cur != expected {
414 unsafe {
415 core::arch::asm!("clrex", options(nomem, nostack, preserves_flags));
416 }
417 return true;
418 }
419 let now = read_tsc();
420 if deadline.wrapping_sub(now) > i64::MAX as u64 || deadline == now {
421 unsafe {
422 core::arch::asm!("clrex", options(nomem, nostack, preserves_flags));
423 }
424 return atomic.load(Ordering::Acquire) != expected;
425 }
426 unsafe {
427 core::arch::asm!("wfe", options(nomem, nostack, preserves_flags));
428 }
429 if atomic.load(Ordering::Acquire) != expected {
430 return true;
431 }
432 }
433}
434
435#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
436pub fn monitor_wait_u64_with(
437 _kind: MonitorWaitKind,
438 _atomic: &std::sync::atomic::AtomicU64,
439 _expected: u64,
440 _budget_cycles: u64,
441) -> bool {
442 false
443}
444
445// ===================================================================
446// Instruction wrappers. Mnemonics, not .byte soup - LLVM's
447// integrated assembler accepts them without target-feature gates
448// (the Linux kernel compiles the identical `asm volatile("mwaitx")`
449// under clang with no -mmwaitx). Register pinning per the verified
450// conventions above; ECX/EDX are explicitly zeroed for MONITORX
451// because nonzero extension bits raise #GP and the Windows x64 ABI
452// leaves caller garbage in RCX.
453// ===================================================================
454
455#[cfg(target_arch = "x86_64")]
456#[inline]
457unsafe fn monitorx(addr: *const u8) {
458 unsafe {
459 core::arch::asm!(
460 "monitorx",
461 in("rax") addr,
462 in("ecx") 0u32,
463 in("edx") 0u32,
464 options(nostack, preserves_flags),
465 );
466 }
467}
468
469/// `EBX` = max wait in TSC-frequency clocks; `ECX` bit 1 enables
470/// the timer; `EAX` hints 0 (C1-class shallow sleep).
471///
472/// RBX is reserved by LLVM for inline asm, so the timeout travels
473/// in a scratch register and swaps through RBX around the
474/// instruction.
475#[cfg(target_arch = "x86_64")]
476#[inline]
477unsafe fn mwaitx(max_cycles: u32) {
478 unsafe {
479 core::arch::asm!(
480 "xchg {scratch}, rbx",
481 "mwaitx",
482 "xchg {scratch}, rbx",
483 scratch = inout(reg) max_cycles as u64 => _,
484 in("eax") 0u32,
485 in("ecx") 2u32,
486 options(nostack, preserves_flags),
487 );
488 }
489}
490
491#[cfg(target_arch = "x86_64")]
492#[inline]
493unsafe fn umonitor(addr: *const u8) {
494 unsafe {
495 core::arch::asm!(
496 "umonitor {addr}",
497 addr = in(reg) addr,
498 options(nostack, preserves_flags),
499 );
500 }
501}
502
503/// Control bit 0 = 1 selects C0.1 (shallow, fastest wake) - this is
504/// a latency tier. Implicit `EDX:EAX` carries the absolute TSC
505/// deadline. CF (OS-cap expiry) is irrelevant to us: the caller's
506/// loop re-checks value + deadline either way.
507#[cfg(target_arch = "x86_64")]
508#[inline]
509unsafe fn umwait(deadline_tsc: u64) {
510 let lo = deadline_tsc as u32;
511 let hi = (deadline_tsc >> 32) as u32;
512 unsafe {
513 core::arch::asm!(
514 "umwait {ctl:e}",
515 ctl = in(reg) 1u32,
516 in("eax") lo,
517 in("edx") hi,
518 options(nostack),
519 );
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use std::sync::Arc;
527 use std::time::{Duration, Instant};
528
529 #[test]
530 fn detection_runs_and_is_cached() {
531 let first = monitor_wait_kind();
532 let second = monitor_wait_kind();
533 assert_eq!(first, second, "probe must be stable across calls");
534 println!("monitor-wait kind: {first:?}, budget {} cycles",
535 monitor_wait_budget_cycles());
536 }
537
538 #[test]
539 fn returns_immediately_when_value_already_differs() {
540 let atomic = AtomicU32::new(7);
541 // Whatever the tier support, a pre-changed value reports
542 // true-or-false without sleeping the full budget.
543 let t0 = Instant::now();
544 let changed = monitor_wait_u32(&atomic, 5, 500_000_000);
545 let elapsed = t0.elapsed();
546 if monitor_wait_kind().is_some() {
547 assert!(changed, "value != expected must report changed");
548 } else {
549 assert!(!changed, "unsupported tier reports false");
550 }
551 assert!(elapsed < Duration::from_millis(200),
552 "must not consume the whole budget: {elapsed:?}");
553 }
554
555 #[test]
556 fn budget_expiry_returns_false_when_nothing_stores() {
557 if monitor_wait_kind().is_none() {
558 return;
559 }
560 let atomic = AtomicU32::new(1);
561 let t0 = Instant::now();
562 // ~30M cycles = ~10ms at 3GHz: long enough to measure, short
563 // enough for a test.
564 let changed = monitor_wait_u32(&atomic, 1, 30_000_000);
565 let elapsed = t0.elapsed();
566 assert!(!changed, "no store happened; must report expiry");
567 assert!(elapsed >= Duration::from_micros(500),
568 "expiry must actually wait, got {elapsed:?}");
569 assert!(elapsed < Duration::from_secs(2),
570 "expiry must be bounded, got {elapsed:?}");
571 }
572
573 #[test]
574 fn cross_thread_store_wakes_the_waiter() {
575 if monitor_wait_kind().is_none() {
576 return;
577 }
578 let atomic = Arc::new(AtomicU32::new(0));
579 let waker_side = Arc::clone(&atomic);
580 let h = std::thread::spawn(move || {
581 std::thread::sleep(Duration::from_millis(5));
582 waker_side.store(1, Ordering::Release);
583 });
584 let t0 = Instant::now();
585 // Budget ~3s at 3GHz: the wake must beat it by orders of
586 // magnitude.
587 let changed = monitor_wait_u32(&atomic, 0, 9_000_000_000);
588 let elapsed = t0.elapsed();
589 h.join().expect("storer thread");
590 assert!(changed, "store must wake the monitor waiter");
591 assert!(elapsed < Duration::from_millis(500),
592 "wake must arrive promptly, got {elapsed:?}");
593 }
594}