Skip to main content

thermite/isa/
mod.rs

1#![allow(unexpected_cfgs)]
2
3//! Instruction Set Architecture detection and utilities
4
5/// Enum of supported instruction sets
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[repr(u8)]
8#[non_exhaustive]
9pub enum InstructionSet {
10    /// Scalar (no SIMD)
11    Scalar,
12
13    /// Standard library SIMD types (e.g. std::simd::Simd) when available.
14    #[cfg(feature = "std_simd")]
15    StdSimd,
16
17    /// Unknown ISA, usually the result of register emulation,
18    /// such as with Glam vectors as registers.
19    Unknown,
20
21    /// x86/x86_64 SIMD instruction set level 1 (SSE2)
22    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
23    X86V1,
24
25    /// x86/x86_64 SIMD instruction set level 2 (SSE4.2 + POPCNT)
26    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
27    X86V2,
28
29    /// x86/x86_64 SIMD instruction set level 3 (AVX2 + FMA)
30    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
31    X86V3,
32
33    /// x86/x86_64 SIMD instruction set level 4 (AVX-512F)
34    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
35    X86V4,
36
37    /// ARM Neon SIMD instruction set
38    #[cfg(target_arch = "aarch64")]
39    NEON,
40
41    /// WebAssembly SIMD instruction set (32-bit)
42    #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
43    WASM32,
44
45    /// WebAssembly SIMD instruction set (64-bit)
46    #[cfg(all(feature = "wasm", target_arch = "wasm64"))]
47    WASM64,
48
49    /// SPIR-V (Vulkan/OpenCL compute shader)
50    #[cfg(all(feature = "spirv", target_arch = "spirv"))]
51    SPIRV,
52}
53
54mod detect_once;
55pub use detect_once::DetectOnce;
56
57/// x86 / x86_64 feature bits via `cpuid`, with the OS `XCR0` state folded in.
58/// What [`InstructionSet::get`] dispatches on.
59#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
60pub mod x86;
61
62#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
63mod x86_detector;
64
65impl InstructionSet {
66    /// Detect the current instruction set at runtime. This result is cached for future calls.
67    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
68    #[inline]
69    pub fn get() -> InstructionSet {
70        static DETECTOR: DetectOnce<InstructionSet> = DetectOnce::new(InstructionSet::Scalar);
71
72        *DETECTOR.get(x86_detector::detect)
73    }
74
75    /// Detect the current instruction set at runtime. This result is cached for future calls.
76    ///
77    /// NEON (AdvSIMD) is a mandatory part of AArch64, so no actual runtime
78    /// detection is needed.
79    #[cfg(target_arch = "aarch64")]
80    pub fn get() -> InstructionSet {
81        InstructionSet::NEON
82    }
83
84    /// Detect the current instruction set at runtime. This result is cached for future calls.
85    ///
86    /// SIMD128 is decided by the engine before the module runs, so there is
87    /// nothing to detect.
88    #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
89    pub fn get() -> InstructionSet {
90        InstructionSet::WASM32
91    }
92
93    /// Detect the current instruction set at runtime. This result is cached for future calls.
94    #[cfg(all(feature = "wasm", target_arch = "wasm64"))]
95    pub fn get() -> InstructionSet {
96        InstructionSet::WASM64
97    }
98
99    /// Detect the current instruction set at runtime. This result is cached for future calls.
100    #[cfg(all(feature = "spirv", target_arch = "spirv"))]
101    pub fn get() -> InstructionSet {
102        InstructionSet::SPIRV
103    }
104
105    /// Detect the current instruction set at runtime. This result is cached for future calls.
106    ///
107    /// Fallback for targets with no SIMD backend compiled in: an unlisted
108    /// architecture, or wasm without its opt-in feature. Without this the method
109    /// would not exist on those targets, so anything calling it
110    /// (including `dispatch_dyn!`) failed to compile rather than falling back to
111    /// scalar.
112    #[cfg(not(any(
113        any(target_arch = "x86", target_arch = "x86_64"),
114        target_arch = "aarch64",
115        all(feature = "wasm", any(target_arch = "wasm32", target_arch = "wasm64")),
116        all(feature = "spirv", target_arch = "spirv"),
117    )))]
118    pub fn get() -> InstructionSet {
119        InstructionSet::Scalar
120    }
121
122    /// Order two sets by capability, returning the weaker.
123    ///
124    /// Ordering is the enum's declaration order, which ascends by capability
125    /// _within_ an architecture (`Scalar < X86V1 < .. < X86V4`). Across
126    /// architectures it is meaningless, but two architectures' variants never
127    /// coexist, since each is `cfg`-gated to its own target.
128    #[inline(always)]
129    pub const fn min(a: InstructionSet, b: InstructionSet) -> InstructionSet {
130        if (a as u8) < (b as u8) { a } else { b }
131    }
132
133    /// Order two sets by capability, returning the stronger. See [`min`](Self::min).
134    #[inline(always)]
135    pub const fn max(a: InstructionSet, b: InstructionSet) -> InstructionSet {
136        if (a as u8) > (b as u8) { a } else { b }
137    }
138
139    /// Assert two sets are the same, returning it. Compares discriminants
140    /// because `PartialEq` is not callable in a `const fn`.
141    #[inline(always)]
142    pub const fn assert_eq(a: InstructionSet, b: InstructionSet) -> InstructionSet {
143        assert!((a as u8) == (b as u8), "InstructionSet equality assertion failed");
144
145        a
146    }
147
148    /// Whether the _target_ executes independent instructions in parallel, so
149    /// that breaking a dependency chain into several accumulators pays off.
150    ///
151    /// A property of the hardware, not of the instruction set: a superscalar CPU
152    /// reorders scalar code just as happily as SIMD code, so this does not vary
153    /// by variant. SIMT targets (SPIR-V) hide latency with occupancy instead and
154    /// gain nothing from extra accumulators.
155    #[inline(always)]
156    pub const fn has_instruction_level_parallelism(self) -> bool {
157        cfg!(any(
158            target_arch = "x86",
159            target_arch = "x86_64",
160            target_arch = "arm",
161            target_arch = "aarch64"
162        ))
163    }
164}
165
166/// Per-ISA properties, one row per variant.
167///
168/// Written as a table because the alternative (a separate `match` per property)
169/// repeated the same three `#[cfg]` predicates on every arm, roughly
170/// `variants x properties` times, and scattered one ISA's characteristics across
171/// the whole file. Here each variant carries its `cfg` once and all of its
172/// properties are visible together.
173///
174/// The generated matches are **exhaustive**: adding a variant without a row is a
175/// compile error rather than silently inheriting a `_ => ..` default.
176macro_rules! isa_properties {
177    ($(
178        $(#[cfg $cfg:tt])?
179        $variant:ident {
180            registers: $registers:expr,
181            fma: $fma:expr,
182            simd: $simd:expr,
183            unaligned_cheap: $unaligned:expr,
184            unroll: $unroll:expr,
185            masked: $masked:expr,
186        }
187    )*) => {
188        impl InstructionSet {
189            /// Estimate of how many SIMD registers the ISA exposes. Used by
190            /// inlining/unrolling heuristics. See also [`NativeIsa::Registers`],
191            /// the type-level equivalent.
192            ///
193            /// [`NativeIsa::Registers`]: crate::simd::NativeIsa::Registers
194            #[inline(always)]
195            pub const fn num_registers(self) -> usize {
196                match self { $( $(#[cfg $cfg])? Self::$variant => $registers, )* }
197            }
198
199            /// Whether the ISA has a true fused multiply-add (one rounding).
200            #[inline(always)]
201            pub const fn has_fma(self) -> bool {
202                match self { $( $(#[cfg $cfg])? Self::$variant => $fma, )* }
203            }
204
205            /// Whether the ISA is actually SIMD. False for `Scalar`, `Unknown`,
206            /// and SPIR-V (which is SIMT: one lane per invocation).
207            #[inline(always)]
208            pub const fn is_simd(self) -> bool {
209                match self { $( $(#[cfg $cfg])? Self::$variant => $simd, )* }
210            }
211
212            /// Whether unaligned loads/stores cost about the same as aligned
213            /// ones, so an unaligned iterator need not be avoided.
214            #[inline(always)]
215            pub const fn unaligned_is_cheap(self) -> bool {
216                match self { $( $(#[cfg $cfg])? Self::$variant => $unaligned, )* }
217            }
218
219            /// Suggested unroll factor for bulk loops, scaled to the register
220            /// file: more registers allow more accumulators in flight.
221            #[inline(always)]
222            pub const fn unroll_factor(self) -> usize {
223                match self { $( $(#[cfg $cfg])? Self::$variant => $unroll, )* }
224            }
225
226            /// Whether the ISA has first-class masked operations (AVX-512
227            /// opmask registers), letting the `_c`/`_m`/`_z` variants lower to a
228            /// single instruction instead of a blend.
229            #[inline(always)]
230            pub const fn has_masked_operations(self) -> bool {
231                match self { $( $(#[cfg $cfg])? Self::$variant => $masked, )* }
232            }
233        }
234    };
235}
236
237isa_properties! {
238    Scalar {
239        registers: 1, fma: false, simd: false, unaligned_cheap: true, unroll: 4, masked: false,
240    }
241
242    #[cfg(feature = "std_simd")]
243    StdSimd {
244        registers: 1, fma: false, simd: true, unaligned_cheap: false, unroll: 1, masked: false,
245    }
246
247    Unknown {
248        registers: 1, fma: false, simd: false, unaligned_cheap: false, unroll: 1, masked: false,
249    }
250
251    // 8 XMM registers on legacy SSE, 16 from SSE4.2/AVX2, 32 with AVX-512.
252    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
253    X86V1 {
254        registers: 8, fma: false, simd: true, unaligned_cheap: false, unroll: 4, masked: false,
255    }
256
257    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
258    X86V2 {
259        registers: 16, fma: false, simd: true, unaligned_cheap: false, unroll: 4, masked: false,
260    }
261
262    // Unaligned access stops being penalised around AVX2.
263    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
264    X86V3 {
265        registers: 16, fma: true, simd: true, unaligned_cheap: true, unroll: 4, masked: false,
266    }
267
268    // Twice the registers, so twice the unroll, and the only ISA here with real
269    // masked operations.
270    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
271    X86V4 {
272        registers: 32, fma: true, simd: true, unaligned_cheap: true, unroll: 8, masked: true,
273    }
274
275    #[cfg(target_arch = "aarch64")]
276    NEON {
277        registers: 32, fma: true, simd: true, unaligned_cheap: true, unroll: 4, masked: false,
278    }
279
280    // TODO: verify the wasm register count and unaligned cost. The engine's JIT
281    // decides both, so these are conservative guesses.
282    #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
283    WASM32 {
284        registers: 16, fma: false, simd: true, unaligned_cheap: false, unroll: 2, masked: false,
285    }
286
287    #[cfg(all(feature = "wasm", target_arch = "wasm64"))]
288    WASM64 {
289        registers: 16, fma: false, simd: true, unaligned_cheap: false, unroll: 2, masked: false,
290    }
291
292    // SIMT: one lane per invocation, FMA via `OpFma`, no alignment penalty, and
293    // extra unrolling only raises register pressure and hurts occupancy.
294    #[cfg(all(feature = "spirv", target_arch = "spirv"))]
295    SPIRV {
296        registers: 1, fma: true, simd: false, unaligned_cheap: true, unroll: 1, masked: false,
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::InstructionSet;
303
304    /// Every variant compiled on this target.
305    fn all() -> &'static [InstructionSet] {
306        &[
307            InstructionSet::Scalar,
308            InstructionSet::Unknown,
309            #[cfg(feature = "std_simd")]
310            InstructionSet::StdSimd,
311            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
312            InstructionSet::X86V1,
313            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
314            InstructionSet::X86V2,
315            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
316            InstructionSet::X86V3,
317            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
318            InstructionSet::X86V4,
319            #[cfg(target_arch = "aarch64")]
320            InstructionSet::NEON,
321            #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
322            InstructionSet::WASM32,
323            #[cfg(all(feature = "wasm", target_arch = "wasm64"))]
324            InstructionSet::WASM64,
325        ]
326    }
327
328    /// Properties must be sane for every variant, whatever the target. These are
329    /// consumed by codegen heuristics, so a zero would be actively harmful.
330    #[test]
331    fn properties_are_sane() {
332        for &isa in all() {
333            assert!(isa.num_registers() >= 1, "{isa:?}: zero registers");
334            assert!(isa.unroll_factor() >= 1, "{isa:?}: zero unroll factor");
335            // Masked operations are a SIMD feature, so nothing scalar can have them.
336            assert!(
337                !isa.has_masked_operations() || isa.is_simd(),
338                "{isa:?}: masked but not SIMD"
339            );
340        }
341
342        assert!(!InstructionSet::Scalar.is_simd());
343        assert!(!InstructionSet::Unknown.is_simd());
344        assert!(!InstructionSet::Scalar.has_fma());
345    }
346
347    /// Pins the x86 rows of the table. These feed real codegen decisions
348    /// (`unroll_factor` in the transform loops, `has_fma` in the math kernels),
349    /// so changing one should be deliberate.
350    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
351    #[test]
352    fn x86_rows() {
353        use InstructionSet::{X86V1, X86V2, X86V3, X86V4};
354
355        assert_eq!((X86V1.num_registers(), X86V2.num_registers()), (8, 16));
356        assert_eq!((X86V3.num_registers(), X86V4.num_registers()), (16, 32));
357
358        // FMA arrives with AVX2 (v3).
359        assert!(!X86V1.has_fma() && !X86V2.has_fma() && X86V3.has_fma() && X86V4.has_fma());
360
361        // Unaligned access stops being penalised at v3.
362        assert!(!X86V1.unaligned_is_cheap() && !X86V2.unaligned_is_cheap());
363        assert!(X86V3.unaligned_is_cheap() && X86V4.unaligned_is_cheap());
364
365        // Only AVX-512 has real masked operations.
366        assert!(!X86V1.has_masked_operations() && !X86V2.has_masked_operations());
367        assert!(!X86V3.has_masked_operations() && X86V4.has_masked_operations());
368
369        // Twice the registers, twice the accumulators.
370        assert_eq!(X86V3.unroll_factor(), 4);
371        assert_eq!(X86V4.unroll_factor(), 8);
372
373        // Declaration order ascends by capability.
374        assert!(InstructionSet::Scalar < X86V1 && X86V1 < X86V2 && X86V2 < X86V3 && X86V3 < X86V4);
375        assert_eq!(InstructionSet::min(X86V2, X86V4), X86V2);
376        assert_eq!(InstructionSet::max(X86V2, X86V4), X86V4);
377        assert_eq!(InstructionSet::assert_eq(X86V3, X86V3), X86V3);
378    }
379
380    #[test]
381    #[should_panic(expected = "InstructionSet equality assertion failed")]
382    fn assert_eq_rejects_mismatch() {
383        InstructionSet::assert_eq(InstructionSet::Scalar, InstructionSet::Unknown);
384    }
385
386    /// ILP is a property of the host, not of the ISA variant. Previously this
387    /// was a `match` whose first arm was a `_` wildcard, making every later arm
388    /// dead code.
389    #[test]
390    fn ilp_does_not_vary_by_variant() {
391        let expected = InstructionSet::Scalar.has_instruction_level_parallelism();
392        for &isa in all() {
393            assert_eq!(isa.has_instruction_level_parallelism(), expected, "{isa:?}");
394        }
395        assert_eq!(
396            expected,
397            cfg!(any(
398                target_arch = "x86",
399                target_arch = "x86_64",
400                target_arch = "arm",
401                target_arch = "aarch64"
402            ))
403        );
404    }
405
406    /// `get()` must exist and return something this build can actually run.
407    #[test]
408    fn get_is_available() {
409        let isa = InstructionSet::get();
410        assert!(
411            all().contains(&isa) || isa == InstructionSet::Scalar,
412            "{isa:?} is not a compiled variant"
413        );
414    }
415}
Last built: 2026-09-08 21:35:55 UTC