vortex_buffer/dispatch.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! One-time CPU-feature-based function dispatch.
5//!
6//! [`CpuKernel`] holds a function pointer chosen by a *selector* exactly once, on the
7//! first call. Every later call is a relaxed atomic load, a never-taken predicted
8//! branch, and an indirect call — measured indistinguishable from a direct call (see
9//! `benches/cpu_dispatch.rs`).
10//!
11//! The selector is ordinary code that models both dispatch dimensions:
12//!
13//! * **Compile time** (x86_64 vs aarch64): one `#[cfg(target_arch = ...)]` block per
14//! architecture, each *returning early* with its chosen kernel.
15//! * **Runtime** (AVX-512 vs BMI2 vs ...): an if-chain of feature probes inside that
16//! block.
17//! * The **portable default** is the plain tail expression. Because the architecture
18//! arms return early, no `#[cfg(not(any(...)))]` negation is ever needed. An arm
19//! that returns unconditionally (e.g. NEON, architecturally guaranteed on aarch64)
20//! makes the tail unreachable on that architecture — wrap just the tail in an
21//! `#[allow(unreachable_code)]` block.
22//!
23//! When kernels are `unsafe` `#[target_feature]` functions, make `F` an
24//! `unsafe fn(...)` pointer type: the bare kernel names then coerce directly (no
25//! closure wrappers), and the one dispatched call is wrapped in `unsafe` with a
26//! SAFETY comment stating that the selector probed the required features.
27//!
28//! Races are benign: the slot only ever holds valid function pointers of the same
29//! type, and every candidate must compute the same result.
30//!
31//! # When NOT to use it
32//!
33//! Do not put the dispatched call inside a per-element hot loop: the indirect call
34//! blocks inlining. Hoist the decision to the outermost per-buffer entry point and
35//! monomorphize the loop instead, like the `Bmi2`/`Portable` type-parameter pattern in
36//! `vortex-mask::intersect_by_rank`.
37//!
38//! For the same reason, gate on input size *before* [`get`](CpuKernel::get) when tiny
39//! inputs are common: call the portable kernel directly below the size where SIMD pays
40//! off, so those calls stay inlinable and skip the dispatch entirely (see
41//! `count_ones_aligned`).
42
43use core::mem::transmute_copy;
44use core::ptr;
45use core::sync::atomic::AtomicPtr;
46use core::sync::atomic::Ordering;
47
48/// A function pointer selected by CPU-feature detection once, on first use.
49///
50/// `F` must be a plain function-pointer type (`fn(...) -> ...`). The selector passed
51/// to [`new`](Self::new) runs at most once per process (once per racing thread in the
52/// worst case), on the first [`get`](Self::get), and its result is cached.
53/// Non-capturing closures coerce to function pointers, so the selector can be written
54/// inline in the `static`, like `LazyLock`.
55///
56/// # Example
57///
58/// ```
59/// use vortex_buffer::CpuKernel;
60///
61/// /// Sums a slice, using the best kernel for the current CPU.
62/// fn sum(values: &[u64]) -> u64 {
63/// static KERNEL: CpuKernel<fn(&[u64]) -> u64> = CpuKernel::new(|| {
64/// // Compile-time arm per architecture; runtime probes inside it.
65/// #[cfg(target_arch = "x86_64")]
66/// {
67/// if std::arch::is_x86_feature_detected!("avx2") {
68/// // return |values| unsafe { sum_avx2(values) };
69/// }
70/// }
71/// // Portable default: plain tail, no cfg(not(...)) needed.
72/// |values| values.iter().sum()
73/// });
74/// KERNEL.get()(values)
75/// }
76///
77/// assert_eq!(sum(&[1, 2, 3]), 6);
78/// ```
79pub struct CpuKernel<F> {
80 selected: AtomicPtr<()>,
81 select: fn() -> F,
82}
83
84impl<F: Copy> CpuKernel<F> {
85 /// Create a kernel slot whose kernel is chosen by `select` on the first
86 /// [`get`](Self::get).
87 pub const fn new(select: fn() -> F) -> Self {
88 assert!(
89 size_of::<F>() == size_of::<*mut ()>(),
90 "CpuKernel requires a function-pointer type"
91 );
92 Self {
93 selected: AtomicPtr::new(ptr::null_mut()),
94 select,
95 }
96 }
97
98 /// Return the selected kernel, running the selector on the first call.
99 ///
100 /// Steady state is a relaxed load plus a never-taken predicted branch.
101 #[inline]
102 pub fn get(&self) -> F {
103 let fn_ptr = self.selected.load(Ordering::Relaxed);
104 if fn_ptr.is_null() {
105 return self.select_slow();
106 }
107 // SAFETY: non-null values in `selected` are always the bits of an `F` stored
108 // by `select_slow`, and `F` is pointer-sized (asserted in `new`). Function
109 // pointers are never null, so the null sentinel stays unambiguous.
110 unsafe { transmute_copy::<*mut (), F>(&fn_ptr) }
111 }
112
113 #[cold]
114 fn select_slow(&self) -> F {
115 let kernel = (self.select)();
116 // SAFETY: `F` is pointer-sized (asserted in `new`); a bitwise copy into a raw
117 // pointer preserves the function pointer for the transmute back in `get`.
118 let fn_ptr = unsafe { transmute_copy::<F, *mut ()>(&kernel) };
119 self.selected.store(fn_ptr, Ordering::Relaxed);
120 kernel
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use std::sync::atomic::AtomicUsize;
127 use std::sync::atomic::Ordering;
128
129 use super::CpuKernel;
130
131 static SELECT_CALLS: AtomicUsize = AtomicUsize::new(0);
132
133 fn add_one(x: u64) -> u64 {
134 static KERNEL: CpuKernel<fn(u64) -> u64> = CpuKernel::new(|| {
135 SELECT_CALLS.fetch_add(1, Ordering::Relaxed);
136 |x| x + 1
137 });
138 KERNEL.get()(x)
139 }
140
141 #[test]
142 fn selects_once_then_dispatches() {
143 assert_eq!(add_one(41), 42);
144 assert_eq!(add_one(1), 2);
145 assert_eq!(add_one(2), 3);
146 assert_eq!(SELECT_CALLS.load(Ordering::Relaxed), 1);
147 }
148
149 #[test]
150 fn selector_early_returns_skip_the_default() {
151 static KERNEL: CpuKernel<fn(u64) -> u64> = CpuKernel::new(|| {
152 if 1 + 1 == 2 {
153 return |x| x + 2;
154 }
155 |x| x + 100
156 });
157 assert_eq!(KERNEL.get()(0), 2);
158 }
159
160 type XorFn = fn(&[u8; 4], &mut [u8; 4]);
161
162 #[test]
163 fn supports_reference_arguments() {
164 static KERNEL: CpuKernel<XorFn> = CpuKernel::new(|| {
165 |input, output| {
166 for (o, i) in output.iter_mut().zip(input) {
167 *o ^= *i;
168 }
169 }
170 });
171 let selected = KERNEL.get();
172 let input = [1, 2, 3, 4];
173 let mut output = [0, 0, 0, 0];
174 selected(&input, &mut output);
175 assert_eq!(output, input);
176 }
177}