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;