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 ¶ms,
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}