1use std::sync::OnceLock;
28
29use super::CpuFeatures;
30
31#[derive(Clone, Copy)]
36pub struct Candidate<F> {
37 name: &'static str,
38 features: CpuFeatures,
39 kernel: F,
40}
41
42impl<F: Copy> Candidate<F> {
43 pub const unsafe fn new(name: &'static str, features: CpuFeatures, kernel: F) -> Self {
51 Self {
52 name,
53 features,
54 kernel,
55 }
56 }
57
58 pub const fn name(&self) -> &'static str {
60 self.name
61 }
62
63 pub const fn features(&self) -> CpuFeatures {
65 self.features
66 }
67
68 pub fn kernel(&self) -> Option<Kernel<F>> {
70 CpuFeatures::host()
71 .contains(self.features)
72 .then_some(Kernel(self.kernel))
73 }
74}
75
76#[derive(Clone, Copy)]
78pub struct Kernel<F>(F);
79
80impl<A: ?Sized, B: ?Sized, R> Kernel<unsafe fn(&A, &B) -> R> {
81 #[inline(always)]
83 pub fn call(self, a: &A, b: &B) -> R {
84 unsafe { (self.0)(a, b) }
87 }
88}
89
90impl<A: ?Sized, B: ?Sized, C, D: ?Sized, R> Kernel<unsafe fn(&A, &B, C, &mut D) -> R> {
91 #[inline(always)]
94 pub fn call(self, a: &A, b: &B, c: C, d: &mut D) -> R {
95 unsafe { (self.0)(a, b, c, d) }
97 }
98}
99
100pub struct Dispatch<F: 'static> {
106 candidates: &'static [Candidate<F>],
107 selected: &'static Selection<F>,
111 is_static: bool,
112}
113
114pub struct Selection<F>(OnceLock<F>);
123
124impl<F> Selection<F> {
125 #[allow(clippy::new_without_default)]
127 pub const fn new() -> Self {
128 Self(OnceLock::new())
129 }
130}
131
132impl<F: Copy> Dispatch<F> {
133 pub const fn new(candidates: &'static [Candidate<F>], selected: &'static Selection<F>) -> Self {
141 let Some(fallback) = candidates.last() else {
142 panic!("a Dispatch needs at least one candidate");
143 };
144 assert!(
145 fallback.features.is_empty(),
146 "the last Dispatch candidate must not need any CPU feature"
147 );
148 Self {
149 candidates,
150 selected,
151 is_static: CpuFeatures::baseline().contains(candidates[0].features),
152 }
153 }
154
155 #[inline(always)]
157 pub fn get(&self) -> Kernel<F> {
158 if self.is_static {
159 return Kernel(self.candidates[0].kernel);
163 }
164 Kernel(*self.selected.0.get_or_init(|| self.selected().kernel))
165 }
166
167 pub fn selected(&self) -> &'static Candidate<F> {
169 &self.candidates[select(self.candidates, CpuFeatures::host())]
170 }
171
172 #[cfg(test)]
175 pub const fn is_static(&self) -> bool {
176 self.is_static
177 }
178
179 pub const fn candidates(&self) -> &'static [Candidate<F>] {
181 self.candidates
182 }
183}
184
185fn select<F>(candidates: &[Candidate<F>], host: CpuFeatures) -> usize {
187 candidates
188 .iter()
189 .position(|candidate| host.contains(candidate.features))
190 .expect("Dispatch::new requires a candidate without features")
191}
192
193#[macro_export]
208macro_rules! target_kernel {
209 (
210 #[target_features($($feature:literal),* $(,)?)]
211 #[candidate($candidate_vis:vis $candidate:ident: $kernel_type:ty)]
212 $(#[doc = $doc:literal])*
213 $(#[inline $(($inline:ident))?])?
214 $vis:vis fn $name:ident($($params:tt)*) $(-> $ret:ty)? $body:block
215 ) => {
216 $(#[doc = $doc])*
217 $(#[inline $(($inline))?])?
218 $(#[target_feature(enable = $feature)])*
219 $vis fn $name($($params)*) $(-> $ret)? $body
220
221 $candidate_vis const $candidate: $crate::utils::cpu::Candidate<$kernel_type> = {
222 const FEATURES: $crate::utils::cpu::CpuFeatures =
223 $crate::utils::cpu::CpuFeatures::from_target_features(&[$($feature),*]);
224 unsafe { $crate::utils::cpu::Candidate::new(stringify!($name), FEATURES, $name) }
227 };
228 };
229}
230
231#[macro_export]
236macro_rules! dispatch {
237 (
238 $(#[$attr:meta])*
239 $vis:vis static $name:ident: Dispatch<$kernel_type:ty> = $candidates:expr;
240 ) => {
241 $(#[$attr])*
242 $vis static $name: $crate::utils::cpu::Dispatch<$kernel_type> = {
243 static SELECTED: $crate::utils::cpu::Selection<$kernel_type> =
244 $crate::utils::cpu::Selection::new();
245 $crate::utils::cpu::Dispatch::new($candidates, &SELECTED)
246 };
247 };
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 type SumFn = unsafe fn(&[f32], &[f32]) -> f32;
255 type FillFn = unsafe fn(&[f32], &[f32], usize, &mut [f32]);
256
257 fn portable_sum(x: &[f32], y: &[f32]) -> f32 {
258 x.iter().chain(y).sum()
259 }
260
261 fn doubled_sum(x: &[f32], y: &[f32]) -> f32 {
262 2.0 * portable_sum(x, y)
263 }
264
265 crate::target_kernel! {
266 #[target_features()]
267 #[candidate(PORTABLE_SUM: SumFn)]
268 fn macro_sum(x: &[f32], y: &[f32]) -> f32 {
269 portable_sum(x, y)
270 }
271 }
272
273 crate::target_kernel! {
274 #[target_features()]
275 #[candidate(PORTABLE_FILL: FillFn)]
276 fn macro_fill(x: &[f32], batch: &[f32], dimension: usize, out: &mut [f32]) {
277 for (o, y) in out.iter_mut().zip(batch.chunks_exact(dimension)) {
278 *o = portable_sum(x, y);
279 }
280 }
281 }
282
283 #[cfg(target_arch = "x86_64")]
284 crate::target_kernel! {
285 #[target_features("avx2", "fma")]
286 #[candidate(ARCH_SUM: SumFn)]
287 fn arch_sum(x: &[f32], y: &[f32]) -> f32 {
288 portable_sum(x, y)
289 }
290 }
291
292 #[cfg(target_arch = "aarch64")]
293 crate::target_kernel! {
294 #[target_features("neon")]
295 #[candidate(ARCH_SUM: SumFn)]
296 fn arch_sum(x: &[f32], y: &[f32]) -> f32 {
297 portable_sum(x, y)
298 }
299 }
300
301 fn feature_missing_from_host() -> CpuFeatures {
302 let host = CpuFeatures::host();
303 CpuFeatures::all()
304 .find(|feature| !host.contains(*feature))
305 .expect("every host lacks the features of the other architectures")
306 }
307
308 #[test]
309 fn select_takes_the_first_candidate_the_host_supports() {
310 let missing = feature_missing_from_host();
311 let candidates: [Candidate<SumFn>; 2] = unsafe {
313 [
314 Candidate::new("unsupported", missing, doubled_sum),
315 Candidate::new("portable", CpuFeatures::empty(), portable_sum),
316 ]
317 };
318 assert_eq!(select(&candidates, CpuFeatures::host()), 1);
319 assert_eq!(select(&candidates, missing), 0);
320 }
321
322 #[test]
323 fn unsupported_candidate_yields_no_kernel() {
324 let candidate: Candidate<SumFn> =
326 unsafe { Candidate::new("unsupported", feature_missing_from_host(), doubled_sum) };
327 assert!(candidate.kernel().is_none());
328 }
329
330 crate::dispatch! {
331 static PORTABLE_ONLY: Dispatch<SumFn> = &[PORTABLE_SUM];
332 }
333
334 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
335 crate::dispatch! {
336 static ARCH_FIRST: Dispatch<SumFn> = &[ARCH_SUM, PORTABLE_SUM];
337 }
338
339 crate::dispatch! {
340 static FILL: Dispatch<FillFn> = &[PORTABLE_FILL];
341 }
342
343 #[test]
344 fn portable_only_dispatch_is_static_and_callable() {
345 assert!(PORTABLE_ONLY.is_static());
346 assert_eq!(PORTABLE_ONLY.get().call(&[1.0, 2.0], &[3.0]), 6.0);
347 assert_eq!(PORTABLE_ONLY.selected().name(), "macro_sum");
348 }
349
350 const OTHER_ARCH_FEATURE: CpuFeatures = if cfg!(target_arch = "x86_64") {
353 CpuFeatures::NEON
354 } else {
355 CpuFeatures::AVX512F
356 };
357
358 const UNSUPPORTED_SUM: Candidate<SumFn> =
360 unsafe { Candidate::new("unsupported", OTHER_ARCH_FEATURE, doubled_sum) };
361
362 crate::dispatch! {
363 static UNSUPPORTED_FIRST: Dispatch<SumFn> = &[UNSUPPORTED_SUM, PORTABLE_SUM];
364 }
365
366 #[test]
367 fn runtime_dispatch_selects_and_caches_the_supported_kernel() {
368 assert!(!UNSUPPORTED_FIRST.is_static());
369 assert_eq!(UNSUPPORTED_FIRST.selected().name(), "macro_sum");
370 assert_eq!(UNSUPPORTED_FIRST.get().call(&[1.0, 2.0], &[3.0]), 6.0);
372 assert_eq!(UNSUPPORTED_FIRST.get().call(&[1.0, 2.0], &[3.0]), 6.0);
373 }
374
375 #[test]
376 fn new_rejects_candidate_lists_without_a_portable_fallback() {
377 static SELECTED: Selection<SumFn> = Selection::new();
378 let empty = std::panic::catch_unwind(|| Dispatch::<SumFn>::new(&[], &SELECTED));
380 assert!(empty.is_err());
381 let needs_features =
382 std::panic::catch_unwind(|| Dispatch::new(&[UNSUPPORTED_SUM], &SELECTED));
383 assert!(needs_features.is_err());
384 }
385
386 #[test]
387 fn slice_output_kernel_is_callable() {
388 let mut out = [0.0; 2];
389 FILL.get().call(&[1.0], &[2.0, 3.0], 1, &mut out);
390 assert_eq!(out, [3.0, 4.0]);
391 }
392
393 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
394 #[test]
395 fn macro_mask_matches_declared_target_features() {
396 #[cfg(target_arch = "x86_64")]
397 let names: &[&str] = &["avx2", "fma"];
398 #[cfg(target_arch = "aarch64")]
399 let names: &[&str] = &["neon"];
400 assert_eq!(
401 ARCH_SUM.features(),
402 CpuFeatures::from_target_features(names)
403 );
404 assert_eq!(ARCH_SUM.name(), "arch_sum");
405 assert!(PORTABLE_SUM.features().is_empty());
406
407 let is_supported = CpuFeatures::host().contains(ARCH_SUM.features());
408 assert_eq!(ARCH_FIRST.selected().name() == "arch_sum", is_supported);
409 assert_eq!(
410 ARCH_FIRST.is_static(),
411 CpuFeatures::baseline().contains(ARCH_SUM.features())
412 );
413 for candidate in ARCH_FIRST.candidates() {
414 if let Some(kernel) = candidate.kernel() {
415 assert_eq!(kernel.call(&[1.0], &[2.0]), 3.0, "{}", candidate.name());
416 }
417 }
418 }
419}