Skip to main content

argon2_rust/fill_block/
mod.rs

1//! Backend selection: one `fill_segment` implementation per instruction set,
2//! chosen by **runtime** CPU feature detection.
3//!
4//! # Cost model
5//!
6//! * Detection runs at most once per process. The result is cached in a
7//!   [`AtomicU8`] read with [`Ordering::Relaxed`]. The initialisation race is
8//!   benign: every thread computes the same answer, so a duplicated `detect()`
9//!   only wastes a `cpuid`.
10//! * A hash call resolves the function pointer **once**, before entering the
11//!   pass/slice/lane loops (see `core::fill_memory_blocks`). Nothing detects or
12//!   dispatches inside the per-block loop.
13//! * Per hash: one relaxed load plus one compare. Per segment: one indirect
14//!   call. A segment is `segment_length` blocks — thousands at any realistic
15//!   `m_cost` — so the per-block overhead is nil.
16//!
17//! # Why `#[target_feature]` sits on `fill_segment`
18//!
19//! LLVM will not inline a callee with a higher target-feature set into a caller
20//! with a lower one. Putting the attribute on the whole `fill_segment` lets
21//! `fill_block` inline into it, which is what preserves the `src/opt.c`
22//! optimisation of keeping the 1 KiB `state` in registers across loop
23//! iterations instead of reloading it from memory for every block. Do **not**
24//! move the attribute down onto `fill_block` or onto individual intrinsics.
25//!
26//! # `no_std`
27//!
28//! Runtime detection needs `std`. Without the `std` feature, [`detect`] falls
29//! back to compile-time `cfg(target_feature = ...)` and then to
30//! [`Backend::Scalar`].
31//!
32//! # Testing under Rosetta on aarch64-apple-darwin
33//!
34//! `is_x86_feature_detected!` expands to
35//! `cfg!(target_feature = "...") || runtime_cpuid_check()`, so a compile-time
36//! `target_feature` short-circuits it to `true`. That matters here because
37//! Rosetta 2 (measured on macOS 26.5.2 / Apple M5 Max) *executes* AVX2 but does
38//! not advertise it in `cpuid`:
39//!
40//! * `cargo test --target x86_64-apple-darwin` — [`detect`] returns
41//!   [`Backend::Sse2`] and `Backend::Avx2.is_available()` is `false`, so AVX2
42//!   gets skipped.
43//! * `RUSTFLAGS="-C target-feature=+avx2" cargo test --target x86_64-apple-darwin`
44//!   — [`detect`] returns [`Backend::Avx2`] and it really runs.
45//! * Never add `+avx512f`: `is_available()` would then report `true` while the
46//!   instruction itself traps with `SIGILL`.
47
48use core::sync::atomic::{AtomicU8, Ordering};
49
50use crate::block::{Instance, Position};
51
52pub mod scalar;
53
54#[cfg(target_arch = "aarch64")]
55pub mod neon;
56
57#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
58pub mod sse2;
59
60#[cfg(target_arch = "x86_64")]
61pub mod avx2;
62
63#[cfg(target_arch = "x86_64")]
64pub mod avx512;
65
66// WebAssembly has no runtime feature detection a module can survive (SIMD
67// instructions fail validation on engines that lack them), so the module
68// exists exactly when the engine contract was given at compile time.
69#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
70pub mod wasm128;
71
72// ---------------------------------------------------------------------------
73// Backend
74// ---------------------------------------------------------------------------
75
76/// Which `fill_segment` implementation to use.
77///
78/// A variant is added every time another ISA is ported, so this is
79/// `#[non_exhaustive]` — write a `_` arm downstream. [`Backend::ALL`] is a
80/// slice for the same reason: its length must not be part of the API.
81#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
82#[repr(u8)]
83#[non_exhaustive]
84pub enum Backend {
85    /// Portable scalar code. Always available.
86    Scalar = 0,
87    /// AArch64 NEON.
88    Neon = 1,
89    /// x86 / x86-64 SSE2.
90    Sse2 = 2,
91    /// x86-64 AVX2.
92    Avx2 = 3,
93    /// x86-64 AVX-512F.
94    Avx512 = 4,
95    /// wasm32 fixed-width SIMD128. Compile-time selected; see `wasm128`.
96    Wasm128 = 5,
97}
98
99/// The signature every backend's `fill_segment` has.
100///
101/// # Safety
102///
103/// Calling one of these requires:
104///
105/// * the CPU to support the backend's instruction set — check
106///   [`Backend::is_available`], or get the pointer from [`backend`];
107/// * `instance`'s arena pointer to be valid for `instance.memory_len()` blocks;
108/// * `position` to be in range, and no other thread to be writing the segment
109///   `(position.lane, position.slice)` at the same time.
110pub type FillSegmentFn = unsafe fn(&Instance, Position);
111
112impl Backend {
113    /// Every backend, in ascending preference order.
114    pub const ALL: &'static [Backend] = &[
115        Backend::Scalar,
116        Backend::Neon,
117        Backend::Sse2,
118        Backend::Avx2,
119        Backend::Avx512,
120        Backend::Wasm128,
121    ];
122
123    /// Short lowercase name, handy for bench ids and test output.
124    #[inline]
125    #[must_use]
126    pub const fn name(self) -> &'static str {
127        match self {
128            Backend::Scalar => "scalar",
129            Backend::Neon => "neon",
130            Backend::Sse2 => "sse2",
131            Backend::Avx2 => "avx2",
132            Backend::Avx512 => "avx512",
133            Backend::Wasm128 => "wasm128",
134        }
135    }
136
137    /// Whether this CPU can execute this backend *right now*.
138    ///
139    /// Unlike `detect`, this asks about one specific backend, so tests and
140    /// benches can loop over [`Backend::ALL`] and skip what the host cannot run.
141    #[inline]
142    #[must_use]
143    pub fn is_available(self) -> bool {
144        match self {
145            Backend::Scalar => true,
146            Backend::Neon => have_neon(),
147            Backend::Sse2 => have_sse2(),
148            Backend::Avx2 => have_avx2(),
149            Backend::Avx512 => have_avx512f(),
150            Backend::Wasm128 => have_wasm_simd128(),
151        }
152    }
153
154    #[inline]
155    const fn to_u8(self) -> u8 {
156        self as u8
157    }
158
159    /// Total inverse of [`Backend::to_u8`]; anything unknown maps to
160    /// [`Backend::Scalar`] so the cache can never produce a panic.
161    #[inline]
162    const fn from_u8(value: u8) -> Backend {
163        match value {
164            1 => Backend::Neon,
165            2 => Backend::Sse2,
166            3 => Backend::Avx2,
167            4 => Backend::Avx512,
168            5 => Backend::Wasm128,
169            _ => Backend::Scalar,
170        }
171    }
172}
173
174impl core::fmt::Display for Backend {
175    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176        f.write_str(self.name())
177    }
178}
179
180// ---------------------------------------------------------------------------
181// Per-feature probes
182// ---------------------------------------------------------------------------
183//
184// Each probe is defined twice under mutually exclusive `cfg`s, so exactly one
185// definition ever exists and there is no dead or unreachable code. With `std`
186// the probe is a real runtime check; without it, it degrades to the
187// compile-time `target_feature` cfg.
188
189#[cfg(all(feature = "std", target_arch = "x86_64"))]
190#[inline]
191fn have_avx512f() -> bool {
192    std::arch::is_x86_feature_detected!("avx512f")
193}
194#[cfg(not(all(feature = "std", target_arch = "x86_64")))]
195#[inline]
196fn have_avx512f() -> bool {
197    cfg!(all(target_arch = "x86_64", target_feature = "avx512f"))
198}
199
200#[cfg(all(feature = "std", target_arch = "x86_64"))]
201#[inline]
202fn have_avx2() -> bool {
203    std::arch::is_x86_feature_detected!("avx2")
204}
205#[cfg(not(all(feature = "std", target_arch = "x86_64")))]
206#[inline]
207fn have_avx2() -> bool {
208    cfg!(all(target_arch = "x86_64", target_feature = "avx2"))
209}
210
211#[cfg(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64")))]
212#[inline]
213fn have_sse2() -> bool {
214    std::arch::is_x86_feature_detected!("sse2")
215}
216#[cfg(not(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64"))))]
217#[inline]
218fn have_sse2() -> bool {
219    cfg!(all(
220        any(target_arch = "x86", target_arch = "x86_64"),
221        target_feature = "sse2"
222    ))
223}
224
225#[cfg(all(feature = "std", target_arch = "aarch64"))]
226#[inline]
227fn have_neon() -> bool {
228    // NEON (Advanced SIMD) is in the architectural baseline Apple and
229    // Windows guarantee for aarch64; probing the OS for it would be a
230    // formality, so on those platforms the answer is compile-time.
231    #[cfg(any(target_vendor = "apple", target_os = "windows"))]
232    {
233        true
234    }
235    #[cfg(not(any(target_vendor = "apple", target_os = "windows")))]
236    {
237        std::arch::is_aarch64_feature_detected!("neon")
238    }
239}
240
241/// wasm32 SIMD128. There is no runtime probe a wasm module can survive
242/// (SIMD instructions fail validation where unsupported), so the answer is
243/// purely compile-time: it is `true` exactly when the crate was built with
244/// `-C target-feature=+simd128`.
245#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
246#[inline]
247fn have_wasm_simd128() -> bool {
248    true
249}
250#[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
251#[inline]
252fn have_wasm_simd128() -> bool {
253    false
254}
255#[cfg(not(all(feature = "std", target_arch = "aarch64")))]
256#[inline]
257fn have_neon() -> bool {
258    cfg!(all(target_arch = "aarch64", target_feature = "neon"))
259}
260
261// ---------------------------------------------------------------------------
262// AArch64: does NEON actually win here?
263// ---------------------------------------------------------------------------
264//
265// "Has NEON" and "NEON is the fastest fill" are different questions on
266// AArch64 — but only on platforms whose microarchitecture is unknown. On
267// Apple Silicon the NEON schedule beats scalar by x1.5-x2 (measured), and
268// Windows aarch64 is likewise a known, NEON-strong target (Snapdragon); on
269// both, NEON is simply the answer and there is nothing to detect. On
270// Neoverse N1 (Ampere Altra; GitHub's `ubuntu-24.04-arm` runners) the same
271// schedule is *slower* than scalar — 467 vs 331 ns/block, measured — and
272// picking it there loses to the C reference, which is scalar-only on
273// AArch64. So the shootout exists for the platforms that need it: aarch64
274// outside Apple and Windows, with `std`, in release.
275
276/// One shootout: both backends fill the same small instance, interleaved.
277///
278/// The instance is 1 MiB single-lane — L2-resident on anything this runs on,
279/// so the measurement is the compute schedule rather than DRAM, which is
280/// also the regime where the N1 regression shows. Cost is ~4 ms once per
281/// process: one pass over all four slices per rep, best of six reps per
282/// side, and the whole thing sits inside [`detect_and_cache`].
283///
284/// Release builds only: unoptimised NEON intrinsics lose to unoptimised
285/// scalar everywhere (per-intrinsic call overhead), so a debug shootout
286/// would measure codegen mode, not microarchitecture.
287#[cfg(all(
288    feature = "std",
289    target_arch = "aarch64",
290    not(miri),
291    not(debug_assertions),
292    not(any(target_vendor = "apple", target_os = "windows"))
293))]
294fn neon_wins_here() -> bool {
295    use crate::block::{Instance, Position};
296    use crate::params::{Algorithm, Memory, Params, TagLen, Version};
297    use std::time::Instant;
298
299    const M_COST: u32 = 1024;
300    const REPS: usize = 6;
301
302    let params = match Params::builder()
303        .memory(Memory::kib(u64::from(M_COST)))
304        .passes(1)
305        .lanes(1)
306        .tag_len(TagLen::bytes(32))
307        .build()
308    {
309        Ok(params) => params,
310        Err(_) => return true, // unreachable at these constants; keep NEON
311    };
312    let blocks = params.memory_layout().0 as usize;
313    let mut arena = match crate::memory::Arena::new(blocks) {
314        Ok(arena) => arena,
315        Err(_) => return true, // 1 MiB; if even that fails, keep NEON
316    };
317    // Non-zero, non-constant seed, so no implementation detail can shortcut.
318    for (i, block) in arena.as_mut_slice().iter_mut().enumerate() {
319        for (j, w) in block.0.iter_mut().enumerate() {
320            *w = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul((i * 128 + j) as u64 + 1);
321        }
322    }
323
324    let mut one_pass = |backend: Backend| -> u128 {
325        let fill = fill_segment_fn(backend);
326        // SAFETY: `arena` is a live allocation of exactly `blocks` `Block`s,
327        // no reference into it is live while the raw pointer is, and the
328        // instance is one lane on one thread, so every position below is in
329        // range with no concurrent access. `fill` is one of scalar/neon, both
330        // executable on any aarch64 CPU.
331        let instance = unsafe {
332            Instance::new(
333                arena.as_mut_ptr(),
334                blocks,
335                Algorithm::Argon2id,
336                Version::V0x13,
337                &params,
338            )
339        };
340        let t0 = Instant::now();
341        for slice in 0..crate::params::SYNC_POINTS {
342            // SAFETY: as above; single lane, in-range slice.
343            unsafe { fill(&instance, Position::new(0, 0, slice, 0)) };
344        }
345        t0.elapsed().as_nanos()
346    };
347
348    // Finely interleaved pairs, min per side. This runs inside `detect()`,
349    // which can be called from a `cargo test` process running suites on
350    // parallel threads: a coarse A/B/A structure lets one contention window
351    // land on every rep of one side, while per-rep interleaving shares each
352    // window between both.
353    let mut scalar_best = u128::MAX;
354    let mut neon_best = u128::MAX;
355    for _ in 0..REPS {
356        scalar_best = scalar_best.min(one_pass(Backend::Scalar));
357        neon_best = neon_best.min(one_pass(Backend::Neon));
358    }
359    core::hint::black_box(arena.as_ptr());
360    neon_best < scalar_best
361}
362
363/// Everywhere the shootout does not run, the answer is NEON: on Apple and
364/// Windows aarch64 because NEON is baseline and measured fastest there; on
365/// `no_std` because there is no clock (and those targets are not the server
366/// parts the regression lives on); under Miri and in debug builds because a
367/// wall-clock measurement means nothing there.
368#[cfg(not(all(
369    feature = "std",
370    target_arch = "aarch64",
371    not(miri),
372    not(debug_assertions),
373    not(any(target_vendor = "apple", target_os = "windows"))
374)))]
375#[inline]
376fn neon_wins_here() -> bool {
377    true
378}
379
380// ---------------------------------------------------------------------------
381// Detection and caching
382// ---------------------------------------------------------------------------
383
384/// Sentinel meaning "detection has not run yet". Not a valid [`Backend`] value.
385const UNINIT: u8 = 0xFF;
386
387/// Cached [`Backend`] as a `u8`, or [`UNINIT`].
388static CACHED_BACKEND: AtomicU8 = AtomicU8::new(UNINIT);
389
390/// Run the feature cascade and return the best backend for this CPU.
391///
392/// Preference order: `Avx512 > Avx2 > Sse2` on x86-64, `Sse2` on x86, `Neon` on
393/// AArch64, `Scalar` everywhere else. The probes are arch-gated, so the single
394/// cascade below cannot pick an off-arch backend.
395///
396/// This always re-runs detection; use [`backend`] for the cached value.
397#[must_use]
398pub fn detect() -> Backend {
399    if cfg!(miri) {
400        // Miri interprets the crate: its intrinsic support stops around
401        // SSE2, and a wall-clock shootout means nothing under an
402        // interpreter. Scalar is the one backend that behaves identically
403        // on every host Miri runs on, which is also what makes the Miri CI
404        // job arch-independent.
405        Backend::Scalar
406    } else if have_avx512f() {
407        Backend::Avx512
408    } else if have_avx2() {
409        Backend::Avx2
410    } else if have_sse2() {
411        Backend::Sse2
412    } else if have_neon() && neon_wins_here() {
413        Backend::Neon
414    } else if have_wasm_simd128() {
415        Backend::Wasm128
416    } else {
417        Backend::Scalar
418    }
419}
420
421/// Detect and populate the cache. Outlined so [`backend`] stays tiny.
422#[cold]
423#[inline(never)]
424fn detect_and_cache() -> Backend {
425    let detected = detect();
426    // Relaxed is enough: the value is a plain `u8` with no associated data, and
427    // every thread that races here computes the same answer.
428    CACHED_BACKEND.store(detected.to_u8(), Ordering::Relaxed);
429    detected
430}
431
432/// The backend for this process: one relaxed atomic load on the hot path.
433///
434/// Deliberately not a `OnceLock` — acquire ordering would buy nothing here
435/// (there is no data to publish) and `OnceLock` needs `std`.
436#[inline]
437#[must_use]
438pub fn backend() -> Backend {
439    let cached = CACHED_BACKEND.load(Ordering::Relaxed);
440    if cached == UNINIT {
441        detect_and_cache()
442    } else {
443        Backend::from_u8(cached)
444    }
445}
446
447/// The `fill_segment` implementation for `backend`.
448///
449/// Resolve this **once per hash call**, outside every loop.
450///
451/// On an architecture that has no module for the requested backend, this
452/// returns the scalar implementation rather than failing to compile, so tests
453/// can iterate over [`Backend::ALL`] on any host. It does **not** check
454/// availability: a pointer for a backend this CPU lacks will fault when called.
455/// Use [`Backend::is_available`] first, or take the value from [`backend`].
456#[must_use]
457pub fn fill_segment_fn(backend: Backend) -> FillSegmentFn {
458    match backend {
459        Backend::Scalar => scalar::fill_segment,
460
461        #[cfg(target_arch = "aarch64")]
462        Backend::Neon => neon::fill_segment,
463        #[cfg(not(target_arch = "aarch64"))]
464        Backend::Neon => scalar::fill_segment,
465
466        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
467        Backend::Sse2 => sse2::fill_segment,
468        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
469        Backend::Sse2 => scalar::fill_segment,
470
471        #[cfg(target_arch = "x86_64")]
472        Backend::Avx2 => avx2::fill_segment,
473        #[cfg(not(target_arch = "x86_64"))]
474        Backend::Avx2 => scalar::fill_segment,
475
476        #[cfg(target_arch = "x86_64")]
477        Backend::Avx512 => avx512::fill_segment,
478        #[cfg(not(target_arch = "x86_64"))]
479        Backend::Avx512 => scalar::fill_segment,
480
481        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
482        Backend::Wasm128 => wasm128::fill_segment,
483        #[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
484        Backend::Wasm128 => scalar::fill_segment,
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn backend_u8_round_trip() {
494        for &b in Backend::ALL {
495            assert_eq!(Backend::from_u8(b.to_u8()), b);
496        }
497        // The sentinel and anything unknown must degrade, never panic.
498        assert_eq!(Backend::from_u8(UNINIT), Backend::Scalar);
499        assert_eq!(Backend::from_u8(200), Backend::Scalar);
500    }
501
502    #[test]
503    fn cache_agrees_with_detect() {
504        let first = backend();
505        assert_eq!(first, detect());
506        // Second call takes the cached path.
507        assert_eq!(backend(), first);
508        assert_ne!(CACHED_BACKEND.load(Ordering::Relaxed), UNINIT);
509    }
510
511    #[test]
512    fn detected_backend_is_available() {
513        assert!(detect().is_available());
514        assert!(Backend::Scalar.is_available());
515    }
516
517    #[test]
518    fn detection_respects_the_architecture() {
519        if cfg!(target_arch = "aarch64") {
520            assert!(!have_sse2());
521            assert!(!have_avx2());
522            assert!(!have_avx512f());
523            if cfg!(target_vendor = "apple") {
524                // The shootout is measured to pick NEON on Apple Silicon.
525                assert_eq!(detect(), Backend::Neon);
526            } else {
527                // Everywhere else the shootout decides: Neoverse N1 gets
528                // Scalar, Apple-class cores get NEON.
529                assert!(matches!(detect(), Backend::Neon | Backend::Scalar));
530            }
531        }
532        if cfg!(target_arch = "x86_64") {
533            // SSE2 is baseline on x86-64.
534            assert!(have_sse2());
535            assert!(!have_neon());
536            assert!(matches!(
537                detect(),
538                Backend::Sse2 | Backend::Avx2 | Backend::Avx512
539            ));
540        }
541        if cfg!(target_arch = "wasm32") {
542            assert!(!have_sse2());
543            assert!(!have_avx2());
544            assert!(!have_avx512f());
545            assert!(!have_neon());
546            if cfg!(target_feature = "simd128") {
547                assert_eq!(detect(), Backend::Wasm128);
548            } else {
549                assert_eq!(detect(), Backend::Scalar);
550            }
551        }
552    }
553
554    #[test]
555    fn every_backend_resolves_to_a_function() {
556        for &b in Backend::ALL {
557            let f = fill_segment_fn(b);
558            // Compare as raw addresses; only Scalar is guaranteed to be itself.
559            let scalar = fill_segment_fn(Backend::Scalar);
560            if b == Backend::Scalar {
561                assert!(core::ptr::fn_addr_eq(f, scalar));
562            }
563        }
564    }
565}