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}