Skip to main content

jay/
simd.rs

1//! Runtime dispatch over CPU feature levels.
2//!
3//! One artifact per platform, several compilations of every hot loop. Each
4//! loop covered here is compiled once for each level below, by the
5//! `multiversion` crate, which attaches the level's `target_feature` set to
6//! a clone of the same generic Rust source; the compiler's autovectoriser
7//! is what turns each clone into vector code. Nothing in libjay writes SIMD
8//! intrinsics, and nothing may start: vectorisation is the backend's job.
9//!
10//! Which clone runs is decided once per process. `LIBJAY_CPU_LEVEL` pins it
11//! — `baseline`, `v2`, `v3`, `v4`, or `native` for what the machine offers
12//! — and a level the CPU cannot run is clamped down to the one it can, so a
13//! pinned level is always a level that actually executes.
14//!
15//! A covered loop may still decline the vector clone: where the loop that
16//! would widen is only a few elements long, entering a vector body costs
17//! more than the width gives back, so the loop takes the baseline
18//! compilation whatever the machine can run. `verb::VECTOR_COLUMNS` is that
19//! rule and carries the measurement behind it.
20//!
21//! An elementwise pass computes the same values whatever clone runs it:
22//! vectorising `dst[i] = a[i] + b[i]` reorders nothing. A reduction is
23//! another matter — the levels agree there only to the tolerance the float
24//! contract already allows for regrouping an associative fold (§5.9).
25
26use std::sync::atomic::{AtomicU8, Ordering};
27
28/// A set of CPU features the hot loops are compiled for.
29///
30/// The names are the x86-64 microarchitecture levels: `V2` is SSE4.2 and
31/// its neighbours, `V3` adds AVX2 and FMA, `V4` adds the AVX-512 subsets
32/// the level names (`f`, `bw`, `cd`, `dq`, `vl`). On aarch64 the ladder has
33/// two rungs — `Baseline` and `V3`, which stands for NEON. NEON is also in
34/// the aarch64 baseline, so those two compile to the same code and exist to
35/// keep the dispatch the same shape on both architectures; `V4` is x86-64
36/// only and is never detected elsewhere.
37#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
38pub enum Level {
39    Baseline = 0,
40    V2 = 1,
41    V3 = 2,
42    V4 = 3,
43}
44
45impl Level {
46    /// The name `LIBJAY_CPU_LEVEL` takes for this level.
47    pub fn name(self) -> &'static str {
48        match self {
49            Level::Baseline => "baseline",
50            Level::V2 => "v2",
51            Level::V3 => "v3",
52            Level::V4 => "v4",
53        }
54    }
55
56    fn from_u8(v: u8) -> Level {
57        match v {
58            1 => Level::V2,
59            2 => Level::V3,
60            3 => Level::V4,
61            _ => Level::Baseline,
62        }
63    }
64
65    /// The level a `LIBJAY_CPU_LEVEL` value names, or None for one that
66    /// names none. `native` and `auto` name whatever the machine offers.
67    fn from_name(s: &str) -> Option<Level> {
68        match s.trim().to_ascii_lowercase().as_str() {
69            "baseline" | "v1" | "none" => Some(Level::Baseline),
70            "v2" => Some(Level::V2),
71            "v3" => Some(Level::V3),
72            "v4" => Some(Level::V4),
73            "native" | "auto" | "max" => Some(detected()),
74            _ => None,
75        }
76    }
77}
78
79/// The highest level this machine can run.
80///
81/// The levels nest, so each test includes the one below it: a machine that
82/// reports V4 is a machine every clone below V4 also runs, which is what
83/// lets [`available`] hand a test the whole ladder up to this point.
84#[cfg(target_arch = "x86_64")]
85pub fn detected() -> Level {
86    let v2 = is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("popcnt");
87    let v3 = v2
88        && is_x86_feature_detected!("avx2")
89        && is_x86_feature_detected!("fma")
90        && is_x86_feature_detected!("bmi1")
91        && is_x86_feature_detected!("bmi2")
92        && is_x86_feature_detected!("f16c")
93        && is_x86_feature_detected!("lzcnt");
94    let v4 = v3
95        && is_x86_feature_detected!("avx512f")
96        && is_x86_feature_detected!("avx512bw")
97        && is_x86_feature_detected!("avx512cd")
98        && is_x86_feature_detected!("avx512dq")
99        && is_x86_feature_detected!("avx512vl");
100    if v4 {
101        Level::V4
102    } else if v3 {
103        Level::V3
104    } else if v2 {
105        Level::V2
106    } else {
107        Level::Baseline
108    }
109}
110
111/// The highest level this machine can run. NEON is in every aarch64
112/// baseline, so the top rung is always reachable.
113#[cfg(target_arch = "aarch64")]
114pub fn detected() -> Level {
115    if std::arch::is_aarch64_feature_detected!("neon") { Level::V3 } else { Level::Baseline }
116}
117
118/// The highest level this machine can run. An architecture with no levels
119/// of its own runs the one compilation there is.
120#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
121pub fn detected() -> Level {
122    Level::Baseline
123}
124
125/// Every level this machine can run, lowest first. A test that wants to
126/// compare the levels against each other iterates this; asking for one
127/// that is not in it would only get the highest one that is.
128pub fn available() -> Vec<Level> {
129    let top = detected();
130    [Level::Baseline, Level::V2, Level::V3, Level::V4].into_iter().filter(|&l| l <= top).collect()
131}
132
133/// Not yet resolved: no level has this value.
134const UNSET: u8 = u8::MAX;
135
136static LEVEL: AtomicU8 = AtomicU8::new(UNSET);
137
138/// `LIBJAY_CPU_LEVEL`, clamped to what the machine can run; the machine's
139/// own level when the variable is unset or names nothing.
140fn from_env() -> Level {
141    let asked = std::env::var("LIBJAY_CPU_LEVEL").ok().and_then(|v| Level::from_name(&v));
142    match asked {
143        Some(l) => l.min(detected()),
144        None => detected(),
145    }
146}
147
148/// The level the hot loops dispatch to. Resolved once, then an atomic load.
149#[inline]
150pub fn level() -> Level {
151    let v = LEVEL.load(Ordering::Relaxed);
152    if v != UNSET {
153        return Level::from_u8(v);
154    }
155    let l = from_env();
156    LEVEL.store(l as u8, Ordering::Relaxed);
157    l
158}
159
160/// Dispatch to `l` from here on, clamped to what the machine can run.
161/// Returns the level that took effect.
162///
163/// This is the same knob `LIBJAY_CPU_LEVEL` turns, for a caller that wants
164/// to turn it more than once — a test comparing the levels against each
165/// other, or a benchmark. Values already computed are not affected.
166pub fn set_level(l: Level) -> Level {
167    let l = l.min(detected());
168    LEVEL.store(l as u8, Ordering::Relaxed);
169    l
170}
171
172/// Compile a hot loop once per CPU feature level and dispatch on [`level`].
173///
174/// The loop itself is written once, as an ordinary function; this generates
175/// the clones and the dispatch:
176///
177/// ```ignore
178/// #[inline(always)]
179/// fn add_body(a: &[f64], b: &[f64], dst: &mut [f64]) { … }
180///
181/// multiversioned! {
182///     /// The doc comment the dispatching function carries.
183///     fn add(a: &[f64], b: &[f64], dst: &mut [f64]) -> () = add_body;
184/// }
185/// ```
186///
187/// Generic loops name their parameters, with bounds inline, in brackets —
188/// `fn fold[T: Copy](…)` — since angle brackets are not a group a macro can
189/// match. The body must be `#[inline(always)]`: it is what carries the
190/// arithmetic into each clone, where the clone's features apply to it.
191macro_rules! multiversioned {
192    (
193        $(#[$attr:meta])*
194        fn $name:ident $([$($gen:tt)*])? ($($arg:ident: $ty:ty),* $(,)?) -> $ret:ty = $body:ident;
195    ) => {
196        mod $name {
197            // The clones only forward, so they inherit the shape of the
198            // loop they wrap, argument count and all.
199            #![allow(clippy::too_many_arguments)]
200
201            #[allow(unused_imports)]
202            use super::*;
203
204            pub(super) fn baseline $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
205                $body($($arg),*)
206            }
207
208            #[::multiversion::multiversion(targets("x86_64+sse3+ssse3+sse4.1+sse4.2+popcnt"))]
209            pub(super) fn v2 $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
210                $body($($arg),*)
211            }
212
213            #[::multiversion::multiversion(targets(
214                "x86_64+avx+avx2+fma+bmi1+bmi2+lzcnt+f16c",
215                "aarch64+neon",
216            ))]
217            pub(super) fn v3 $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
218                $body($($arg),*)
219            }
220
221            // x86-64 only: no other architecture has a rung above v3, and
222            // on those this clone is the unversioned body, never dispatched
223            // to because `detected` cannot return V4 there.
224            #[::multiversion::multiversion(targets(
225                "x86_64+avx512f+avx512bw+avx512cd+avx512dq+avx512vl",
226            ))]
227            pub(super) fn v4 $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
228                $body($($arg),*)
229            }
230        }
231
232        $(#[$attr])*
233        #[inline]
234        fn $name $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
235            match $crate::simd::level() {
236                $crate::simd::Level::Baseline => $name::baseline($($arg),*),
237                $crate::simd::Level::V2 => $name::v2($($arg),*),
238                $crate::simd::Level::V3 => $name::v3($($arg),*),
239                $crate::simd::Level::V4 => $name::v4($($arg),*),
240            }
241        }
242    };
243}
244
245pub(crate) use multiversioned;
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn the_machines_own_level_is_available() {
253        let all = available();
254        assert_eq!(all.last().copied(), Some(detected()));
255        assert!(all.contains(&Level::Baseline));
256    }
257
258    #[test]
259    fn a_level_the_machine_lacks_clamps_to_one_it_has() {
260        assert!(set_level(Level::V4) <= detected());
261        assert_eq!(set_level(Level::Baseline), Level::Baseline);
262        assert_eq!(level(), Level::Baseline);
263        set_level(detected());
264    }
265
266    #[test]
267    fn every_level_has_a_name_and_reads_back() {
268        for l in [Level::Baseline, Level::V2, Level::V3, Level::V4] {
269            assert_eq!(Level::from_name(l.name()), Some(l));
270        }
271        assert_eq!(Level::from_name("nonsense"), None);
272        assert_eq!(Level::from_name("native"), Some(detected()));
273    }
274}