subetha_core/cpuid.rs
1//! Tiny CPUID feature-detection surface that adaptive primitives
2//! across the SubEtha workspace consult to gate
3//! hardware-acceleration paths.
4//!
5//! The substrate exposes:
6//!
7//! - [`has_waitpkg`] - is the WAITPKG ISA extension
8//! (`UMONITOR` / `UMWAIT` / `TPAUSE`) available?
9//! - [`has_movdir64b`] - is the MOVDIR64B ISA extension (atomic
10//! non-temporal 64-byte cache-line store) available?
11//!
12//! WAITPKG was introduced on Intel Tremont (2019) and Tiger Lake
13//! (2020) microarchitectures, and on AMD Zen 5 (2024). Hosts older
14//! than that need a `PAUSE`-spin fallback; primitives that wait on
15//! a cache line (e.g. `SharedDequeUrd` in `subetha-cxc`) pick the
16//! wait strategy at runtime by calling this function once and
17//! caching the result.
18//!
19//! MOVDIR64B was introduced on Intel Tremont (2019) and Tiger Lake
20//! (2020), and on AMD Zen 5 (2024). It is the atomic 64-byte
21//! non-temporal store; `SharedDequeUrd` uses it to publish a whole
22//! mailbox line in one transaction, eliminating the cross-CCX
23//! coherence-upgrade traffic that the byte-by-byte fallback path
24//! pays on hosts without it.
25//!
26//! Both flags are probed via CPUID leaf 7 sub-leaf 0
27//! (`Structured Extended Feature Flags`): ECX bit 5 = WAITPKG,
28//! ECX bit 28 = MOVDIR64B.
29
30#[cfg(target_arch = "x86_64")]
31use std::sync::OnceLock;
32
33/// Returns `true` when the CPU advertises the WAITPKG ISA extension
34/// (`UMONITOR` / `UMWAIT` / `TPAUSE`).
35///
36/// On x86_64 the result is cached after the first probe. On
37/// non-x86_64 targets this always returns `false`.
38#[cfg(target_arch = "x86_64")]
39pub fn has_waitpkg() -> bool {
40 *waitpkg_available()
41}
42
43#[cfg(not(target_arch = "x86_64"))]
44pub fn has_waitpkg() -> bool {
45 false
46}
47
48#[cfg(target_arch = "x86_64")]
49fn waitpkg_available() -> &'static bool {
50 static CACHE: OnceLock<bool> = OnceLock::new();
51 CACHE.get_or_init(|| {
52 use std::arch::x86_64::{__cpuid, __cpuid_count};
53 // Validate CPUID leaf 7 is implemented by reading the
54 // max-leaf id from leaf 0. Pre-Nehalem silicon may not
55 // implement leaf 7 at all. `__cpuid` and `__cpuid_count`
56 // are safe on the current x86_64 stable surface; the
57 // `#[cfg(target_arch = "x86_64")]` gate above guarantees
58 // the only required invariant.
59 let max_leaf = __cpuid(0).eax;
60 if max_leaf < 7 {
61 return false;
62 }
63 // CPUID leaf 7, sub-leaf 0, ECX bit 5 = WAITPKG.
64 let r = __cpuid_count(7, 0);
65 (r.ecx >> 5) & 1 == 1
66 })
67}
68
69/// Returns `true` when the CPU advertises the MOVDIR64B ISA
70/// extension (atomic non-temporal 64-byte cache-line store).
71///
72/// MOVDIR64B encodes as `66 0F 38 F8 /r` and writes a 64-byte-
73/// aligned source cache line to a 64-byte-aligned destination cache
74/// line in one atomic transaction that bypasses the writing core's
75/// L1d (Write-Combining store). It eliminates the RFO coherence
76/// upgrade that a byte-by-byte fallback path pays when the
77/// destination line is in a remote core's L1d in M-state.
78///
79/// Available on Intel Tremont (2019), Tiger Lake (2020) and later
80/// Intel cores, and on AMD Zen 5 (2024) and later AMD cores.
81///
82/// On x86_64 the result is cached after the first probe. On
83/// non-x86_64 targets this always returns `false`.
84#[cfg(target_arch = "x86_64")]
85pub fn has_movdir64b() -> bool {
86 *movdir64b_available()
87}
88
89#[cfg(not(target_arch = "x86_64"))]
90pub fn has_movdir64b() -> bool {
91 false
92}
93
94#[cfg(target_arch = "x86_64")]
95fn movdir64b_available() -> &'static bool {
96 static CACHE: OnceLock<bool> = OnceLock::new();
97 CACHE.get_or_init(|| {
98 use std::arch::x86_64::{__cpuid, __cpuid_count};
99 let max_leaf = __cpuid(0).eax;
100 if max_leaf < 7 {
101 return false;
102 }
103 // CPUID leaf 7, sub-leaf 0, ECX bit 28 = MOVDIR64B.
104 let r = __cpuid_count(7, 0);
105 (r.ecx >> 28) & 1 == 1
106 })
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn has_waitpkg_does_not_panic_and_caches() {
115 // Exercise the cached probe path twice; the actual answer
116 // depends on the host CPU and is not asserted. The second
117 // call hits the `OnceLock` cache.
118 let first = has_waitpkg();
119 let second = has_waitpkg();
120 assert_eq!(first, second, "has_waitpkg must be idempotent");
121 }
122
123 #[test]
124 fn has_movdir64b_does_not_panic_and_caches() {
125 let first = has_movdir64b();
126 let second = has_movdir64b();
127 assert_eq!(first, second, "has_movdir64b must be idempotent");
128 }
129}