Skip to main content

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}