diff --git a/CHANGELOG.md b/CHANGELOG.md index af9fcfc8..529644dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ You can find its changes [documented below](#060-2026-07-10). - The `new_unchecked()` function on SIMD level tokens such as `Avx2` has been renamed to `assume_supported()` and is now safe to call from contexts that already contain the appropriate `#[target_feature]` annotations. Functions without such annotations can still call `assume_supported()` with an `unsafe` block. ([#293][] by [@Shnatsel][]) - On x86_64 targets with static SSE2 support, `Level::baseline()` now returns `Sse2` instead of `Fallback`. ([#270][] by [@Shnatsel][]) +- The scalar `Fallback` backend and `Level::Fallback` variant are no longer compiled when the target has a better ambient SIMD baseline (e.g. SSE2 on x86, NEON on Aarch64). The `force_support_fallback` feature continues to make them available for testing. `disable_dispatch_sse2` no longer disables SSE2 if it is the baseline level. - The `fxsr` CPU feature is now required for all x86 SIMD levels. It is present in hardware on all SIMD-capable CPUs, but it is possible to disable it in some emulators combined with a custom Rust target specification. ([#270][] by [@Shnatsel][]) - All native-width non-mask vector types now share `u8s` as their byte representation, enabling `Bytes::bitcast` between arbitrary lane types in code generic over `Simd`. The byte representation of any `SimdBase` type is now also guaranteed to be an idempotent, same-token `u8` SIMD vector, so it can be manipulated directly in generic code. - `SimdBase::Mask` now guarantees support for selecting vectors of its associated `SimdBase` type, enabling mask selection in generic code without additional bounds. diff --git a/fearless_simd/README.md b/fearless_simd/README.md index 8ac91324..95a0834a 100644 --- a/fearless_simd/README.md +++ b/fearless_simd/README.md @@ -199,6 +199,8 @@ As a last resort, you can turn off multiversioning for specific SIMD instruction These configuration flags only control automatic multiversioning. Disabling one does not remove its token type, its [`Simd`] implementation, or explicit [`kernel`] support; for example, an `Avx2` token can still be used to call an AVX2 kernel when the CPU supports it. +`disable_dispatch_sse2` has no effect when SSE2 is part of the ambient target baseline, because +that baseline remains the terminal dispatch backend. Note that later extensions can be beneficial even if you are only using 128-bit vectors: AVX2 and AVX-512 provide more efficient instructions for some operations, diff --git a/fearless_simd/src/generated.rs b/fearless_simd/src/generated.rs index d42fbb31..b80f7a6b 100644 --- a/fearless_simd/src/generated.rs +++ b/fearless_simd/src/generated.rs @@ -3,48 +3,50 @@ #![expect( missing_docs, - trivial_numeric_casts, - clippy::unnecessary_cast, clippy::cast_possible_truncation, clippy::unseparated_literal_suffix, clippy::use_self, clippy::wrong_self_convention, reason = "Simplifies the generator and has no effect on the machine code" )] -#![cfg_attr( - target_arch = "x86_64", - expect( - clippy::new_without_default, - reason = "TODO: https://github.com/linebender/fearless_simd/issues/40" - ) -)] -#![cfg_attr( - target_arch = "wasm32", - expect( - clippy::new_without_default, - reason = "TODO: https://github.com/linebender/fearless_simd/issues/40" - ) -)] -#![cfg_attr( - all( - feature = "std", - all(not(target_arch = "x86_64"), not(target_arch = "wasm32")) - ), - expect( - clippy::new_without_default, - reason = "TODO: https://github.com/linebender/fearless_simd/issues/40" - ) +#![allow( + trivial_numeric_casts, + clippy::unnecessary_cast, + clippy::new_without_default, + reason = "Simplifies the generator and has no effect on the machine code, only tripped by some backends" )] //! A module containing generated files //! //! All files in this subdirectory are autogenerated by the `fearless_simd_gen` crate. +// Fallback is unnecessary exactly when one of our SIMD backends is guaranteed as the baseline. +// Keep this predicate in sync with `Level::Fallback` and `dispatch!`. +macro_rules! with_fallback { + ($($item:item)*) => {$( + #[cfg(any( + feature = "force_support_fallback", + not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + target_feature = "fxsr" + ), + all(target_arch = "wasm32", target_feature = "simd128") + )) + ))] + $item + )*}; +} + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod avx2; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod avx512; -mod fallback; +with_fallback! { + mod fallback; +} #[cfg(target_arch = "aarch64")] mod neon; mod ops; @@ -61,7 +63,9 @@ mod wasm; pub use avx2::*; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub use avx512::*; -pub use fallback::*; +with_fallback! { + pub use fallback::*; +} #[cfg(target_arch = "aarch64")] pub use neon::*; pub use simd_trait::*; diff --git a/fearless_simd/src/lib.rs b/fearless_simd/src/lib.rs index 31a7711a..2fc71414 100644 --- a/fearless_simd/src/lib.rs +++ b/fearless_simd/src/lib.rs @@ -166,6 +166,8 @@ //! These configuration flags only control automatic multiversioning. Disabling one does not remove its token type, its //! [`Simd`] implementation, or explicit [`kernel`] support; for example, an `Avx2` token can still be used to call an //! AVX2 kernel when the CPU supports it. +//! `disable_dispatch_sse2` has no effect when SSE2 is part of the ambient target baseline, because +//! that baseline remains the terminal dispatch backend. //! //! Note that later extensions can be beneficial even if you are only using 128-bit vectors: //! AVX2 and AVX-512 provide more efficient instructions for some operations, @@ -352,7 +354,7 @@ fn detect_x86_level() -> Level { // Safety: All features required by Sse2 were detected above. unsafe { Level::Sse2(Sse2::assume_supported()) } } else { - Level::Fallback(Fallback::new()) + Level::baseline() } } @@ -365,7 +367,25 @@ fn detect_x86_level() -> Level { pub enum Level { /// Scalar fallback level, i.e. no supported SIMD features are to be used. /// + /// This variant is **absent** on targets that supports a higher baseline + /// (`aarch64-*`, `i686-*`, `x86_64-*`, WASM with SIMD) unless the `force_support_fallback` + /// Cargo feature is enabled. Instead of matching on this variant, + /// call [`is_fallback`](Level::is_fallback) which is always available. + /// /// This can be created with [`Level::fallback`]. + // Keep this predicate in sync with the fallback module and `dispatch!`. + #[cfg(any( + feature = "force_support_fallback", + not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + target_feature = "fxsr" + ), + all(target_arch = "wasm32", target_feature = "simd128") + )) + ))] Fallback(Fallback), /// The Neon instruction set on 64 bit ARM. #[cfg(target_arch = "aarch64")] @@ -450,8 +470,29 @@ impl Level { /// Check whether this is the `Fallback` level; that is, whether no better feature level could /// be statically or dynamically detected. This is useful if there's a scalarized version of /// your algorithm that runs faster if SIMD isn't supported. + /// + /// This method is always available, even when the fallback backend is not compiled. In that + /// case, it always returns `false`. pub fn is_fallback(self) -> bool { - matches!(self, Self::Fallback(_)) + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + return self.as_sse2().is_none(); + + #[cfg(target_arch = "aarch64")] + return self.as_neon().is_none(); + + #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] + return self.as_wasm_simd128().is_none(); + + #[cfg(any( + all(target_arch = "wasm32", not(target_feature = "simd128")), + not(any( + target_arch = "x86", + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "wasm32" + )) + ))] + return true; } /// If this is a proof that Neon (or better) is available, access that instruction set. @@ -799,7 +840,12 @@ impl Level { return Self::Sse4_2(sse4_2); } - #[cfg(not(disable_dispatch_sse2))] + // The ambient SSE2 baseline is the terminal backend and cannot be disabled. Falling + // below it would require compiling the otherwise-unneeded fallback implementation. + #[cfg(any( + not(disable_dispatch_sse2), + all(target_feature = "sse2", target_feature = "fxsr") + ))] if let Some(sse2) = self.as_sse2().or_else(|| baseline.as_sse2()) { return Self::Sse2(sse2); } @@ -824,7 +870,7 @@ impl Level { } } - Self::Fallback(Fallback::new()) + Self::baseline() } /// Create a scalar fallback level, which uses no SIMD instructions. @@ -852,6 +898,21 @@ mod tests { assert_is_send_sync::(); } + #[test] + fn baseline_reports_whether_fallback_is_required() { + let has_simd_baseline = cfg!(any( + all(target_arch = "aarch64", target_feature = "neon"), + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + target_feature = "fxsr" + ), + all(target_arch = "wasm32", target_feature = "simd128") + )); + + assert_eq!(Level::baseline().is_fallback(), !has_simd_baseline); + } + #[cfg(all( any(feature = "std", target_arch = "wasm32"), not(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64"))) diff --git a/fearless_simd/src/macros.rs b/fearless_simd/src/macros.rs index 8f16e658..7671e123 100644 --- a/fearless_simd/src/macros.rs +++ b/fearless_simd/src/macros.rs @@ -49,6 +49,9 @@ #[macro_export] macro_rules! dispatch { ($level:expr, $simd:pat => $op:expr) => {{ + $crate::__fearless_simd_dispatch_inner!($level, $simd => $op) + }}; + (@impl $level:expr, $simd:pat => $op:expr; $forced_fallback_arm:literal) => {{ match $crate::Level::__dispatch_target($level) { #[cfg(target_arch = "aarch64")] $crate::Level::Neon(neon) => { @@ -74,14 +77,53 @@ macro_rules! dispatch { $crate::Level::Avx512(avx512) => { $crate::__fearless_simd_dispatch_dispatch_avx512!(avx512, $simd => $op) } - $crate::Level::Fallback(fb) => { - $crate::__fearless_simd_dispatch_dispatch_fallback!(fb, $simd => $op) + // Keep this predicate in sync with `Level::Fallback` and the fallback module. The + // literal carries fearless_simd's feature selection into expansion in a downstream + // crate, where `cfg(feature = ...)` would be incorrect. + #[cfg(any( + $forced_fallback_arm, + not(any( + all(target_arch = "aarch64", target_feature = "neon"), + all( + any(target_arch = "x86", target_arch = "x86_64"), + target_feature = "sse2", + target_feature = "fxsr" + ), + all(target_arch = "wasm32", target_feature = "simd128") + )) + ))] + $crate::Level::Fallback(fallback) => { + $crate::__fearless_simd_dispatch_with_token!(fallback, $simd => $op) } _ => unreachable!(), } }}; } +// This macro turns whether `force_support_fallback` is enabled into a boolean literal in +// `dispatch!`. Exported macro bodies are expanded in the downstream crate, so evaluating the +// feature cfg directly in `dispatch!` would inspect the wrong crate's features. + +/// Implementation detail of [`crate::dispatch`]; this is not public API. +#[macro_export] +#[doc(hidden)] +#[cfg(feature = "force_support_fallback")] +macro_rules! __fearless_simd_dispatch_inner { + ($level:expr, $simd:pat => $op:expr) => { + $crate::dispatch!(@impl $level, $simd => $op; true) + }; +} + +/// Implementation detail of [`crate::dispatch`]; this is not public API. +#[macro_export] +#[doc(hidden)] +#[cfg(not(feature = "force_support_fallback"))] +macro_rules! __fearless_simd_dispatch_inner { + ($level:expr, $simd:pat => $op:expr) => { + $crate::dispatch!(@impl $level, $simd => $op; false) + }; +} + // The dispatch helpers are split into cfg-selected macro definitions // because exported macro bodies are expanded in the downstream crate, // so selecting the helper definitions here preserves `fearless_simd`'s @@ -126,58 +168,6 @@ macro_rules! __fearless_simd_dispatch_pruned { }}; } -/// Implementation detail of [`crate::dispatch`]; this is not public API. -#[macro_export] -#[doc(hidden)] -#[cfg(any( - all(target_arch = "aarch64", not(target_feature = "neon")), - all( - any(target_arch = "x86", target_arch = "x86_64"), - any( - disable_dispatch_sse2, - not(all(target_feature = "sse2", target_feature = "fxsr")) - ), - ), - all(target_arch = "wasm32", not(target_feature = "simd128")), - not(any( - target_arch = "x86", - target_arch = "x86_64", - target_arch = "aarch64", - target_arch = "wasm32" - )), - feature = "force_support_fallback" -))] -macro_rules! __fearless_simd_dispatch_dispatch_fallback { - ($fallback:expr, $simd:pat => $op:expr) => { - $crate::__fearless_simd_dispatch_with_token!($fallback, $simd => $op) - }; -} - -/// Implementation detail of [`crate::dispatch`]; this is not public API. -#[macro_export] -#[doc(hidden)] -#[cfg(not(any( - all(target_arch = "aarch64", not(target_feature = "neon")), - all( - any(target_arch = "x86", target_arch = "x86_64"), - any( - disable_dispatch_sse2, - not(all(target_feature = "sse2", target_feature = "fxsr")) - ), - ), - all(target_arch = "wasm32", not(target_feature = "simd128")), - not(any( - target_arch = "x86", - target_arch = "x86_64", - target_arch = "aarch64", - target_arch = "wasm32" - )), - feature = "force_support_fallback" -)))] -macro_rules! __fearless_simd_dispatch_dispatch_fallback { - ($fallback:expr, $simd:pat => $op:expr) => {{ $crate::__fearless_simd_dispatch_pruned!($fallback) }}; -} - /// Implementation detail of [`crate::dispatch`]; this is not public API. #[macro_export] #[doc(hidden)] @@ -344,7 +334,10 @@ macro_rules! __fearless_simd_dispatch_dispatch_sse4_2 { #[macro_export] #[doc(hidden)] #[cfg(all( - not(disable_dispatch_sse2), + any( + not(disable_dispatch_sse2), + all(target_feature = "sse2", target_feature = "fxsr") + ), any( disable_dispatch_sse4_2, not(all( @@ -365,7 +358,10 @@ macro_rules! __fearless_simd_dispatch_dispatch_sse2 { #[macro_export] #[doc(hidden)] #[cfg(any( - disable_dispatch_sse2, + all( + disable_dispatch_sse2, + not(all(target_feature = "sse2", target_feature = "fxsr")) + ), all( not(disable_dispatch_sse4_2), target_feature = "fxsr", @@ -401,9 +397,15 @@ mod tests { fn x86_dispatch_backend(_: S) -> X86DispatchBackend { use core::any::TypeId; + #[cfg(any( + not(all(target_feature = "sse2", target_feature = "fxsr")), + feature = "force_support_fallback" + ))] if TypeId::of::() == TypeId::of::() { - X86DispatchBackend::Fallback - } else if TypeId::of::() == TypeId::of::() { + return X86DispatchBackend::Fallback; + } + + if TypeId::of::() == TypeId::of::() { X86DispatchBackend::Sse2 } else if TypeId::of::() == TypeId::of::() { X86DispatchBackend::Sse4_2 @@ -424,7 +426,11 @@ mod tests { X86DispatchBackend::Avx2 } else if cfg!(not(disable_dispatch_sse4_2)) && level.as_sse4_2().is_some() { X86DispatchBackend::Sse4_2 - } else if cfg!(not(disable_dispatch_sse2)) && level.as_sse2().is_some() { + } else if cfg!(any( + not(disable_dispatch_sse2), + all(target_feature = "sse2", target_feature = "fxsr") + )) && level.as_sse2().is_some() + { X86DispatchBackend::Sse2 } else { X86DispatchBackend::Fallback @@ -514,6 +520,20 @@ mod tests { let _ = level.as_avx512().is_some(); } + #[cfg(all( + feature = "std", + any(target_arch = "x86", target_arch = "x86_64"), + disable_dispatch_sse2, + target_feature = "sse2", + target_feature = "fxsr" + ))] + #[test] + fn ambient_sse2_is_the_terminal_backend_when_dispatch_is_disabled() { + let actual = dispatch!(Level::new(), simd => x86_dispatch_backend(simd)); + + assert_ne!(actual, X86DispatchBackend::Fallback); + } + /// Mostly useful with `RUSTFLAGS='-C target-cpu=x86-64-v3'` and higher, doesn't do much otherwise #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] #[test] @@ -533,7 +553,8 @@ mod tests { #[cfg(all( not(feature = "force_support_fallback"), - any(target_arch = "x86", target_arch = "x86_64") + any(target_arch = "x86", target_arch = "x86_64"), + not(all(target_feature = "sse2", target_feature = "fxsr")) ))] #[test] fn fallback_dispatches_as_baseline() { diff --git a/fearless_simd_tests/Cargo.toml b/fearless_simd_tests/Cargo.toml index 92039257..0d00bf94 100644 --- a/fearless_simd_tests/Cargo.toml +++ b/fearless_simd_tests/Cargo.toml @@ -21,5 +21,8 @@ workspace = true [dependencies] fastrand = "2.5.0" -fearless_simd = { workspace = true, features = ["std"] } +fearless_simd = { workspace = true, features = [ + "std", + "force_support_fallback", +] } fearless_simd_dev_macros = { workspace = true } diff --git a/fearless_simd_tests/tests/mod.rs b/fearless_simd_tests/tests/mod.rs index 55b7dd31..caed2bb2 100644 --- a/fearless_simd_tests/tests/mod.rs +++ b/fearless_simd_tests/tests/mod.rs @@ -19,6 +19,13 @@ mod harness; mod soundness; mod token_soundness; +#[test] +fn dispatches_forced_fallback_across_crates() { + let is_fallback = dispatch!(Level::fallback(), simd => simd.level().is_fallback()); + + assert!(is_fallback); +} + #[allow(clippy::allow_attributes, reason = "Only needed in some cfgs.")] #[allow( unused_variables,