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
45 changes: 45 additions & 0 deletions .claude/plans/simd-sse-v2-tier-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Plan: x86-64-v2 SSE tier (v1)

Operator decision 2026-10-10: a compile-time SIMD tier for **x86-64-v2**
builds (SSE2 through SSE4.2, no AVX). Baseline x86-64 builds (no SSE4.2) are
unchanged: they keep the AVX2 arm and the `cpu_guard` AVX2 startup floor.
No runtime feature checks in the SIMD code.

## Selection

* Arm predicate: `all(target_arch = "x86_64", target_feature = "sse4.2",
not(target_feature = "avx"))`, written out at each gate.
* Reached by `target-cpu=native` on a v2-only CPU (Nehalem/Westmere, Atom
Silvermont-class, VMs that hide AVX), or the new `.cargo/config-v2.toml`
(`-Ctarget-cpu=x86-64-v2`).
* Unchanged: AVX-512, AVX2, AVX-without-AVX2 (`simd_avx.rs`), and baseline.

## Shape: route to the scalar realization, not a new intrinsic file

Every type in `simd_avx2.rs` / `simd_avx512.rs` wraps a 256- or 512-bit
register (`U8x32(pub __m256i)`, …), so none of them can be the v2 realization:
unlike the AVX arm, there is no impl-block split that works. `simd.rs` instead
routes the v2 arm to `simd_scalar.rs` — plain `[T; N]` arrays that LLVM
vectorizes to SSE. Measured earlier (existing `simd_scalar.rs` compiled at
x86-64 / v2 / sandybridge / v3): element-wise ops ~2× the AVX2 instruction
count, SSE2/SSSE3/SSE4.2 produce identical code, reductions stay scalar, and
`mul_add` becomes a software `fmaf` call on any tier without FMA.

Only the `simd_*` backends import the x86 modules directly; every consumer
goes through `crate::simd`, so the switch is confined to `simd.rs`.

## Work items

- [ ] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
run under `qemu-x86_64-static -cpu Nehalem`) in `scripts/masking-parity.sh`
and a `realization/v2` row in `simd-matrix.yaml`, with an objdump witness
(no AVX instruction at all: no `%ymm`, no VEX `v`-prefixed op). Expect it
to fail before the arm exists.
- [ ] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
the v2 arm.
- [ ] P2 close the API gap: compile v2, list every missing item, add each to
`simd_scalar.rs` (types `U8x32`, `F32Mask16`, `F64Mask8` and the x86-only
methods). Safe Rust only.
- [ ] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
v2/v3/v4/avx/baseline; codegen witness avx2/avx512 unchanged.
- [ ] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.
20 changes: 20 additions & 0 deletions src/simd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,26 @@ mod tests {
/// transpose that drops, duplicates or misroutes any word fails, and the
/// fixture is asserted not to be symmetric, so an identity "transpose"
/// fails too. Transposing twice must return the input.
/// Every `i8` value through `I8x16`/`I8x32::saturating_abs`, on whichever
/// path this build compiled: the SSSE3 intrinsic (`cfg(target_feature =
/// "ssse3")`: v3, v4, native) or the scalar fallback (a baseline build,
/// e.g. CI's `tests/*` jobs, whose RUSTFLAGS drop `target-cpu`). Lives
/// here, not in `simd_avx512.rs`, whose test modules are `avx512f`-only.
#[cfg(all(feature = "std", target_arch = "x86_64"))]
#[test]
fn x86_saturating_abs_is_exact_for_all_256_i8_values() {
use crate::simd_avx512::{I8x16, I8x32};
let all: [i8; 256] = core::array::from_fn(|i| i as u8 as i8);
for c in all.chunks_exact(16) {
let v: [i8; 16] = c.try_into().unwrap();
assert_eq!(I8x16::from_array(v).saturating_abs().to_array(), v.map(i8::saturating_abs));
}
for c in all.chunks_exact(32) {
let v: [i8; 32] = c.try_into().unwrap();
assert_eq!(I8x32::from_array(v).saturating_abs().to_array(), v.map(i8::saturating_abs));
}
}

/// The x86 256-bit integer methods that had no test on any stable arm,
/// each checked lane-by-lane against a scalar reference on asymmetric
/// data. They run on the AVX2, AVX-512 and AVX-without-AVX2 realizations;
Expand Down
45 changes: 27 additions & 18 deletions src/simd_avx512.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2943,24 +2943,33 @@ impl I8x16 {
/// ```
#[inline(always)]
pub fn saturating_abs(self) -> Self {
// SAFETY: `_mm_abs_epi8` needs SSSE3 and `_mm_min_epu8` needs SSE2. SSE2
// is in the x86_64 baseline; SSSE3 is NOT, and this file compiles for
// every x86_64 build, so SSSE3 at run time is the caller's obligation.
// `target-cpu=native` (default) and `config-v3`/`config-v4` include it;
// a baseline build (e.g. a RUSTFLAGS env replacing the config) compiles
// this and would SIGILL. The unaligned load/store match the
// `[i8; 16]` storage. VPABSB returns 0x80 for `i8::MIN` (the bit pattern
// of +128, which does not fit in i8); VPMINUB then clamps 0x80 (= 128
// unsigned) down to 0x7f (= 127 = `i8::MAX`), producing the saturating
// result bare VPABSB cannot — per the consumer contract's VPABSB
// correction. All 16 lanes are saturated branchlessly.
use core::arch::x86_64::*;
unsafe {
let v = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
let clamped = _mm_min_epu8(_mm_abs_epi8(v), _mm_set1_epi8(0x7f_u8 as i8));
let mut o = [0i8; 16];
_mm_storeu_si128(o.as_mut_ptr() as *mut __m128i, clamped);
Self(o)
// SSSE3 (`_mm_abs_epi8`) is not in the x86_64 baseline, and this file
// compiles for every x86_64 build. With SSSE3 compiled in (the default
// `target-cpu=native` on any host since 2006, `config-v3`, `config-v4`)
// the intrinsic path runs; otherwise the scalar loop does, so no build
// of this method can SIGILL. Both give identical results.
#[cfg(target_feature = "ssse3")]
{
// SAFETY: SSSE3 is enabled at compile time (`cfg` above), so the
// whole build already assumes it; `_mm_min_epu8` is SSE2, which is
// baseline. The unaligned load/store match the `[i8; 16]` storage.
// VPABSB returns 0x80 for `i8::MIN` (the bit pattern of +128,
// which does not fit in i8); VPMINUB then clamps 0x80 (= 128
// unsigned) down to 0x7f (= 127 = `i8::MAX`), producing the
// saturating result bare VPABSB cannot — per the consumer
// contract's VPABSB correction.
use core::arch::x86_64::*;
unsafe {
let v = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
let clamped = _mm_min_epu8(_mm_abs_epi8(v), _mm_set1_epi8(0x7f_u8 as i8));
let mut o = [0i8; 16];
_mm_storeu_si128(o.as_mut_ptr() as *mut __m128i, clamped);
Self(o)
}
}
#[cfg(not(target_feature = "ssse3"))]
{
Self(core::array::from_fn(|i| self.0[i].saturating_abs()))
}
}
}
Expand Down
Loading