Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ rust.unused_import_braces = "warn"
rust.unused_lifetimes = "warn"
rust.unused_macro_rules = "warn"
rust.unused_qualifications = "warn"
rust.unexpected_cfgs = { level = "warn", check-cfg = [
"cfg(disable_dispatch_sse4_2)",
"cfg(disable_dispatch_avx2)",
"cfg(disable_dispatch_avx512)",
] }

clippy.too_many_arguments = "allow"

Expand Down
15 changes: 1 addition & 14 deletions fearless_simd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ rustdoc-args = [


[features]
default = ["std", "dispatch_sse4_2", "dispatch_avx2", "dispatch_avx512"]
default = ["std"]
# Get floating point functions from the standard library (likely using your targets libc).
# Also allows using `Level::new` on all platforms, to detect which target features are enabled
std = []
Expand All @@ -37,19 +37,6 @@ libm = ["dep:libm"]
# This is primarily used for tests
force_support_fallback = []

# Enable automatic multiversioning for SSE4.2 through `dispatch!` and `Level::dispatch`.
# Has no effect on architectures other than x86 and x86-64.
# Disabling this does not remove the `Sse4_2` token or explicit `kernel!` support.
dispatch_sse4_2 = []
# Enable automatic multiversioning for AVX2 through `dispatch!` and `Level::dispatch`.
# Has no effect on architectures other than x86 and x86-64.
# Disabling this does not remove the `Avx2` token or explicit `kernel!` support.
dispatch_avx2 = []
# Enable automatic multiversioning for AVX-512 through `dispatch!` and `Level::dispatch`.
# Has no effect on architectures other than x86 and x86-64.
# Disabling this doeces not remove the `Avx512` token or explicit `kernel!` support.
dispatch_avx512 = []

[lints]
workspace = true

Expand Down
16 changes: 7 additions & 9 deletions fearless_simd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,18 @@ runtime.
x86 CPUs are not guaranteed to have any SIMD particular instruction set, so `fearless_simd` compiles a version
of each function generic over [`Simd`] for each instruction set, and [`dispatch`] selects the best one at runtime.

This is strictly required to take advantage of SIMD, but results in an increased binary size on x86.
This is necessary to take advantage of SIMD, but results in an increased binary size on x86.
If binary size is a concern, the increase can be partially mitigated by setting
[`codegen-units=1`](https://nnethercote.github.io/perf-book/build-configuration.html#codegen-units)
or [`lto=true`](https://nnethercote.github.io/perf-book/build-configuration.html#link-time-optimization) in your Cargo.toml,
at the cost of longer build times.

As a last resort, you can turn off multiversioning for specific SIMD instruction sets, see "Feature Flags" below.
As a last resort, you can turn off multiversioning for specific SIMD instruction sets by passing
`--cfg disable_dispatch_sse4_2`, `--cfg disable_dispatch_avx2`, or `--cfg disable_dispatch_avx512` in `RUSTFLAGS`.
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.

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,
and AVX-512 also more than doubles the number of vector registers of all sizes.
Expand All @@ -210,13 +215,6 @@ The following crate [feature flags](https://doc.rust-lang.org/cargo/reference/fe
Also allows using [`Level::new`] on all platforms, to detect which target features are enabled.
- `libm`: Use floating point implementations from [libm]. Useful for `#[no_std]`.
- `force_support_fallback`: Force scalar fallback, to be supported, even if your compilation target has a better baseline.
- `dispatch_sse4_2`, `dispatch_avx2`, `dispatch_avx512` (enabled by default): Enable automatic x86 multiversioning for the
corresponding instruction set in [`dispatch`] and [`Level::dispatch`].

The x86 feature 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. Because Cargo features are additive, opt out of x86 multiversioning with
`default-features = false`, then enable the pieces you want, for example `features = ["std", "dispatch_sse4_2"]`.

At least one of `std` and `libm` is required; `std` overrides `libm`.

Expand Down
29 changes: 1 addition & 28 deletions fearless_simd/src/generated/sse4_2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,7 @@ impl Simd for Sse4_2 {
type mask64s = mask64x2<Self>;
#[inline(always)]
fn level(self) -> Level {
#[cfg(not(all(
target_feature = "avx2",
target_feature = "bmi1",
target_feature = "bmi2",
target_feature = "cmpxchg16b",
target_feature = "f16c",
target_feature = "fma",
target_feature = "lzcnt",
target_feature = "movbe",
target_feature = "popcnt",
target_feature = "xsave"
)))]
return Level::Sse4_2(self);
#[cfg(all(
target_feature = "avx2",
target_feature = "bmi1",
target_feature = "bmi2",
target_feature = "cmpxchg16b",
target_feature = "f16c",
target_feature = "fma",
target_feature = "lzcnt",
target_feature = "movbe",
target_feature = "popcnt",
target_feature = "xsave"
))]
{
Level::baseline()
}
Level::Sse4_2(self)
}
#[inline]
fn vectorize<F: FnOnce() -> R, R>(self, f: F) -> R {
Expand Down
171 changes: 68 additions & 103 deletions fearless_simd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,18 @@
//! x86 CPUs are not guaranteed to have any SIMD particular instruction set, so `fearless_simd` compiles a version
//! of each function generic over [`Simd`] for each instruction set, and [`dispatch`] selects the best one at runtime.
//!
//! This is strictly required to take advantage of SIMD, but results in an increased binary size on x86.
//! This is necessary to take advantage of SIMD, but results in an increased binary size on x86.
//! If binary size is a concern, the increase can be partially mitigated by setting
//! [`codegen-units=1`](https://nnethercote.github.io/perf-book/build-configuration.html#codegen-units)
//! or [`lto=true`](https://nnethercote.github.io/perf-book/build-configuration.html#link-time-optimization) in your Cargo.toml,
//! at the cost of longer build times.
//!
//! As a last resort, you can turn off multiversioning for specific SIMD instruction sets, see "Feature Flags" below.
//! As a last resort, you can turn off multiversioning for specific SIMD instruction sets by passing
//! `--cfg disable_dispatch_sse4_2`, `--cfg disable_dispatch_avx2`, or `--cfg disable_dispatch_avx512` in `RUSTFLAGS`.
//! 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.
//!
//! 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,
//! and AVX-512 also more than doubles the number of vector registers of all sizes.
Expand All @@ -177,13 +182,6 @@
//! Also allows using [`Level::new`] on all platforms, to detect which target features are enabled.
//! - `libm`: Use floating point implementations from [libm]. Useful for `#[no_std]`.
//! - `force_support_fallback`: Force scalar fallback, to be supported, even if your compilation target has a better baseline.
//! - `dispatch_sse4_2`, `dispatch_avx2`, `dispatch_avx512` (enabled by default): Enable automatic x86 multiversioning for the
//! corresponding instruction set in [`dispatch`] and [`Level::dispatch`].
//!
//! The x86 feature 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. Because Cargo features are additive, opt out of x86 multiversioning with
//! `default-features = false`, then enable the pieces you want, for example `features = ["std", "dispatch_sse4_2"]`.
//!
//! At least one of `std` and `libm` is required; `std` overrides `libm`.
//!
Expand Down Expand Up @@ -305,29 +303,6 @@ pub enum Level {
/// Scalar fallback level, i.e. no supported SIMD features are to be used.
///
/// This can be created with [`Level::fallback`].
// We only want to compile the fallback implementation if:
// - We're on a supported architecture, but don't statically support the lowest alternative level; OR
// - We're on an unsupported architecture; OR
// - The fallback is forcibly enabled
#[cfg(any(
all(target_arch = "aarch64", not(target_feature = "neon")),
all(
any(target_arch = "x86", target_arch = "x86_64"),
not(all(
target_feature = "sse4.2",
target_feature = "cmpxchg16b",
target_feature = "popcnt"
))
),
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"
))]
Fallback(Fallback),
/// The Neon instruction set on 64 bit ARM.
#[cfg(target_arch = "aarch64")]
Expand All @@ -339,22 +314,7 @@ pub enum Level {
/// Also known as x86-64-v2.
///
/// All production CPUs with SSE4.2 also support the other two extensions, so it is safe to require them.
// We don't need to support this if the compilation target definitely supports something better.
#[cfg(all(
any(target_arch = "x86", target_arch = "x86_64"),
not(all(
target_feature = "avx2",
target_feature = "bmi1",
target_feature = "bmi2",
target_feature = "cmpxchg16b",
target_feature = "f16c",
target_feature = "fma",
target_feature = "lzcnt",
target_feature = "movbe",
target_feature = "popcnt",
target_feature = "xsave"
))
))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Sse4_2(Sse4_2),
/// Ice Lake-class AVX-512 on (32 and 64 bit) x86.
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Expand Down Expand Up @@ -438,18 +398,6 @@ impl Level {
&& std::arch::is_x86_feature_detected!("cmpxchg16b")
&& std::arch::is_x86_feature_detected!("popcnt")
{
#[cfg(not(all(
target_feature = "avx2",
target_feature = "bmi1",
target_feature = "bmi2",
target_feature = "cmpxchg16b",
target_feature = "f16c",
target_feature = "fma",
target_feature = "lzcnt",
target_feature = "movbe",
target_feature = "popcnt",
target_feature = "xsave"
)))]
return unsafe { Self::Sse4_2(Sse4_2::new_unchecked()) };
}
}
Expand Down Expand Up @@ -500,34 +448,8 @@ 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 in cases where `Fallback` is not; for instance, if
/// you're targeting a platform that always supports some level of SIMD. In such cases, it will
/// always return false.
pub fn is_fallback(self) -> bool {
#[cfg(any(
all(target_arch = "aarch64", not(target_feature = "neon")),
all(
any(target_arch = "x86", target_arch = "x86_64"),
not(all(
target_feature = "sse4.2",
target_feature = "cmpxchg16b",
target_feature = "popcnt"
))
),
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"
))]
return matches!(self, Self::Fallback(_));

