diff --git a/.claude/plans/simd-sse-v2-tier-v1.md b/.claude/plans/simd-sse-v2-tier-v1.md new file mode 100644 index 00000000..c52a14ce --- /dev/null +++ b/.claude/plans/simd-sse-v2-tier-v1.md @@ -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. diff --git a/src/simd.rs b/src/simd.rs index b347724f..24253c3e 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -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; diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index 9af9aa1a..ec8f8ae9 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -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())) } } }