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`, or `native` for what the machine offers — and a
12//! level the CPU cannot run is clamped down to the one it can, so a pinned
13//! 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. On aarch64 the ladder has two
32/// rungs — `Baseline` and `V3`, which stands for NEON. NEON is also in the
33/// aarch64 baseline, so those two compile to the same code and exist to
34/// keep the dispatch the same shape on both architectures.
35///
36/// There is no `V4`: AVX-512 has no stable `target_feature` name on the
37/// toolchain libjay is built with, so no clone can ask for it. Adding the
38/// rung is a target string and a detection arm once it does.
39#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
40pub enum Level {
41    Baseline = 0,
42    V2 = 1,
43    V3 = 2,
44}
45
46impl Level {
47    /// The name `LIBJAY_CPU_LEVEL` takes for this level.
48    pub fn name(self) -> &'static str {
49        match self {
50            Level::Baseline => "baseline",
51            Level::V2 => "v2",
52            Level::V3 => "v3",
53        }
54    }
55
56    fn from_u8(v: u8) -> Level {
57        match v {
58            1 => Level::V2,
59            2 => Level::V3,
60            _ => Level::Baseline,
61        }
62    }
63
64    /// The level a `LIBJAY_CPU_LEVEL` value names, or None for one that
65    /// names none. `native` and `auto` name whatever the machine offers.
66    fn from_name(s: &str) -> Option<Level> {
67        match s.trim().to_ascii_lowercase().as_str() {
68            "baseline" | "v1" | "none" => Some(Level::Baseline),
69            "v2" => Some(Level::V2),
70            "v3" => Some(Level::V3),
71            "native" | "auto" | "max" => Some(detected()),
72            _ => None,
73        }
74    }
75}
76
77/// The highest level this machine can run.
78#[cfg(target_arch = "x86_64")]
79pub fn detected() -> Level {
80    if is_x86_feature_detected!("avx2")
81        && is_x86_feature_detected!("fma")
82        && is_x86_feature_detected!("bmi1")
83        && is_x86_feature_detected!("bmi2")
84        && is_x86_feature_detected!("f16c")
85        && is_x86_feature_detected!("lzcnt")
86    {
87        Level::V3
88    } else if is_x86_feature_detected!("sse4.2") && is_x86_feature_detected!("popcnt") {
89        Level::V2
90    } else {
91        Level::Baseline
92    }
93}
94
95/// The highest level this machine can run. NEON is in every aarch64
96/// baseline, so the top rung is always reachable.
97#[cfg(target_arch = "aarch64")]
98pub fn detected() -> Level {
99    if std::arch::is_aarch64_feature_detected!("neon") { Level::V3 } else { Level::Baseline }
100}
101
102/// The highest level this machine can run. An architecture with no levels
103/// of its own runs the one compilation there is.
104#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
105pub fn detected() -> Level {
106    Level::Baseline
107}
108
109/// Every level this machine can run, lowest first. A test that wants to
110/// compare the levels against each other iterates this; asking for one
111/// that is not in it would only get the highest one that is.
112pub fn available() -> Vec<Level> {
113    let top = detected();
114    [Level::Baseline, Level::V2, Level::V3].into_iter().filter(|&l| l <= top).collect()
115}
116
117/// Not yet resolved: no level has this value.
118const UNSET: u8 = u8::MAX;
119
120static LEVEL: AtomicU8 = AtomicU8::new(UNSET);
121
122/// `LIBJAY_CPU_LEVEL`, clamped to what the machine can run; the machine's
123/// own level when the variable is unset or names nothing.
124fn from_env() -> Level {
125    let asked = std::env::var("LIBJAY_CPU_LEVEL").ok().and_then(|v| Level::from_name(&v));
126    match asked {
127        Some(l) => l.min(detected()),
128        None => detected(),
129    }
130}
131
132/// The level the hot loops dispatch to. Resolved once, then an atomic load.
133#[inline]
134pub fn level() -> Level {
135    let v = LEVEL.load(Ordering::Relaxed);
136    if v != UNSET {
137        return Level::from_u8(v);
138    }
139    let l = from_env();
140    LEVEL.store(l as u8, Ordering::Relaxed);
141    l
142}
143
144/// Dispatch to `l` from here on, clamped to what the machine can run.
145/// Returns the level that took effect.
146///
147/// This is the same knob `LIBJAY_CPU_LEVEL` turns, for a caller that wants
148/// to turn it more than once — a test comparing the levels against each
149/// other, or a benchmark. Values already computed are not affected.
150pub fn set_level(l: Level) -> Level {
151    let l = l.min(detected());
152    LEVEL.store(l as u8, Ordering::Relaxed);
153    l
154}
155
156/// Compile a hot loop once per CPU feature level and dispatch on [`level`].
157///
158/// The loop itself is written once, as an ordinary function; this generates
159/// the clones and the dispatch:
160///
161/// ```ignore
162/// #[inline(always)]
163/// fn add_body(a: &[f64], b: &[f64], dst: &mut [f64]) { … }
164///
165/// multiversioned! {
166///     /// The doc comment the dispatching function carries.
167///     fn add(a: &[f64], b: &[f64], dst: &mut [f64]) -> () = add_body;
168/// }
169/// ```
170///
171/// Generic loops name their parameters, with bounds inline, in brackets —
172/// `fn fold[T: Copy](…)` — since angle brackets are not a group a macro can
173/// match. The body must be `#[inline(always)]`: it is what carries the
174/// arithmetic into each clone, where the clone's features apply to it.
175macro_rules! multiversioned {
176    (
177        $(#[$attr:meta])*
178        fn $name:ident $([$($gen:tt)*])? ($($arg:ident: $ty:ty),* $(,)?) -> $ret:ty = $body:ident;
179    ) => {
180        mod $name {
181            // The clones only forward, so they inherit the shape of the
182            // loop they wrap, argument count and all.
183            #![allow(clippy::too_many_arguments)]
184
185            #[allow(unused_imports)]
186            use super::*;
187
188            pub(super) fn baseline $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
189                $body($($arg),*)
190            }
191
192            #[::multiversion::multiversion(targets("x86_64+sse3+ssse3+sse4.1+sse4.2+popcnt"))]
193            pub(super) fn v2 $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
194                $body($($arg),*)
195            }
196
197            #[::multiversion::multiversion(targets(
198                "x86_64+avx+avx2+fma+bmi1+bmi2+lzcnt+f16c",
199                "aarch64+neon",
200            ))]
201            pub(super) fn v3 $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
202                $body($($arg),*)
203            }
204        }
205
206        $(#[$attr])*
207        #[inline]
208        fn $name $(<$($gen)*>)? ($($arg: $ty),*) -> $ret {
209            match $crate::simd::level() {
210                $crate::simd::Level::Baseline => $name::baseline($($arg),*),
211                $crate::simd::Level::V2 => $name::v2($($arg),*),
212                $crate::simd::Level::V3 => $name::v3($($arg),*),
213            }
214        }
215    };
216}
217
218pub(crate) use multiversioned;
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn the_machines_own_level_is_available() {
226        let all = available();
227        assert_eq!(all.last().copied(), Some(detected()));
228        assert!(all.contains(&Level::Baseline));
229    }
230
231    #[test]
232    fn a_level_the_machine_lacks_clamps_to_one_it_has() {
233        assert!(set_level(Level::V3) <= detected());
234        assert_eq!(set_level(Level::Baseline), Level::Baseline);
235        assert_eq!(level(), Level::Baseline);
236        set_level(detected());
237    }
238
239    #[test]
240    fn every_level_has_a_name_and_reads_back() {
241        for l in [Level::Baseline, Level::V2, Level::V3] {
242            assert_eq!(Level::from_name(l.name()), Some(l));
243        }
244        assert_eq!(Level::from_name("nonsense"), None);
245        assert_eq!(Level::from_name("native"), Some(detected()));
246    }
247}