#[allow(unreachable_code, reason = "Fallback unreachable in some cfgs.")]
false
matches!(self, Self::Fallback(_))
}

/// If this is a proof that Neon (or better) is available, access that instruction set.
Expand Down Expand Up @@ -593,23 +515,7 @@ impl Level {
// Safety: The Avx2 struct represents the x86-64-v3 feature set being enabled, which
// includes the `sse4.2`, `cmpxchg16b`, and `popcnt` features required by Sse4_2.
Self::Avx2(_avx) => unsafe { Some(Sse4_2::new_unchecked()) },
#[cfg(not(all(
target_feature = "avx2",
target_feature = "bmi1",
target_feature = "bmi2",
target_feature = "cmpxchg16b",
target_feature = "f16c",
target_feature = "fma",
target_feature = "lzcnt",
target_feature = "movbe",
target_feature = "popcnt",
target_feature = "xsave"
)))]
Self::Sse4_2(sse42) => Some(sse42),
#[allow(
unreachable_patterns,
reason = "This arm is reachable on baseline x86/x86_64."
)]
_ => None,
}
}
Expand Down Expand Up @@ -814,6 +720,65 @@ impl Level {
}
}

#[doc(hidden)]
#[inline]
pub fn __dispatch_target(self) -> Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this method!

// Dispatch compiles only the selected multiversioned backends, but public tokens can
// still name lower levels even when the ambient target baseline makes those backends
// redundant. Normalize the proof to the best dispatchable level, while leaving exact
// token identity available for `kernel!` and explicit token use.
#[cfg(feature = "force_support_fallback")]
#[allow(
irrefutable_let_patterns,
reason = "On targets without supported SIMD, Fallback is the only Level variant."
)]
if let Self::Fallback(fallback) = self {
return Self::Fallback(fallback);
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
#[allow(unused_variables, reason = "Unused with all cfgs active")]
let baseline = Self::baseline();

#[cfg(not(disable_dispatch_avx512))]
if let Some(avx512) = self.as_avx512().or_else(|| baseline.as_avx512()) {
return Self::Avx512(avx512);
}

#[cfg(not(disable_dispatch_avx2))]
if let Some(avx2) = self.as_avx2().or_else(|| baseline.as_avx2()) {
return Self::Avx2(avx2);
}

#[cfg(not(disable_dispatch_sse4_2))]
if let Some(sse4_2) = self.as_sse4_2().or_else(|| baseline.as_sse4_2()) {
return Self::Sse4_2(sse4_2);
}
}

#[cfg(target_arch = "aarch64")]
{
let baseline = Self::baseline();
if let Some(neon) = self.as_neon().or_else(|| baseline.as_neon()) {
return Self::Neon(neon);
}
}

#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
{
let baseline = Self::baseline();
if let Some(wasm) = self
.as_wasm_simd128()
.or_else(|| baseline.as_wasm_simd128())
{
return Self::WasmSimd128(wasm);
}
}

Self::Fallback(Fallback::new())
}

/// Create a scalar fallback level, which uses no SIMD instructions.
///
/// This is primarily intended for tests; most users should prefer [`Level::new`] or [`Level::baseline`].
Expand Down
Loading
Loading