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, Params, Version};
297 use std::time::Instant;
298
299 const M_COST: u32 = 1024;
300 const REPS: usize = 6;
301
302 let params = match Params::new(M_COST, 1, 1, 32) {
303 Ok(params) => params,
304 Err(_) => return true, // unreachable at these constants; keep NEON
305 };
306 let blocks = params.memory_layout().0 as usize;
307 let mut arena = match crate::memory::Arena::new(blocks) {
308 Ok(arena) => arena,
309 Err(_) => return true, // 1 MiB; if even that fails, keep NEON
310 };
311 // Non-zero, non-constant seed, so no implementation detail can shortcut.
312 for (i, block) in arena.as_mut_slice().iter_mut().enumerate() {
313 for (j, w) in block.0.iter_mut().enumerate() {
314 *w = 0x9E37_79B9_7F4A_7C15u64.wrapping_mul((i * 128 + j) as u64 + 1);
315 }
316 }
317
318 let mut one_pass = |backend: Backend| -> u128 {
319 let fill = fill_segment_fn(backend);
320 // SAFETY: `arena` is a live allocation of exactly `blocks` `Block`s,
321 // no reference into it is live while the raw pointer is, and the
322 // instance is one lane on one thread, so every position below is in
323 // range with no concurrent access. `fill` is one of scalar/neon, both
324 // executable on any aarch64 CPU.
325 let instance = unsafe {
326 Instance::new(
327 arena.as_mut_ptr(),
328 blocks,
329 Algorithm::Argon2id,
330 Version::V0x13,
331 ¶ms,
332 )
333 };
334 let t0 = Instant::now();
335 for slice in 0..crate::params::SYNC_POINTS {
336 // SAFETY: as above; single lane, in-range slice.
337 unsafe { fill(&instance, Position::new(0, 0, slice, 0)) };
338 }
339 t0.elapsed().as_nanos()
340 };
341
342 // Finely interleaved pairs, min per side. This runs inside `detect()`,
343 // which can be called from a `cargo test` process running suites on
344 // parallel threads: a coarse A/B/A structure lets one contention window
345 // land on every rep of one side, while per-rep interleaving shares each
346 // window between both.
347 let mut scalar_best = u128::MAX;
348 let mut neon_best = u128::MAX;
349 for _ in 0..REPS {
350 scalar_best = scalar_best.min(one_pass(Backend::Scalar));
351 neon_best = neon_best.min(one_pass(Backend::Neon));
352 }
353 core::hint::black_box(arena.as_ptr());
354 neon_best < scalar_best
355}
356
357/// Everywhere the shootout does not run, the answer is NEON: on Apple and
358/// Windows aarch64 because NEON is baseline and measured fastest there; on
359/// `no_std` because there is no clock (and those targets are not the server
360/// parts the regression lives on); under Miri and in debug builds because a
361/// wall-clock measurement means nothing there.
362#[cfg(not(all(
363 feature = "std",
364 target_arch = "aarch64",
365 not(miri),
366 not(debug_assertions),
367 not(any(target_vendor = "apple", target_os = "windows"))
368)))]
369#[inline]
370fn neon_wins_here() -> bool {
371 true
372}
373
374// ---------------------------------------------------------------------------
375// Detection and caching
376// ---------------------------------------------------------------------------
377
378/// Sentinel meaning "detection has not run yet". Not a valid [`Backend`] value.
379const UNINIT: u8 = 0xFF;
380
381/// Cached [`Backend`] as a `u8`, or [`UNINIT`].
382static CACHED_BACKEND: AtomicU8 = AtomicU8::new(UNINIT);
383
384/// Run the feature cascade and return the best backend for this CPU.
385///
386/// Preference order: `Avx512 > Avx2 > Sse2` on x86-64, `Sse2` on x86, `Neon` on
387/// AArch64, `Scalar` everywhere else. The probes are arch-gated, so the single
388/// cascade below cannot pick an off-arch backend.
389///
390/// This always re-runs detection; use [`backend`] for the cached value.
391#[must_use]
392pub fn detect() -> Backend {
393 if cfg!(miri) {
394 // Miri interprets the crate: its intrinsic support stops around
395 // SSE2, and a wall-clock shootout means nothing under an
396 // interpreter. Scalar is the one backend that behaves identically
397 // on every host Miri runs on, which is also what makes the Miri CI
398 // job arch-independent.
399 Backend::Scalar
400 } else if have_avx512f() {
401 Backend::Avx512
402 } else if have_avx2() {
403 Backend::Avx2
404 } else if have_sse2() {
405 Backend::Sse2
406 } else if have_neon() && neon_wins_here() {
407 Backend::Neon
408 } else if have_wasm_simd128() {
409 Backend::Wasm128
410 } else {
411 Backend::Scalar
412 }
413}
414
415/// Detect and populate the cache. Outlined so [`backend`] stays tiny.
416#[cold]
417#[inline(never)]
418fn detect_and_cache() -> Backend {
419 let detected = detect();
420 // Relaxed is enough: the value is a plain `u8` with no associated data, and
421 // every thread that races here computes the same answer.
422 CACHED_BACKEND.store(detected.to_u8(), Ordering::Relaxed);
423 detected
424}
425
426/// The backend for this process: one relaxed atomic load on the hot path.
427///
428/// Deliberately not a `OnceLock` — acquire ordering would buy nothing here
429/// (there is no data to publish) and `OnceLock` needs `std`.
430#[inline]
431#[must_use]
432pub fn backend() -> Backend {
433 let cached = CACHED_BACKEND.load(Ordering::Relaxed);
434 if cached == UNINIT {
435 detect_and_cache()
436 } else {
437 Backend::from_u8(cached)
438 }
439}
440
441/// The `fill_segment` implementation for `backend`.
442///
443/// Resolve this **once per hash call**, outside every loop.
444///
445/// On an architecture that has no module for the requested backend, this
446/// returns the scalar implementation rather than failing to compile, so tests
447/// can iterate over [`Backend::ALL`] on any host. It does **not** check
448/// availability: a pointer for a backend this CPU lacks will fault when called.
449/// Use [`Backend::is_available`] first, or take the value from [`backend`].
450#[must_use]
451pub fn fill_segment_fn(backend: Backend) -> FillSegmentFn {
452 match backend {
453 Backend::Scalar => scalar::fill_segment,
454
455 #[cfg(target_arch = "aarch64")]
456 Backend::Neon => neon::fill_segment,
457 #[cfg(not(target_arch = "aarch64"))]
458 Backend::Neon => scalar::fill_segment,
459
460 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
461 Backend::Sse2 => sse2::fill_segment,
462 #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
463 Backend::Sse2 => scalar::fill_segment,
464
465 #[cfg(target_arch = "x86_64")]
466 Backend::Avx2 => avx2::fill_segment,
467 #[cfg(not(target_arch = "x86_64"))]
468 Backend::Avx2 => scalar::fill_segment,
469
470 #[cfg(target_arch = "x86_64")]
471 Backend::Avx512 => avx512::fill_segment,
472 #[cfg(not(target_arch = "x86_64"))]
473 Backend::Avx512 => scalar::fill_segment,
474
475 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
476 Backend::Wasm128 => wasm128::fill_segment,
477 #[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
478 Backend::Wasm128 => scalar::fill_segment,
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn backend_u8_round_trip() {
488 for &b in Backend::ALL {
489 assert_eq!(Backend::from_u8(b.to_u8()), b);
490 }
491 // The sentinel and anything unknown must degrade, never panic.
492 assert_eq!(Backend::from_u8(UNINIT), Backend::Scalar);
493 assert_eq!(Backend::from_u8(200), Backend::Scalar);
494 }
495
496 #[test]
497 fn cache_agrees_with_detect() {
498 let first = backend();
499 assert_eq!(first, detect());
500 // Second call takes the cached path.
501 assert_eq!(backend(), first);
502 assert_ne!(CACHED_BACKEND.load(Ordering::Relaxed), UNINIT);
503 }
504
505 #[test]
506 fn detected_backend_is_available() {
507 assert!(detect().is_available());
508 assert!(Backend::Scalar.is_available());
509 }
510
511 #[test]
512 fn detection_respects_the_architecture() {
513 if cfg!(target_arch = "aarch64") {
514 assert!(!have_sse2());
515 assert!(!have_avx2());
516 assert!(!have_avx512f());
517 if cfg!(target_vendor = "apple") {
518 // The shootout is measured to pick NEON on Apple Silicon.
519 assert_eq!(detect(), Backend::Neon);
520 } else {
521 // Everywhere else the shootout decides: Neoverse N1 gets
522 // Scalar, Apple-class cores get NEON.
523 assert!(matches!(detect(), Backend::Neon | Backend::Scalar));
524 }
525 }
526 if cfg!(target_arch = "x86_64") {
527 // SSE2 is baseline on x86-64.
528 assert!(have_sse2());
529 assert!(!have_neon());
530 assert!(matches!(
531 detect(),
532 Backend::Sse2 | Backend::Avx2 | Backend::Avx512
533 ));
534 }
535 if cfg!(target_arch = "wasm32") {
536 assert!(!have_sse2());
537 assert!(!have_avx2());
538 assert!(!have_avx512f());
539 assert!(!have_neon());
540 if cfg!(target_feature = "simd128") {
541 assert_eq!(detect(), Backend::Wasm128);
542 } else {
543 assert_eq!(detect(), Backend::Scalar);
544 }
545 }
546 }
547
548 #[test]
549 fn every_backend_resolves_to_a_function() {
550 for &b in Backend::ALL {
551 let f = fill_segment_fn(b);
552 // Compare as raw addresses; only Scalar is guaranteed to be itself.
553 let scalar = fill_segment_fn(Backend::Scalar);
554 if b == Backend::Scalar {
555 assert!(core::ptr::fn_addr_eq(f, scalar));
556 }
557 }
558 }
559}