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}