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/// An instruction-set level the codec's kernels may be limited to, from the
31/// portable scalar code up. The x86 levels (`Sse2` through `Avx512`) and the
32/// aarch64 ones (`Neon`, `Sve`) are separate ladders; `Scalar` sits under
33/// both.
34///
35/// Used with [`set_cpu_ceiling`] to compare kernel tiers on one machine.
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum CpuLevel {
39 /// Portable code only.
40 Scalar,
41 /// x86 SSE2.
42 Sse2,
43 /// x86 SSE4.2.
44 Sse42,
45 /// x86 BMI2, with everything below it.
46 Bmi2,
47 /// x86 AVX2 (and BMI2), the x86-64-v3 kernels.
48 Avx2,
49 /// x86 AVX-512 (the VBMI2 kernels, when built with `kernel-vbmi2`).
50 Avx512,
51 /// aarch64 NEON.
52 Neon,
53 /// aarch64 SVE and SVE2.
54 Sve,
55}
56
57impl CpuLevel {
58 /// Every level, lowest first within each ladder.
59 pub const ALL: [CpuLevel; 8] = [
60 CpuLevel::Scalar,
61 CpuLevel::Sse2,
62 CpuLevel::Sse42,
63 CpuLevel::Bmi2,
64 CpuLevel::Avx2,
65 CpuLevel::Avx512,
66 CpuLevel::Neon,
67 CpuLevel::Sve,
68 ];
69
70 /// The level's name as [`FromStr`](core::str::FromStr) reads it.
71 ///
72 /// # Examples
73 /// ```
74 /// use structured_zstd::CpuLevel;
75 ///
76 /// assert_eq!(CpuLevel::Sse42.name(), "sse4.2");
77 /// assert_eq!("avx2".parse::<CpuLevel>(), Ok(CpuLevel::Avx2));
78 /// ```
79 pub const fn name(self) -> &'static str {
80 match self {
81 CpuLevel::Scalar => "scalar",
82 CpuLevel::Sse2 => "sse2",
83 CpuLevel::Sse42 => "sse4.2",
84 CpuLevel::Bmi2 => "bmi2",
85 CpuLevel::Avx2 => "avx2",
86 CpuLevel::Avx512 => "avx512",
87 CpuLevel::Neon => "neon",
88 CpuLevel::Sve => "sve",
89 }
90 }
91
92 /// Which ladder the level is on, and its rung: 0 for `Scalar` on either.
93 const fn ladder(self) -> (Ladder, u8) {
94 match self {
95 CpuLevel::Scalar => (Ladder::Any, 0),
96 CpuLevel::Sse2 => (Ladder::X86, 1),
97 CpuLevel::Sse42 => (Ladder::X86, 2),
98 CpuLevel::Bmi2 => (Ladder::X86, 3),
99 CpuLevel::Avx2 => (Ladder::X86, 4),
100 CpuLevel::Avx512 => (Ladder::X86, 5),
101 CpuLevel::Neon => (Ladder::Arm, 1),
102 CpuLevel::Sve => (Ladder::Arm, 2),
103 }
104 }
105
106 /// Whether the level exists on the architecture this build targets.
107 const fn native(self) -> bool {
108 match self.ladder().0 {
109 Ladder::Any => true,
110 Ladder::X86 => cfg!(any(target_arch = "x86", target_arch = "x86_64")),
111 Ladder::Arm => cfg!(target_arch = "aarch64"),
112 }
113 }
114}
115
116impl core::fmt::Display for CpuLevel {
117 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118 f.write_str(self.name())
119 }
120}
121
122/// A name that is not a [`CpuLevel`].
123#[derive(Copy, Clone, Debug, PartialEq, Eq)]
124pub struct UnknownCpuLevel;
125
126impl core::fmt::Display for UnknownCpuLevel {
127 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128 f.write_str("unknown CPU level; expected one of ")?;
129 for (index, level) in CpuLevel::ALL.iter().enumerate() {
130 if index > 0 {
131 f.write_str(", ")?;
132 }
133 f.write_str(level.name())?;
134 }
135 Ok(())
136 }
137}
138
139#[cfg(feature = "std")]
140impl std::error::Error for UnknownCpuLevel {}
141
142impl core::str::FromStr for CpuLevel {
143 type Err = UnknownCpuLevel;
144
145 /// The names [`CpuLevel::name`] gives, case-insensitive, plus the spellings
146 /// `sse42`, `avx-512` and `avx512f`.
147 fn from_str(text: &str) -> Result<Self, Self::Err> {
148 let lower = |expected: &str| text.eq_ignore_ascii_case(expected);
149 CpuLevel::ALL
150 .into_iter()
151 .find(|level| lower(level.name()))
152 .or_else(|| {
153 if lower("sse42") {
154 Some(CpuLevel::Sse42)
155 } else if lower("avx-512") || lower("avx512f") {
156 Some(CpuLevel::Avx512)
157 } else {
158 None
159 }
160 })
161 .ok_or(UnknownCpuLevel)
162 }
163}
164
165#[derive(Copy, Clone, PartialEq, Eq)]
166enum Ladder {
167 Any,
168 X86,
169 Arm,
170}
171
172/// Why [`set_cpu_ceiling`] could not set the ceiling.
173#[derive(Copy, Clone, Debug, PartialEq, Eq)]
174#[non_exhaustive]
175pub enum CpuCeilingError {
176 /// A kernel was already chosen, or a ceiling already set: the tiers in
177 /// use cannot change for the rest of the process.
178 AlreadyResolved,
179 /// The level belongs to another architecture than this build's.
180 OtherArchitecture(CpuLevel),
181 /// This target has no atomics to hold a ceiling in.
182 Unsupported,
183}
184
185impl core::fmt::Display for CpuCeilingError {
186 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
187 match self {
188 CpuCeilingError::AlreadyResolved => {
189 f.write_str("the CPU kernels were already chosen; set the ceiling before any work")
190 }
191 CpuCeilingError::OtherArchitecture(level) => {
192 write!(f, "{level} is not an instruction set of this architecture")
193 }
194 CpuCeilingError::Unsupported => f.write_str("this target cannot hold a CPU ceiling"),
195 }
196 }
197}
198
199#[cfg(feature = "std")]
200impl std::error::Error for CpuCeilingError {}
201
202/// The ceiling, frozen at its first read: 0 is not yet read or set,
203/// [`CEILING_NONE`] is frozen without a limit, anything else is a level's
204/// index in [`CpuLevel::ALL`] plus one.
205#[cfg(target_has_atomic = "8")]
206static CEILING: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
207#[cfg(target_has_atomic = "8")]
208const CEILING_NONE: u8 = u8::MAX;
209
210/// Limit every kernel the codec picks at run time to `level` and below, for
211/// the rest of the process. A kernel is still used only where the CPU
212/// supports it: the ceiling never raises a choice.
213///
214/// Only the run-time choices are limited. A build compiled for a wider
215/// baseline (`-C target-cpu=...`) uses those instructions throughout, below
216/// any ceiling.
217///
218/// # Errors
219///
220/// [`CpuCeilingError::AlreadyResolved`] once any kernel has been chosen or a
221/// ceiling set. Building a codec context can already choose kernels (a
222/// decoder does, at construction), and so does querying one
223/// ([`active_cpu_kernel_name`],
224/// [`cpu_ceiling`]): set the ceiling before creating any context, not merely
225/// before the first compression or decompression.
226/// [`CpuCeilingError::OtherArchitecture`] for a level of another architecture.
227///
228/// # Examples
229/// ```standalone_crate
230/// use structured_zstd::{CpuLevel, set_cpu_ceiling};
231///
232/// // Before any compression or decompression in the process.
233/// set_cpu_ceiling(CpuLevel::Scalar).unwrap();
234/// assert_eq!(structured_zstd::cpu_ceiling(), Some(CpuLevel::Scalar));
235/// ```
236pub fn set_cpu_ceiling(level: CpuLevel) -> Result<(), CpuCeilingError> {
237 if !level.native() {
238 return Err(CpuCeilingError::OtherArchitecture(level));
239 }
240 #[cfg(target_has_atomic = "8")]
241 {
242 use core::sync::atomic::Ordering;
243 let index = CpuLevel::ALL
244 .iter()
245 .position(|candidate| *candidate == level)
246 .expect("every level is in ALL") as u8;
247 CEILING
248 .compare_exchange(0, index + 1, Ordering::AcqRel, Ordering::Acquire)
249 .map(|_| ())
250 .map_err(|_| CpuCeilingError::AlreadyResolved)
251 }
252 #[cfg(not(target_has_atomic = "8"))]
253 {
254 Err(CpuCeilingError::Unsupported)
255 }
256}
257
258/// The ceiling in force, freezing it (as none) if nothing set one yet.
259pub fn cpu_ceiling() -> Option<CpuLevel> {
260 #[cfg(target_has_atomic = "8")]
261 {
262 use core::sync::atomic::Ordering;
263 // Once frozen the value never changes, so a plain load answers every
264 // later call; only the first read pays the locked compare-exchange.
265 let mut stored = CEILING.load(Ordering::Acquire);
266 if stored == 0 {
267 stored = match CEILING.compare_exchange(
268 0,
269 CEILING_NONE,
270 Ordering::AcqRel,
271 Ordering::Acquire,
272 ) {
273 Ok(_) => CEILING_NONE,
274 Err(stored) => stored,
275 };
276 }
277 (stored != CEILING_NONE).then(|| CpuLevel::ALL[usize::from(stored - 1)])
278 }
279 #[cfg(not(target_has_atomic = "8"))]
280 {
281 None
282 }
283}
284
285/// Whether a kernel needing `needed` may run under `ceiling`.
286// Only a build that compiles a run-time kernel choice asks; wasm, or a target
287// with its kernel features off, compiles none.
288#[cfg_attr(
289 not(any(
290 test,
291 target_arch = "x86_64",
292 all(target_arch = "x86", feature = "kernel-sse"),
293 all(target_arch = "aarch64", feature = "kernel-neon"),
294 )),
295 allow(dead_code)
296)]
297const fn allowed_under(ceiling: Option<CpuLevel>, needed: CpuLevel) -> bool {
298 let Some(ceiling) = ceiling else {
299 return true;
300 };
301 let (needed_ladder, needed_rung) = needed.ladder();
302 let (ceiling_ladder, ceiling_rung) = ceiling.ladder();
303 needed_rung == 0 || (needed_ladder as u8 == ceiling_ladder as u8 && needed_rung <= ceiling_rung)
304}
305
306/// Whether a kernel needing `needed` may be chosen. Every run-time kernel
307/// selection asks this beside its CPU-feature probe.
308#[cfg_attr(
309 not(any(
310 target_arch = "x86_64",
311 all(target_arch = "x86", feature = "kernel-sse"),
312 all(target_arch = "aarch64", feature = "kernel-neon"),
313 )),
314 allow(dead_code)
315)]
316#[inline]
317pub(crate) fn cpu_allows(needed: CpuLevel) -> bool {
318 allowed_under(cpu_ceiling(), needed)
319}
320
321/// Trait covering the leaf hot-path operations whose bodies differ
322/// per ISA. Implementations are ZSTs; the trait is `Copy` so it can
323/// be `Default`-constructed at each call site without runtime cost.
324///
325/// New methods land here ONLY when their codegen genuinely differs
326/// per kernel (BMI2 intrinsic vs scalar shift, AVX2 256-bit move vs
327/// SSE2 128-bit move, etc.). Structure ops that have one canonical
328/// implementation must NOT be on this trait — they stay on the
329/// existing decoder / encoder types.
330// Public (rather than `pub(crate)`) because `BitReaderReversed` is
331// generic over `K: CpuKernel = ScalarKernel` and is re-exported via
332// the `bench-internals`-gated `testing` module; under that feature
333// the visibility of every type that appears in `BitReaderReversed`'s
334// bounds (the trait + the default kernel) must match the type's own
335// visibility, otherwise rustc rejects with `private_bounds` /
336// `private_interfaces`. The trait surface stays narrow on stable
337// crate users: nothing outside `bench-internals` constructs a
338// non-Scalar kernel directly.
339pub trait CpuKernel: Copy + 'static {
340 /// Mask the low `n` bits of `value`, returning the remaining
341 /// high bits zeroed. The FSE bitstream hot path fires this 3×
342 /// per decoded sequence; on BMI2-capable hardware this maps to
343 /// a single `_bzhi_u64` instruction, otherwise to a scalar
344 /// `u64::MAX >> (64 - n)` shift + mask.
345 ///
346 /// Precondition: `n <= 64`. Behaviour for `n == 0` is "return 0";
347 /// behaviour for `n > 64` is unspecified — callers MUST uphold
348 /// the bound. The test-only `mask_lower_bits` helper in
349 /// `bit_reader_reverse.rs` debug-asserts the bound for its
350 /// unit tests, but production callers (FSE / HUF hot paths)
351 /// derive `n` from `accuracy_log` / `max_num_bits` which the
352 /// per-stream table builders pin to `n <= MAX_*_BITS` at
353 /// construction time; no per-call wrapper assert runs.
354 fn mask_lower_bits(value: u64, n: u8) -> u64;
355
356 /// Split the low `n1 + n2 + n3` bits of `packed` into three fields, the
357 /// highest first. The FSE sequence decoder reads its three state updates
358 /// this way, once per sequence.
359 ///
360 /// The default is three [`Self::mask_lower_bits`]; a kernel whose hardware
361 /// extracts them in one instruction overrides it. Every implementation
362 /// returns the same three values, so which one ran is invisible to the
363 /// stream being decoded.
364 ///
365 /// Precondition: `n1 + n2 + n3 <= 64`, as for `mask_lower_bits`.
366 #[inline(always)]
367 fn extract_triple(packed: u64, n1: u8, n2: u8, n3: u8) -> (u64, u64, u64) {
368 (
369 Self::mask_lower_bits(packed.wrapping_shr(u32::from(n3) + u32::from(n2)), n1),
370 Self::mask_lower_bits(packed.wrapping_shr(u32::from(n3)), n2),
371 Self::mask_lower_bits(packed, n3),
372 )
373 }
374
375 /// Width in bytes of [`Self::copy_chunks`]'s stores. The default is
376 /// portable code, a machine word (`simd128` on wasm, which has no run-time
377 /// tier); each SIMD tier overrides it with its own vector.
378 const COPY_CHUNK: usize = crate::decoding::simd_copy::PORTABLE_COPY_CHUNK;
379
380 /// Copy `len` bytes, a multiple of [`Self::COPY_CHUNK`], in whole chunks:
381 /// the wildcopy body of the decoder's buffer copies.
382 ///
383 /// # Safety
384 /// `src` readable and `dst` writable for `len` bytes; the regions do not
385 /// overlap; the running CPU supports this kernel's tier.
386 #[inline(always)]
387 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
388 unsafe { crate::decoding::simd_copy::copy_chunks_portable(src, dst, len) }
389 }
390
391 /// Width of [`Self::copy_step`], the chunk a wide tier steps down to when
392 /// the slack left does not fit its own; equal to [`Self::COPY_CHUNK`]
393 /// where there is nothing narrower to step to.
394 const STEP_CHUNK: usize = Self::COPY_CHUNK;
395
396 /// Copy `len` bytes, a multiple of [`Self::STEP_CHUNK`], in whole chunks.
397 ///
398 /// # Safety
399 /// As [`Self::copy_chunks`].
400 #[inline(always)]
401 unsafe fn copy_step(src: *const u8, dst: *mut u8, len: usize) {
402 unsafe { Self::copy_chunks(src, dst, len) }
403 }
404
405 /// Copy exactly 16 bytes in one transfer, for a short copy whose buffers
406 /// have the room to overshoot to 16.
407 ///
408 /// # Safety
409 /// `src` readable and `dst` writable for 16 bytes; the regions do not
410 /// overlap; the running CPU supports this kernel's tier.
411 #[inline(always)]
412 unsafe fn copy16(src: *const u8, dst: *mut u8) {
413 unsafe { crate::decoding::simd_copy::copy16_portable(src, dst) }
414 }
415}
416
417/// Copies at the build's baseline vector (SSE2 where the target guarantees it,
418/// NEON, `simd128`) and masks as the scalar tier does: for the copies that run
419/// outside any tier's dispatch, a raw block's or a literals-only block's, which
420/// every CPU the build runs on can take at that width.
421#[derive(Copy, Clone, Default)]
422pub(crate) struct BaselineKernel;
423
424impl CpuKernel for BaselineKernel {
425 #[inline(always)]
426 fn mask_lower_bits(value: u64, n: u8) -> u64 {
427 ScalarKernel::mask_lower_bits(value, n)
428 }
429
430 const COPY_CHUNK: usize = crate::decoding::simd_copy::BASELINE_COPY_CHUNK;
431
432 #[inline(always)]
433 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
434 // SAFETY: the baseline runs on every CPU the build does.
435 unsafe { crate::decoding::simd_copy::copy_chunks_baseline(src, dst, len) }
436 }
437
438 #[inline(always)]
439 unsafe fn copy16(src: *const u8, dst: *mut u8) {
440 // SAFETY: as above.
441 unsafe { crate::decoding::simd_copy::copy16_baseline(src, dst) }
442 }
443}
444
445/// The SSE2 copies every x86 SIMD tier shares: 16-byte chunks. A macro, so
446/// each kernel's methods expand them in place.
447#[cfg(all(
448 any(target_arch = "x86", target_arch = "x86_64"),
449 feature = "kernel-sse"
450))]
451macro_rules! sse2_copies {
452 () => {
453 const COPY_CHUNK: usize = 16;
454
455 #[inline(always)]
456 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
457 // SAFETY: every tier using this was selected with SSE2 confirmed.
458 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, len) }
459 }
460
461 #[inline(always)]
462 unsafe fn copy16(src: *const u8, dst: *mut u8) {
463 // SAFETY: as above.
464 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, 16) }
465 }
466 };
467}
468
469/// x86 SSE2 kernel: scalar masks (SSE2 has no bit extract) and 16-byte SSE2
470/// copies. Chosen at run time, so a 32-bit build whose baseline lacks SSE2
471/// still copies in vectors on a CPU that has it.
472#[cfg(all(
473 any(target_arch = "x86", target_arch = "x86_64"),
474 feature = "kernel-sse"
475))]
476#[derive(Copy, Clone, Default)]
477pub(crate) struct Sse2Kernel;
478
479#[cfg(all(
480 any(target_arch = "x86", target_arch = "x86_64"),
481 feature = "kernel-sse"
482))]
483impl CpuKernel for Sse2Kernel {
484 #[inline(always)]
485 fn mask_lower_bits(value: u64, n: u8) -> u64 {
486 ScalarKernel::mask_lower_bits(value, n)
487 }
488
489 sse2_copies!();
490}
491
492/// Scalar fallback — portable, no SIMD or BMI2 intrinsics. Selected
493/// when no x86 or aarch64 feature is detected at runtime.
494#[derive(Copy, Clone, Default)]
495pub struct ScalarKernel;
496
497/// `BIT_MASK[n]` is the low `n` bits set for `n` in `0..=64`, and all bits for
498/// anything past that (a width the formats cannot ask for).
499///
500/// A table rather than `u64::MAX >> (64 - n)`: the shift form needs a guard for
501/// `n == 0`, since a 64-bit shift is undefined, and that guard is a branch or a
502/// `cmov` on every field of every sequence. Indexed by a `u8`, and sized for
503/// every `u8`, so the load carries no bounds check either. The widths in use
504/// are small, so the hot part is the first few cache lines of it.
505pub(crate) const BIT_MASK: [u64; 256] = {
506 let mut table = [u64::MAX; 256];
507 let mut i: usize = 0;
508 while i < 64 {
509 table[i] = (1u64 << i) - 1;
510 i += 1;
511 }
512 table
513};
514
515impl CpuKernel for ScalarKernel {
516 #[inline(always)]
517 fn mask_lower_bits(value: u64, n: u8) -> u64 {
518 value & BIT_MASK[n as usize]
519 }
520}
521
522/// BMI2-only kernel: `bzhi` for mask_lower_bits. Selected when the CPU has
523/// BMI2 but not the AVX2 SIMD width to upgrade to the Avx2 kernel. Treated as
524/// a stepping stone between Sse2 and Avx2 on hardware that has BMI2 but not
525/// AVX2 (rare in practice but matches upstream zstd's gating). Present on
526/// 32-bit x86 as well as x86_64: the instruction is there, only its width
527/// differs, and without this tier a 32-bit build would decode on the scalar
528/// bodies whatever the CPU offers.
529#[cfg(all(
530 any(target_arch = "x86", target_arch = "x86_64"),
531 feature = "kernel-bmi2"
532))]
533#[derive(Copy, Clone, Default)]
534pub(crate) struct Bmi2Kernel;
535
536#[cfg(all(
537 any(target_arch = "x86", target_arch = "x86_64"),
538 feature = "kernel-bmi2"
539))]
540impl CpuKernel for Bmi2Kernel {
541 #[inline(always)]
542 fn mask_lower_bits(value: u64, n: u8) -> u64 {
543 // SAFETY: this kernel ZST is only reachable via the
544 // `match detect_cpu_kernel() { CpuKernelTag::Bmi2 => ... }`
545 // dispatch arms at decoder entry sites, all of which fire only
546 // after `detect_cpu_kernel` confirmed BMI2 is available on the
547 // running CPU.
548 unsafe { mask_lower_bits_bmi2_impl(value, n) }
549 }
550
551 // Every BMI2 CPU has SSE2.
552 sse2_copies!();
553}
554
555/// x86 AVX2 + BMI2 kernel (x86-64-v3 baseline). The common modern
556/// x86 case — most CPUs released since 2013 (Haswell) have AVX2+BMI2.
557/// Uses `bzhi` for mask ops and 256-bit moves for the buffer copies. Present
558/// on 32-bit x86 too, where the kernel's masks are the table ones and its
559/// copies are what it adds; the tier still decodes literals on the BMI2 path,
560/// so, like every tier, it requires the ones below it.
561#[cfg(all(
562 any(target_arch = "x86", target_arch = "x86_64"),
563 feature = "kernel-avx2"
564))]
565#[derive(Copy, Clone, Default)]
566pub(crate) struct Avx2Kernel;
567
568#[cfg(all(
569 any(target_arch = "x86", target_arch = "x86_64"),
570 feature = "kernel-avx2"
571))]
572impl CpuKernel for Avx2Kernel {
573 #[inline(always)]
574 fn mask_lower_bits(value: u64, n: u8) -> u64 {
575 // SAFETY: Avx2Kernel is selected only after runtime detect
576 // confirmed both AVX2 and BMI2 — `_bzhi_u64` is callable.
577 #[cfg(target_arch = "x86_64")]
578 unsafe {
579 mask_lower_bits_bmi2_impl(value, n)
580 }
581 // 32-bit x86 has only a 32-bit `bzhi`: two of them and the branches
582 // choosing between them cost more than the table lookup in the
583 // sequence loop, which calls this three times a sequence (decoding
584 // z000033 on i686 measured 12.6% slower at level 1 with them).
585 #[cfg(target_arch = "x86")]
586 {
587 ScalarKernel::mask_lower_bits(value, n)
588 }
589 }
590
591 // 32 bytes, the width the buffers' trailing slack is sized for.
592 const COPY_CHUNK: usize = 32;
593
594 #[inline(always)]
595 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
596 // SAFETY: this kernel is selected only after detect confirmed AVX2.
597 unsafe { crate::decoding::simd_copy::copy_avx2(src, dst, len) }
598 }
599
600 const STEP_CHUNK: usize = 16;
601
602 #[inline(always)]
603 unsafe fn copy_step(src: *const u8, dst: *mut u8, len: usize) {
604 // SAFETY: an AVX2 CPU has SSE2.
605 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, len) }
606 }
607
608 #[inline(always)]
609 unsafe fn copy16(src: *const u8, dst: *mut u8) {
610 // SAFETY: as above.
611 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, 16) }
612 }
613}
614
615/// x86_64 AVX-512 VBMI2 + AVX2 + BMI2 kernel. Selected when the CPU
616/// has the AVX-512 VBMI2 family available — VBMI2 unlocks a faster
617/// HUF burst inner loop (VPSHUFB-based table lookup); BMI2 mask_lower
618/// bits stays identical to Avx2 kernel.
619#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
620#[derive(Copy, Clone, Default)]
621pub(crate) struct Vbmi2Kernel;
622
623#[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
624impl CpuKernel for Vbmi2Kernel {
625 #[inline(always)]
626 fn mask_lower_bits(value: u64, n: u8) -> u64 {
627 // SAFETY: same precondition as Avx2Kernel — BMI2 confirmed
628 // at runtime before this kernel is instantiated.
629 unsafe { mask_lower_bits_bmi2_impl(value, n) }
630 }
631
632 // The AVX2 width: a 64-byte store would need twice the buffers' trailing
633 // slack to fire on the short copies that dominate.
634 const COPY_CHUNK: usize = 32;
635
636 #[inline(always)]
637 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
638 // SAFETY: the VBMI2 tier is selected only with AVX2 confirmed.
639 unsafe { crate::decoding::simd_copy::copy_avx2(src, dst, len) }
640 }
641
642 const STEP_CHUNK: usize = 16;
643
644 #[inline(always)]
645 unsafe fn copy_step(src: *const u8, dst: *mut u8, len: usize) {
646 // SAFETY: an AVX2 CPU has SSE2.
647 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, len) }
648 }
649
650 #[inline(always)]
651 unsafe fn copy16(src: *const u8, dst: *mut u8) {
652 // SAFETY: as above.
653 unsafe { crate::decoding::simd_copy::copy_sse2(src, dst, 16) }
654 }
655}
656
657/// aarch64 NEON kernel: scalar masks and 16-byte NEON copies. Used on all
658/// aarch64 hardware that exposes NEON (effectively universal on the
659/// supported targets).
660#[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
661#[derive(Copy, Clone, Default)]
662pub(crate) struct NeonKernel;
663
664#[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
665impl CpuKernel for NeonKernel {
666 #[inline(always)]
667 fn mask_lower_bits(value: u64, n: u8) -> u64 {
668 // aarch64 has no BMI2 equivalent that improves on the scalar
669 // shift-and-mask sequence for this op; the codegen is
670 // identical to the Scalar kernel here.
671 ScalarKernel::mask_lower_bits(value, n)
672 }
673
674 const COPY_CHUNK: usize = BaselineKernel::COPY_CHUNK;
675
676 #[inline(always)]
677 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
678 // SAFETY: NEON is the aarch64 baseline.
679 unsafe { BaselineKernel::copy_chunks(src, dst, len) }
680 }
681
682 #[inline(always)]
683 unsafe fn copy16(src: *const u8, dst: *mut u8) {
684 // SAFETY: as above.
685 unsafe { BaselineKernel::copy16(src, dst) }
686 }
687}
688
689/// aarch64 SVE kernel. Variable-vector-length SVE extends NEON for
690/// HUF burst / SIMD copy on Graviton3 / Apple M-series with SVE
691/// support. Masks and copies as the NEON kernel's.
692#[cfg(all(target_arch = "aarch64", feature = "kernel-sve"))]
693#[allow(dead_code)]
694#[derive(Copy, Clone, Default)]
695pub(crate) struct SveKernel;
696
697#[cfg(all(target_arch = "aarch64", feature = "kernel-sve"))]
698impl CpuKernel for SveKernel {
699 #[inline(always)]
700 fn mask_lower_bits(value: u64, n: u8) -> u64 {
701 ScalarKernel::mask_lower_bits(value, n)
702 }
703
704 const COPY_CHUNK: usize = BaselineKernel::COPY_CHUNK;
705
706 #[inline(always)]
707 unsafe fn copy_chunks(src: *const u8, dst: *mut u8, len: usize) {
708 // SAFETY: NEON is the aarch64 baseline.
709 unsafe { BaselineKernel::copy_chunks(src, dst, len) }
710 }
711
712 #[inline(always)]
713 unsafe fn copy16(src: *const u8, dst: *mut u8) {
714 // SAFETY: as above.
715 unsafe { BaselineKernel::copy16(src, dst) }
716 }
717}
718
719/// Single `#[target_feature(enable = "bmi2")]` wrapper around the
720/// `_bzhi_u64` intrinsic. Lifted to a free function so each kernel
721/// impl that needs the BMI2 path (Bmi2 / Avx2 / Vbmi2) calls the
722/// same shared body. With `#[inline]` LLVM inlines the call into
723/// any caller that itself has BMI2 in scope; outside that scope the
724/// target_feature boundary is preserved.
725#[cfg(all(
726 any(target_arch = "x86", target_arch = "x86_64"),
727 feature = "kernel-bmi2"
728))]
729#[target_feature(enable = "bmi2")]
730#[inline]
731unsafe fn mask_lower_bits_bmi2_impl(value: u64, n: u8) -> u64 {
732 // The intrinsic call is permitted directly inside a function
733 // already annotated `#[target_feature(enable = "bmi2")]` — no
734 // `unsafe { ... }` block needed (the function-level `unsafe`
735 // already covers it). SAFETY: caller selected a kernel whose
736 // CpuKernelTag was resolved after `is_x86_feature_detected!("bmi2")`
737 // returned true, so the BMI2 instruction set is available.
738 #[cfg(target_arch = "x86_64")]
739 {
740 core::arch::x86_64::_bzhi_u64(value, n as u32)
741 }
742 // 32-bit x86 has `bzhi` on 32-bit registers only. Widths up to 32 take one
743 // instruction on the low half; wider ones keep the low 32 bits whole and
744 // apply it to the high half, which is what a 64-bit `bzhi` does in one go.
745 #[cfg(target_arch = "x86")]
746 {
747 use core::arch::x86::_bzhi_u32;
748 if n >= 64 {
749 return value;
750 }
751 if n <= 32 {
752 return u64::from(_bzhi_u32(value as u32, u32::from(n)));
753 }
754 let high = _bzhi_u32((value >> 32) as u32, u32::from(n) - 32);
755 (value & u64::from(u32::MAX)) | (u64::from(high) << 32)
756 }
757}
758
759/// Pure boolean-input variant of the x86 kernel-tag selection. Both the
760/// `std` runtime-detect path and the `no_std` compile-time-cfg path
761/// route through this helper so the precedence rules stay in one place
762/// (and are unit-testable without runtime CPUID).
763///
764/// The VBMI2 tier requires every AVX-512 sub-feature it touches AND the
765/// AVX2 baseline — VBMI2 kernels mix VBMI2-only intrinsics with AVX2
766/// 256-bit moves, so the dispatch must be conditioned on `has_avx2` too.
767/// Likewise the Avx2 tier requires both AVX2 and BMI2.
768#[cfg(target_arch = "x86_64")]
769#[inline(always)]
770// Params go unused when the matching `kernel_*` feature is disabled (the
771// rung that consumes them is `#[cfg]`-ed out); they are still passed by the
772// detect callers. Silence the conditional unused-variable warning rather
773// than thread per-feature `_`-prefixes through the signature.
774#[allow(unused_variables)]
775const fn select_x86_kernel(
776 has_avx512vbmi2: bool,
777 has_avx512f: bool,
778 has_avx512vl: bool,
779 has_avx512bw: bool,
780 has_bmi2: bool,
781 has_avx2: bool,
782 has_sse2: bool,
783) -> CpuKernelTag {
784 #[cfg(feature = "kernel-vbmi2")]
785 if has_avx512vbmi2 && has_avx512f && has_avx512vl && has_avx512bw && has_bmi2 && has_avx2 {
786 return CpuKernelTag::Vbmi2;
787 }
788 #[cfg(feature = "kernel-avx2")]
789 if has_avx2 && has_bmi2 {
790 return CpuKernelTag::Avx2;
791 }
792 #[cfg(feature = "kernel-bmi2")]
793 if has_bmi2 {
794 return CpuKernelTag::Bmi2;
795 }
796 #[cfg(feature = "kernel-sse")]
797 if has_sse2 {
798 return CpuKernelTag::Sse2;
799 }
800 CpuKernelTag::Scalar
801}
802
803/// Cached runtime-detected kernel tag. The actual `CpuKernel` impl
804/// (`ScalarKernel` / `Bmi2Kernel` / `Avx2Kernel` / `Vbmi2Kernel` /
805/// `NeonKernel` / `SveKernel`) is constructed at the dispatch site —
806/// currently only `decoding::literals_section_decoder::decompress_literals`
807/// — via a `match` on this tag that branches into the per-K
808/// `target_feature`-wrapped specialisation. Pipeline-wide dispatch
809/// (FrameDecoder / FrameCompressor entry, sequence executor, match
810/// copy) lands incrementally in follow-up tiers.
811#[derive(Copy, Clone, Debug, Eq, PartialEq)]
812pub(crate) enum CpuKernelTag {
813 Scalar,
814 /// Chosen at run time on 32-bit x86 too, where a build's baseline may
815 /// lack SSE2 while the CPU has it.
816 #[cfg(all(
817 any(target_arch = "x86", target_arch = "x86_64"),
818 feature = "kernel-sse"
819 ))]
820 Sse2,
821 /// Reachable on 32-bit x86 as well: `bzhi` is there, and without the tier
822 /// such a build would decode on the scalar bodies whatever the CPU offers.
823 #[cfg(all(
824 any(target_arch = "x86", target_arch = "x86_64"),
825 feature = "kernel-bmi2"
826 ))]
827 Bmi2,
828 /// On 32-bit x86 it runs the portable sequence walk with this kernel's
829 /// 32-byte copies.
830 #[cfg(all(
831 any(target_arch = "x86", target_arch = "x86_64"),
832 feature = "kernel-avx2"
833 ))]
834 Avx2,
835 #[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
836 Vbmi2,
837 #[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
838 Neon,
839 // Both constructors of `Sve` need a reachable feature: runtime
840 // detection via `std::arch::is_aarch64_feature_detected!` (so
841 // `feature = "std"`) or compile-time `target_feature = "sve"` in
842 // RUSTFLAGS. Without either, the variant is unreachable and a
843 // `match` arm referencing it warns as dead.
844 #[cfg(all(
845 target_arch = "aarch64",
846 feature = "kernel-sve",
847 any(feature = "std", target_feature = "sve"),
848 ))]
849 Sve,
850}
851
852/// Detect once and cache the best available CPU kernel for the
853/// current process. Subsequent calls return the cached tag without
854/// re-running CPU-feature detection. Std-only — no-std targets use
855/// the compile-time variant below that resolves at build time.
856#[cfg(feature = "std")]
857pub(crate) fn detect_cpu_kernel() -> CpuKernelTag {
858 static CACHED: OnceLock<CpuKernelTag> = OnceLock::new();
859 *CACHED.get_or_init(detect_cpu_kernel_uncached)
860}
861
862#[cfg(feature = "std")]
863fn detect_cpu_kernel_uncached() -> CpuKernelTag {
864 #[cfg(target_arch = "x86_64")]
865 {
866 use std::arch::is_x86_feature_detected;
867 // Gate each probe on its tier feature: `cfg!(...)` const-folds, so the
868 // `&&` short-circuits away the runtime `is_x86_feature_detected!` call
869 // (and its CPUID/cache traffic) for tiers the build disabled — the
870 // matching `select_x86_kernel` rung is `#[cfg]`-ed out anyway.
871 let avx512 = cfg!(feature = "kernel-vbmi2") && cpu_allows(CpuLevel::Avx512);
872 return select_x86_kernel(
873 avx512 && is_x86_feature_detected!("avx512vbmi2"),
874 avx512 && is_x86_feature_detected!("avx512f"),
875 avx512 && is_x86_feature_detected!("avx512vl"),
876 avx512 && is_x86_feature_detected!("avx512bw"),
877 cfg!(feature = "kernel-bmi2")
878 && cpu_allows(CpuLevel::Bmi2)
879 && is_x86_feature_detected!("bmi2"),
880 cfg!(feature = "kernel-avx2")
881 && cpu_allows(CpuLevel::Avx2)
882 && is_x86_feature_detected!("avx2"),
883 cfg!(feature = "kernel-sse")
884 && cpu_allows(CpuLevel::Sse2)
885 && is_x86_feature_detected!("sse2"),
886 );
887 }
888 // 32-bit x86 carries the SSE2, BMI2 and AVX2 tiers: SSE2 copies, `bzhi`
889 // for the entropy tables and the AVX2 buffer copies. The VBMI2 bodies are
890 // x86_64-only.
891 #[cfg(target_arch = "x86")]
892 {
893 #[cfg(feature = "kernel-sse")]
894 {
895 use std::arch::is_x86_feature_detected;
896 #[cfg(feature = "kernel-bmi2")]
897 {
898 let bmi2 = cpu_allows(CpuLevel::Bmi2) && is_x86_feature_detected!("bmi2");
899 // The AVX2 tier sits above BMI2, as on x86_64: its literals take
900 // the BMI2 body, so an AVX2 CPU without BMI2 (in practice a
901 // virtual machine that hides it) runs the SSE2 tier. Its copies
902 // stay 16 bytes wide; a tier of AVX2 copies without BMI2 would be
903 // one more sequence decoder for a CPU no vendor ships.
904 #[cfg(feature = "kernel-avx2")]
905 if bmi2 && cpu_allows(CpuLevel::Avx2) && is_x86_feature_detected!("avx2") {
906 return CpuKernelTag::Avx2;
907 }
908 if bmi2 {
909 return CpuKernelTag::Bmi2;
910 }
911 }
912 if cpu_allows(CpuLevel::Sse2) && is_x86_feature_detected!("sse2") {
913 return CpuKernelTag::Sse2;
914 }
915 }
916 return CpuKernelTag::Scalar;
917 }
918 #[cfg(target_arch = "aarch64")]
919 {
920 #[cfg(any(feature = "kernel-sve", feature = "kernel-neon"))]
921 use std::arch::is_aarch64_feature_detected;
922 #[cfg(feature = "kernel-sve")]
923 if cpu_allows(CpuLevel::Sve) && is_aarch64_feature_detected!("sve") {
924 return CpuKernelTag::Sve;
925 }
926 #[cfg(feature = "kernel-neon")]
927 if cpu_allows(CpuLevel::Neon) && is_aarch64_feature_detected!("neon") {
928 return CpuKernelTag::Neon;
929 }
930 return CpuKernelTag::Scalar;
931 }
932 #[allow(unreachable_code)]
933 CpuKernelTag::Scalar
934}
935
936/// no-std variant: rely on compile-time `target_feature` flags
937/// instead of runtime detection. Resolves to the most-capable kernel
938/// that the build target supports.
939#[cfg(not(feature = "std"))]
940pub(crate) fn detect_cpu_kernel() -> CpuKernelTag {
941 #[cfg(target_arch = "x86_64")]
942 {
943 // Route through the same const-fn precedence helper as the
944 // `feature = "std"` path. `cfg!(target_feature = ...)`
945 // returns a compile-time bool that constant-folds through
946 // `select_x86_kernel`, so the runtime call has the same
947 // codegen as the previous hand-written #[cfg] chain.
948 let avx512 = cpu_allows(CpuLevel::Avx512);
949 return select_x86_kernel(
950 avx512 && cfg!(target_feature = "avx512vbmi2"),
951 avx512 && cfg!(target_feature = "avx512f"),
952 avx512 && cfg!(target_feature = "avx512vl"),
953 avx512 && cfg!(target_feature = "avx512bw"),
954 cpu_allows(CpuLevel::Bmi2) && cfg!(target_feature = "bmi2"),
955 cpu_allows(CpuLevel::Avx2) && cfg!(target_feature = "avx2"),
956 cpu_allows(CpuLevel::Sse2) && cfg!(target_feature = "sse2"),
957 );
958 }
959 // `cfg!` rather than `#[cfg]`, as `select_x86_kernel` does: the tests fold
960 // at compile time, and the tiers stay constructed in every build.
961 #[cfg(all(target_arch = "x86", feature = "kernel-bmi2"))]
962 {
963 let bmi2 = cpu_allows(CpuLevel::Bmi2) && cfg!(target_feature = "bmi2");
964 #[cfg(feature = "kernel-avx2")]
965 if bmi2 && cpu_allows(CpuLevel::Avx2) && cfg!(target_feature = "avx2") {
966 return CpuKernelTag::Avx2;
967 }
968 if bmi2 {
969 return CpuKernelTag::Bmi2;
970 }
971 }
972 #[cfg(all(target_arch = "x86", feature = "kernel-sse"))]
973 if cpu_allows(CpuLevel::Sse2) && cfg!(target_feature = "sse2") {
974 return CpuKernelTag::Sse2;
975 }
976 #[cfg(target_arch = "aarch64")]
977 {
978 #[cfg(all(feature = "kernel-sve", target_feature = "sve"))]
979 if cpu_allows(CpuLevel::Sve) {
980 return CpuKernelTag::Sve;
981 }
982 #[cfg(all(feature = "kernel-neon", target_feature = "neon"))]
983 if cpu_allows(CpuLevel::Neon) {
984 return CpuKernelTag::Neon;
985 }
986 }
987 #[allow(unreachable_code)]
988 CpuKernelTag::Scalar
989}
990
991impl CpuKernelTag {
992 /// Stable lowercase diagnostic name for this tier (used by
993 /// [`active_cpu_kernel_name`] and the bench/dashboard reporting). Pure
994 /// mapping over the tag, so every arm is exercisable in tests regardless
995 /// of which tier the running CPU actually resolves to.
996 pub(crate) fn name(self) -> &'static str {
997 match self {
998 CpuKernelTag::Scalar => "scalar",
999 #[cfg(all(
1000 any(target_arch = "x86", target_arch = "x86_64"),
1001 feature = "kernel-sse"
1002 ))]
1003 CpuKernelTag::Sse2 => "sse2",
1004 #[cfg(all(
1005 any(target_arch = "x86", target_arch = "x86_64"),
1006 feature = "kernel-bmi2"
1007 ))]
1008 CpuKernelTag::Bmi2 => "bmi2",
1009 #[cfg(all(
1010 any(target_arch = "x86", target_arch = "x86_64"),
1011 feature = "kernel-avx2"
1012 ))]
1013 CpuKernelTag::Avx2 => "avx2",
1014 #[cfg(all(target_arch = "x86_64", feature = "kernel-vbmi2"))]
1015 CpuKernelTag::Vbmi2 => "vbmi2",
1016 #[cfg(all(target_arch = "aarch64", feature = "kernel-neon"))]
1017 CpuKernelTag::Neon => "neon",
1018 #[cfg(all(
1019 target_arch = "aarch64",
1020 feature = "kernel-sve",
1021 any(feature = "std", target_feature = "sve"),
1022 ))]
1023 CpuKernelTag::Sve => "sve",
1024 }
1025 }
1026}
1027
1028/// Name of the CPU kernel tier this process selected for the entropy /
1029/// sequence hot paths: decode (literals + FSE sequence decode) and encode
1030/// (entropy) share this dispatch (see #247). Returned as a stable lowercase
1031/// string for diagnostics and benchmark/dashboard reporting; the value is
1032/// what the runtime CPU-feature detection (or compile-time `target_feature`
1033/// on `no_std`) actually resolves to on this machine, so a dashboard can
1034/// attribute a measurement to the kernel that produced it.
1035pub fn active_cpu_kernel_name() -> &'static str {
1036 detect_cpu_kernel().name()
1037}
1038
1039#[cfg(test)]
1040mod tests;