Skip to main content

structured_zstd/
cpu_kernel.rs

1//! CPU kernel dispatch — single detect+match at the dispatch site,
2//! propagated through the inner pipeline as a generic parameter so
3//! leaf hot-path code monomorphises against the chosen kernel.
4//!
5//! See issue #247 for the architecture rationale: per-subsystem
6//! dispatch scatters the choice across HUF / FSE / SIMD-copy
7//! independently and pays the cost N times per call. Lifting the
8//! dispatch to the outermost feasible call site collapses it to one
9//! detect there; the inner leaf-hot-path ops then route through
10//! `K::method` calls on the chosen kernel zero-sized type.
11//!
12//! Current wiring (as of #247 Part 2): the only active dispatch site
13//! is `decoding::literals_section_decoder::decompress_literals`,
14//! which `match`es `detect_cpu_kernel()` and routes into per-K
15//! `decompress_literals_*` `#[target_feature]` wrappers. The full
16//! pipeline-wide propagation envisioned in the issue (FrameDecoder /
17//! FrameCompressor entry, sequence executor, match copy) is
18//! incremental; subsequent tiers extend the dispatch surface without
19//! changing this trait or the kernel ZSTs.
20//!
21//! Structure code (block loop, FCS check, offset history, repeat
22//! semantics) stays single-impl and only carries `K` as a phantom on
23//! the outer function. Monomorphisation specialises ONLY the bodies
24//! that actually differ per ISA — `mask_lower_bits`, `huf_burst`,
25//! `copy_chunk`, etc.
26
27#[cfg(feature = "std")]
28use std::sync::OnceLock;
29
30/// Trait covering the leaf hot-path operations whose bodies differ
31/// per ISA. Implementations are ZSTs; the trait is `Copy` so it can
32/// be `Default`-constructed at each call site without runtime cost.
33///
34/// New methods land here ONLY when their codegen genuinely differs
35/// per kernel (BMI2 intrinsic vs scalar shift, AVX2 256-bit move vs
36/// SSE2 128-bit move, etc.). Structure ops that have one canonical
37/// implementation must NOT be on this trait — they stay on the
38/// existing decoder / encoder types.
39// Public (rather than `pub(crate)`) because `BitReaderReversed` is
40// generic over `K: CpuKernel = ScalarKernel` and is re-exported via
41// the `bench-internals`-gated `testing` module; under that feature
42// the visibility of every type that appears in `BitReaderReversed`'s
43// bounds (the trait + the default kernel) must match the type's own
44// visibility, otherwise rustc rejects with `private_bounds` /
45// `private_interfaces`. The trait surface stays narrow on stable
46// crate users: nothing outside `bench-internals` constructs a
47// non-Scalar kernel directly.
48pub trait CpuKernel: Copy + 'static {
49    /// Mask the low `n` bits of `value`, returning the remaining
50    /// high bits zeroed. The FSE bitstream hot path fires this 3×
51    /// per decoded sequence; on BMI2-capable hardware this maps to
52    /// a single `_bzhi_u64` instruction, otherwise to a scalar
53    /// `u64::MAX >> (64 - n)` shift + mask.
54    ///
55    /// Precondition: `n <= 64`. Behaviour for `n == 0` is "return 0";
56    /// behaviour for `n > 64` is unspecified — callers MUST uphold
57    /// the bound. The test-only `mask_lower_bits` helper in
58    /// `bit_reader_reverse.rs` debug-asserts the bound for its
59    /// unit tests, but production callers (FSE / HUF hot paths)
60    /// derive `n` from `accuracy_log` / `max_num_bits` which the
61    /// per-stream table builders pin to `n <= MAX_*_BITS` at
62    /// construction time; no per-call wrapper assert runs.
63    fn mask_lower_bits(value: u64, n: u8) -> u64;
64
65    /// Split the low `n1 + n2 + n3` bits of `packed` into three fields, the
66    /// highest first. The FSE sequence decoder reads its three state updates
67    /// this way, once per sequence.
68    ///
69    /// The default is three [`Self::mask_lower_bits`]; a kernel whose hardware
70    /// extracts them in one instruction overrides it. Every implementation
71    /// returns the same three values, so which one ran is invisible to the
72    /// stream being decoded.
73    ///
74    /// Precondition: `n1 + n2 + n3 <= 64`, as for `mask_lower_bits`.
75    #[inline(always)]
76    fn extract_triple(packed: u64, n1: u8, n2: u8, n3: u8) -> (u64, u64, u64) {
77        (
78            Self::mask_lower_bits(packed.wrapping_shr(u32::from(n3) + u32::from(n2)), n1),
79            Self::mask_lower_bits(packed.wrapping_shr(u32::from(n3)), n2),
80            Self::mask_lower_bits(packed, n3),
81        )
82    }
83}
84
85/// Scalar fallback — portable, no SIMD or BMI2 intrinsics. Selected
86/// when no x86 or aarch64 feature is detected at runtime.
87#[derive(Copy, Clone, Default)]
88pub struct ScalarKernel;
89
90/// `BIT_MASK[n]` is the low `n` bits set for `n` in `0..=64`, and all bits for
91/// anything past that (a width the formats cannot ask for).
92///
93/// A table rather than `u64::MAX >> (64 - n)`: the shift form needs a guard for
94/// `n == 0`, since a 64-bit shift is undefined, and that guard is a branch or a
95/// `cmov` on every field of every sequence. Indexed by a `u8`, and sized for
96/// every `u8`, so the load carries no bounds check either. The widths in use
97/// are small, so the hot part is the first few cache lines of it.
98pub(crate) const BIT_MASK: [u64; 256] = {
99    let mut table = [u64::MAX; 256];
100    let mut i: usize = 0;
101    while i < 64 {
102        table[i] = (1u64 << i) - 1;
103        i += 1;
104    }
105    table
106};
107
108impl CpuKernel for ScalarKernel {
109    #[inline(always)]
110    fn mask_lower_bits(value: u64, n: u8) -> u64 {
111        value & BIT_MASK[n as usize]
112    }
113}
114
115// The SSE2 tier exists in `CpuKernelTag` (it carries the 128-bit copy-chunk
116// choice for the unified copy dispatch) but needs no `CpuKernel` ZST yet: the
117// only trait method, `mask_lower_bits`, has no SSE2-specific form (SSE2 has no
118// bit-extract), so the Sse2 tag routes through the scalar bodies for the
119// FSE/HUF paths. A dedicated `Sse2Kernel` lands when `copy_chunk` moves onto
120// the trait.
121
122/// BMI2-only kernel: `bzhi` for mask_lower_bits. Selected when the CPU has
123/// BMI2 but not the AVX2 SIMD width to upgrade to the Avx2 kernel. Treated as
124/// a stepping stone between Sse2 and Avx2 on hardware that has BMI2 but not
125/// AVX2 (rare in practice but matches upstream zstd's gating). Present on
126/// 32-bit x86 as well as x86_64: the instruction is there, only its width
127/// differs, and without this tier a 32-bit build would decode on the scalar
128/// bodies whatever the CPU offers.
129#[cfg(all(
130    any(target_arch = "x86", target_arch = "x86_64"),
131    feature = "kernel-bmi2"
132))]
133#[derive(Copy, Clone, Default)]
134pub(crate) struct Bmi2Kernel;
135
136#[cfg(all(
137    any(target_arch = "x86", target_arch = "x86_64"),
138    feature = "kernel-bmi2"
139))]
140impl CpuKernel for Bmi2Kernel {
141    #[inline(always)]
142    fn mask_lower_bits(value: u64, n: u8) -> u64 {
143        // SAFETY: this kernel ZST is only reachable via the
144        // `match detect_cpu_kernel() { CpuKernelTag::Bmi2 => ... }`
145        // dispatch arms at decoder entry sites, all of which fire only
146        // after `detect_cpu_kernel` confirmed BMI2 is available on the
147        // running CPU.
148        unsafe { mask_lower_bits_bmi2_impl(value, n) }
149    }
150}
151
152/// x86_64 AVX2 + BMI2 kernel (x86-64-v3 baseline). The common modern
153/// x86 case — most CPUs released since 2013 (Haswell) have AVX2+BMI2.
154/// Uses `_bzhi_u64` for mask ops; future trait methods will use AVX2
155/// 256-bit moves for `copy_chunk` and pext for HUF burst.
156#[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
157#[derive(Copy, Clone, Default)]
158pub(crate) struct Avx2Kernel;
159
160#[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
161impl CpuKernel for Avx2Kernel {
162    #[inline(always)]
163    fn mask_lower_bits(value: u64, n: u8) -> u64 {
164        // SAFETY: Avx2Kernel is selected only after runtime detect
165        // confirmed both AVX2 and BMI2 — `_bzhi_u64` is callable.
166        unsafe { mask_lower_bits_bmi2_impl(value, n) }
167    }
168}
169
170/// x86_64 AVX-512 VBMI2 + AVX2 + BMI2 kernel. Selected when the CPU
171/// has the AVX-512 VBMI2 family available — VBMI2 unlocks a faster
172/// HUF burst inner loop (VPSHUFB-based table lookup); BMI2 mask_lower
173/// bits stays identical to Avx2 kernel.
174#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
175#[derive(Copy, Clone, Default)]
176pub(crate) struct Vbmi2Kernel;
177
178#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
179impl CpuKernel for Vbmi2Kernel {
180    #[inline(always)]
181    fn mask_lower_bits(value: u64, n: u8) -> u64 {
182        // SAFETY: same precondition as Avx2Kernel — BMI2 confirmed
183        // at runtime before this kernel is instantiated.
184        unsafe { mask_lower_bits_bmi2_impl(value, n) }
185    }
186}
187
188/// aarch64 NEON baseline kernel. Used on all aarch64 hardware that
189/// exposes NEON (effectively universal on the supported targets).
190///
191/// `#[allow(dead_code)]`: scaffolding for the future aarch64 dispatch
192/// arm in `decompress_literals` / `decode_and_execute_sequences`.
193/// The struct + trait impl land first so the dispatch wiring can be
194/// added incrementally without churning the CpuKernel surface; until
195/// the dispatch arm uses it the type is reachable only as a phantom.
196#[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
197#[allow(dead_code)]
198#[derive(Copy, Clone, Default)]
199pub(crate) struct NeonKernel;
200
201#[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
202impl CpuKernel for NeonKernel {
203    #[inline(always)]
204    fn mask_lower_bits(value: u64, n: u8) -> u64 {
205        // aarch64 has no BMI2 equivalent that improves on the scalar
206        // shift-and-mask sequence for this op; the codegen is
207        // identical to the Scalar kernel here. Other trait methods
208        // (huf_burst, copy_chunk) will diverge once they land.
209        ScalarKernel::mask_lower_bits(value, n)
210    }
211}
212
213/// aarch64 SVE kernel. Variable-vector-length SVE extends NEON for
214/// HUF burst / SIMD copy on Graviton3 / Apple M-series with SVE
215/// support. Mask op identical to NEON / Scalar.
216///
217/// `#[allow(dead_code)]`: same scaffolding rationale as `NeonKernel`.
218#[cfg(all(target_arch = "aarch64", feature = "kernel-sve"))]
219#[allow(dead_code)]
220#[derive(Copy, Clone, Default)]
221pub(crate) struct SveKernel;
222
223#[cfg(all(target_arch = "aarch64", feature = "kernel-sve"))]
224impl CpuKernel for SveKernel {
225    #[inline(always)]
226    fn mask_lower_bits(value: u64, n: u8) -> u64 {
227        ScalarKernel::mask_lower_bits(value, n)
228    }
229}
230
231/// Single `#[target_feature(enable = "bmi2")]` wrapper around the
232/// `_bzhi_u64` intrinsic. Lifted to a free function so each kernel
233/// impl that needs the BMI2 path (Bmi2 / Avx2 / Vbmi2) calls the
234/// same shared body. With `#[inline]` LLVM inlines the call into
235/// any caller that itself has BMI2 in scope; outside that scope the
236/// target_feature boundary is preserved.
237#[cfg(all(
238    any(target_arch = "x86", target_arch = "x86_64"),
239    feature = "kernel-bmi2"
240))]
241#[target_feature(enable = "bmi2")]
242#[inline]
243unsafe fn mask_lower_bits_bmi2_impl(value: u64, n: u8) -> u64 {
244    // The intrinsic call is permitted directly inside a function
245    // already annotated `#[target_feature(enable = "bmi2")]` — no
246    // `unsafe { ... }` block needed (the function-level `unsafe`
247    // already covers it). SAFETY: caller selected a kernel whose
248    // CpuKernelTag was resolved after `is_x86_feature_detected!("bmi2")`
249    // returned true, so the BMI2 instruction set is available.
250    #[cfg(target_arch = "x86_64")]
251    {
252        core::arch::x86_64::_bzhi_u64(value, n as u32)
253    }
254    // 32-bit x86 has `bzhi` on 32-bit registers only. Widths up to 32 take one
255    // instruction on the low half; wider ones keep the low 32 bits whole and
256    // apply it to the high half, which is what a 64-bit `bzhi` does in one go.
257    #[cfg(target_arch = "x86")]
258    {
259        use core::arch::x86::_bzhi_u32;
260        if n >= 64 {
261            return value;
262        }
263        if n <= 32 {
264            return u64::from(_bzhi_u32(value as u32, u32::from(n)));
265        }
266        let high = _bzhi_u32((value >> 32) as u32, u32::from(n) - 32);
267        (value & u64::from(u32::MAX)) | (u64::from(high) << 32)
268    }
269}
270
271/// Pure boolean-input variant of the x86 kernel-tag selection. Both the
272/// `std` runtime-detect path and the `no_std` compile-time-cfg path
273/// route through this helper so the precedence rules stay in one place
274/// (and are unit-testable without runtime CPUID).
275///
276/// The VBMI2 tier requires every AVX-512 sub-feature it touches AND the
277/// AVX2 baseline — VBMI2 kernels mix VBMI2-only intrinsics with AVX2
278/// 256-bit moves, so the dispatch must be conditioned on `has_avx2` too.
279/// Likewise the Avx2 tier requires both AVX2 and BMI2.
280#[cfg(target_arch = "x86_64")]
281#[inline(always)]
282// Params go unused when the matching `kernel_*` feature is disabled (the
283// rung that consumes them is `#[cfg]`-ed out); they are still passed by the
284// detect callers. Silence the conditional unused-variable warning rather
285// than thread per-feature `_`-prefixes through the signature.
286#[allow(unused_variables)]
287const fn select_x86_kernel(
288    has_avx512vbmi2: bool,
289    has_avx512f: bool,
290    has_avx512vl: bool,
291    has_avx512bw: bool,
292    has_bmi2: bool,
293    has_avx2: bool,
294    has_sse2: bool,
295) -> CpuKernelTag {
296    #[cfg(feature = "kernel-vbmi2")]
297    if has_avx512vbmi2 && has_avx512f && has_avx512vl && has_avx512bw && has_bmi2 && has_avx2 {
298        return CpuKernelTag::Vbmi2;
299    }
300    #[cfg(feature = "kernel-avx2")]
301    if has_avx2 && has_bmi2 {
302        return CpuKernelTag::Avx2;
303    }
304    #[cfg(feature = "kernel-bmi2")]
305    if has_bmi2 {
306        return CpuKernelTag::Bmi2;
307    }
308    #[cfg(feature = "kernel-sse")]
309    if has_sse2 {
310        return CpuKernelTag::Sse2;
311    }
312    CpuKernelTag::Scalar
313}
314
315/// Cached runtime-detected kernel tag. The actual `CpuKernel` impl
316/// (`ScalarKernel` / `Bmi2Kernel` / `Avx2Kernel` / `Vbmi2Kernel` /
317/// `NeonKernel` / `SveKernel`) is constructed at the dispatch site —
318/// currently only `decoding::literals_section_decoder::decompress_literals`
319/// — via a `match` on this tag that branches into the per-K
320/// `target_feature`-wrapped specialisation. Pipeline-wide dispatch
321/// (FrameDecoder / FrameCompressor entry, sequence executor, match
322/// copy) lands incrementally in follow-up tiers.
323#[derive(Copy, Clone, Debug, Eq, PartialEq)]
324pub(crate) enum CpuKernelTag {
325    Scalar,
326    #[cfg(all(target_arch = "x86_64", feature = "kernel-sse"))]
327    Sse2,
328    /// Reachable on 32-bit x86 as well: `bzhi` is there, and without the tier
329    /// such a build would decode on the scalar bodies whatever the CPU offers.
330    #[cfg(all(
331        any(target_arch = "x86", target_arch = "x86_64"),
332        feature = "kernel-bmi2"
333    ))]
334    Bmi2,
335    #[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
336    Avx2,
337    #[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
338    Vbmi2,
339    #[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
340    Neon,
341    // Both constructors of `Sve` need a reachable feature: runtime
342    // detection via `std::arch::is_aarch64_feature_detected!` (so
343    // `feature = "std"`) or compile-time `target_feature = "sve"` in
344    // RUSTFLAGS. Without either, the variant is unreachable and a
345    // `match` arm referencing it warns as dead.
346    #[cfg(all(
347        target_arch = "aarch64",
348        feature = "kernel-sve",
349        any(feature = "std", target_feature = "sve"),
350    ))]
351    Sve,
352}
353
354/// Detect once and cache the best available CPU kernel for the
355/// current process. Subsequent calls return the cached tag without
356/// re-running CPU-feature detection. Std-only — no-std targets use
357/// the compile-time variant below that resolves at build time.
358#[cfg(feature = "std")]
359pub(crate) fn detect_cpu_kernel() -> CpuKernelTag {
360    static CACHED: OnceLock<CpuKernelTag> = OnceLock::new();
361    *CACHED.get_or_init(detect_cpu_kernel_uncached)
362}
363
364#[cfg(feature = "std")]
365fn detect_cpu_kernel_uncached() -> CpuKernelTag {
366    #[cfg(target_arch = "x86_64")]
367    {
368        use std::arch::is_x86_feature_detected;
369        // Gate each probe on its tier feature: `cfg!(...)` const-folds, so the
370        // `&&` short-circuits away the runtime `is_x86_feature_detected!` call
371        // (and its CPUID/cache traffic) for tiers the build disabled — the
372        // matching `select_x86_kernel` rung is `#[cfg]`-ed out anyway.
373        return select_x86_kernel(
374            cfg!(feature = "kernel-vbmi2") && is_x86_feature_detected!("avx512vbmi2"),
375            cfg!(feature = "kernel-vbmi2") && is_x86_feature_detected!("avx512f"),
376            cfg!(feature = "kernel-vbmi2") && is_x86_feature_detected!("avx512vl"),
377            cfg!(feature = "kernel-vbmi2") && is_x86_feature_detected!("avx512bw"),
378            cfg!(feature = "kernel-bmi2") && is_x86_feature_detected!("bmi2"),
379            cfg!(feature = "kernel-avx2") && is_x86_feature_detected!("avx2"),
380            cfg!(feature = "kernel-sse") && is_x86_feature_detected!("sse2"),
381        );
382    }
383    // 32-bit x86 carries only the BMI2 tier: the wider tiers' kernels and
384    // their `target_feature` bodies are x86_64-only, so there is nothing
385    // above `bzhi` to select here.
386    #[cfg(target_arch = "x86")]
387    {
388        #[cfg(feature = "kernel-bmi2")]
389        {
390            use std::arch::is_x86_feature_detected;
391            if is_x86_feature_detected!("bmi2") {
392                return CpuKernelTag::Bmi2;
393            }
394        }
395        return CpuKernelTag::Scalar;
396    }
397    #[cfg(target_arch = "aarch64")]
398    {
399        #[cfg(any(feature = "kernel-sve", feature = "kernel-neon"))]
400        use std::arch::is_aarch64_feature_detected;
401        #[cfg(feature = "kernel-sve")]
402        if is_aarch64_feature_detected!("sve") {
403            return CpuKernelTag::Sve;
404        }
405        #[cfg(feature = "kernel-neon")]
406        if is_aarch64_feature_detected!("neon") {
407            return CpuKernelTag::Neon;
408        }
409        return CpuKernelTag::Scalar;
410    }
411    #[allow(unreachable_code)]
412    CpuKernelTag::Scalar
413}
414
415/// no-std variant: rely on compile-time `target_feature` flags
416/// instead of runtime detection. Resolves to the most-capable kernel
417/// that the build target supports.
418#[cfg(not(feature = "std"))]
419pub(crate) fn detect_cpu_kernel() -> CpuKernelTag {
420    #[cfg(target_arch = "x86_64")]
421    {
422        // Route through the same const-fn precedence helper as the
423        // `feature = "std"` path. `cfg!(target_feature = ...)`
424        // returns a compile-time bool that constant-folds through
425        // `select_x86_kernel`, so the runtime call has the same
426        // codegen as the previous hand-written #[cfg] chain.
427        return select_x86_kernel(
428            cfg!(target_feature = "avx512vbmi2"),
429            cfg!(target_feature = "avx512f"),
430            cfg!(target_feature = "avx512vl"),
431            cfg!(target_feature = "avx512bw"),
432            cfg!(target_feature = "bmi2"),
433            cfg!(target_feature = "avx2"),
434            cfg!(target_feature = "sse2"),
435        );
436    }
437    #[cfg(target_arch = "x86")]
438    {
439        #[cfg(all(feature = "kernel-bmi2", target_feature = "bmi2"))]
440        {
441            return CpuKernelTag::Bmi2;
442        }
443    }
444    #[cfg(target_arch = "aarch64")]
445    {
446        #[cfg(all(feature = "kernel-sve", target_feature = "sve"))]
447        {
448            return CpuKernelTag::Sve;
449        }
450        #[cfg(all(feature = "kernel-neon", target_feature = "neon"))]
451        {
452            return CpuKernelTag::Neon;
453        }
454    }
455    #[allow(unreachable_code)]
456    CpuKernelTag::Scalar
457}
458
459impl CpuKernelTag {
460    /// Stable lowercase diagnostic name for this tier (used by
461    /// [`active_cpu_kernel_name`] and the bench/dashboard reporting). Pure
462    /// mapping over the tag, so every arm is exercisable in tests regardless
463    /// of which tier the running CPU actually resolves to.
464    pub(crate) fn name(self) -> &'static str {
465        match self {
466            CpuKernelTag::Scalar => "scalar",
467            #[cfg(all(target_arch = "x86_64", feature = "kernel-sse"))]
468            CpuKernelTag::Sse2 => "sse2",
469            #[cfg(all(
470                any(target_arch = "x86", target_arch = "x86_64"),
471                feature = "kernel-bmi2"
472            ))]
473            CpuKernelTag::Bmi2 => "bmi2",
474            #[cfg(all(target_arch = "x86_64", feature = "kernel-avx2"))]
475            CpuKernelTag::Avx2 => "avx2",
476            #[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
477            CpuKernelTag::Vbmi2 => "vbmi2",
478            #[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
479            CpuKernelTag::Neon => "neon",
480            #[cfg(all(
481                target_arch = "aarch64",
482                feature = "kernel-sve",
483                any(feature = "std", target_feature = "sve"),
484            ))]
485            CpuKernelTag::Sve => "sve",
486        }
487    }
488}
489
490/// Name of the CPU kernel tier this process selected for the entropy /
491/// sequence hot paths: decode (literals + FSE sequence decode) and encode
492/// (entropy) share this dispatch (see #247). Returned as a stable lowercase
493/// string for diagnostics and benchmark/dashboard reporting; the value is
494/// what the runtime CPU-feature detection (or compile-time `target_feature`
495/// on `no_std`) actually resolves to on this machine, so a dashboard can
496/// attribute a measurement to the kernel that produced it.
497pub fn active_cpu_kernel_name() -> &'static str {
498    detect_cpu_kernel().name()
499}
500
501#[cfg(test)]
502mod tests;