Skip to main content

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}