Skip to content

Commit db4daa8

Browse files
authored
Merge pull request #352 from AdaWorldAPI/ccr-9f3b8075-74u47l
simd: I8x16::saturating_abs no longer needs SSSE3 at run time
2 parents 59bdb04 + f902b22 commit db4daa8

3 files changed

Lines changed: 92 additions & 18 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Plan: x86-64-v2 SSE tier (v1)
2+
3+
Operator decision 2026-10-10: a compile-time SIMD tier for **x86-64-v2**
4+
builds (SSE2 through SSE4.2, no AVX). Baseline x86-64 builds (no SSE4.2) are
5+
unchanged: they keep the AVX2 arm and the `cpu_guard` AVX2 startup floor.
6+
No runtime feature checks in the SIMD code.
7+
8+
## Selection
9+
10+
* Arm predicate: `all(target_arch = "x86_64", target_feature = "sse4.2",
11+
not(target_feature = "avx"))`, written out at each gate.
12+
* Reached by `target-cpu=native` on a v2-only CPU (Nehalem/Westmere, Atom
13+
Silvermont-class, VMs that hide AVX), or the new `.cargo/config-v2.toml`
14+
(`-Ctarget-cpu=x86-64-v2`).
15+
* Unchanged: AVX-512, AVX2, AVX-without-AVX2 (`simd_avx.rs`), and baseline.
16+
17+
## Shape: route to the scalar realization, not a new intrinsic file
18+
19+
Every type in `simd_avx2.rs` / `simd_avx512.rs` wraps a 256- or 512-bit
20+
register (`U8x32(pub __m256i)`, …), so none of them can be the v2 realization:
21+
unlike the AVX arm, there is no impl-block split that works. `simd.rs` instead
22+
routes the v2 arm to `simd_scalar.rs` — plain `[T; N]` arrays that LLVM
23+
vectorizes to SSE. Measured earlier (existing `simd_scalar.rs` compiled at
24+
x86-64 / v2 / sandybridge / v3): element-wise ops ~2× the AVX2 instruction
25+
count, SSE2/SSSE3/SSE4.2 produce identical code, reductions stay scalar, and
26+
`mul_add` becomes a software `fmaf` call on any tier without FMA.
27+
28+
Only the `simd_*` backends import the x86 modules directly; every consumer
29+
goes through `crate::simd`, so the switch is confined to `simd.rs`.
30+
31+
## Work items
32+
33+
- [ ] P0 TESTS FIRST: `v2-qemu` parity arm (`--config .cargo/config-v2.toml`,
34+
run under `qemu-x86_64-static -cpu Nehalem`) in `scripts/masking-parity.sh`
35+
and a `realization/v2` row in `simd-matrix.yaml`, with an objdump witness
36+
(no AVX instruction at all: no `%ymm`, no VEX `v`-prefixed op). Expect it
37+
to fail before the arm exists.
38+
- [ ] P1 `.cargo/config-v2.toml`; `simd.rs` routing; `cpu_guard` floor gated off
39+
the v2 arm.
40+
- [ ] P2 close the API gap: compile v2, list every missing item, add each to
41+
`simd_scalar.rs` (types `U8x32`, `F32Mask16`, `F64Mask8` and the x86-only
42+
methods). Safe Rust only.
43+
- [ ] P3 gates: lib tests on v2 under qemu Nehalem; parity on every arm; clippy
44+
v2/v3/v4/avx/baseline; codegen witness avx2/avx512 unchanged.
45+
- [ ] P4 docs: CLAUDE.md realization list, blackboard, `simd.rs` dispatch comment.

‎src/simd.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,26 @@ mod tests {
10831083
/// transpose that drops, duplicates or misroutes any word fails, and the
10841084
/// fixture is asserted not to be symmetric, so an identity "transpose"
10851085
/// fails too. Transposing twice must return the input.
1086+
/// Every `i8` value through `I8x16`/`I8x32::saturating_abs`, on whichever
1087+
/// path this build compiled: the SSSE3 intrinsic (`cfg(target_feature =
1088+
/// "ssse3")`: v3, v4, native) or the scalar fallback (a baseline build,
1089+
/// e.g. CI's `tests/*` jobs, whose RUSTFLAGS drop `target-cpu`). Lives
1090+
/// here, not in `simd_avx512.rs`, whose test modules are `avx512f`-only.
1091+
#[cfg(all(feature = "std", target_arch = "x86_64"))]
1092+
#[test]
1093+
fn x86_saturating_abs_is_exact_for_all_256_i8_values() {
1094+
use crate::simd_avx512::{I8x16, I8x32};
1095+
let all: [i8; 256] = core::array::from_fn(|i| i as u8 as i8);
1096+
for c in all.chunks_exact(16) {
1097+
let v: [i8; 16] = c.try_into().unwrap();
1098+
assert_eq!(I8x16::from_array(v).saturating_abs().to_array(), v.map(i8::saturating_abs));
1099+
}
1100+
for c in all.chunks_exact(32) {
1101+
let v: [i8; 32] = c.try_into().unwrap();
1102+
assert_eq!(I8x32::from_array(v).saturating_abs().to_array(), v.map(i8::saturating_abs));
1103+
}
1104+
}
1105+
10861106
/// The x86 256-bit integer methods that had no test on any stable arm,
10871107
/// each checked lane-by-lane against a scalar reference on asymmetric
10881108
/// data. They run on the AVX2, AVX-512 and AVX-without-AVX2 realizations;

‎src/simd_avx512.rs‎

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,24 +2943,33 @@ impl I8x16 {
29432943
/// ```
29442944
#[inline(always)]
29452945
pub fn saturating_abs(self) -> Self {
2946-
// SAFETY: `_mm_abs_epi8` needs SSSE3 and `_mm_min_epu8` needs SSE2. SSE2
2947-
// is in the x86_64 baseline; SSSE3 is NOT, and this file compiles for
2948-
// every x86_64 build, so SSSE3 at run time is the caller's obligation.
2949-
// `target-cpu=native` (default) and `config-v3`/`config-v4` include it;
2950-
// a baseline build (e.g. a RUSTFLAGS env replacing the config) compiles
2951-
// this and would SIGILL. The unaligned load/store match the
2952-
// `[i8; 16]` storage. VPABSB returns 0x80 for `i8::MIN` (the bit pattern
2953-
// of +128, which does not fit in i8); VPMINUB then clamps 0x80 (= 128
2954-
// unsigned) down to 0x7f (= 127 = `i8::MAX`), producing the saturating
2955-
// result bare VPABSB cannot — per the consumer contract's VPABSB
2956-
// correction. All 16 lanes are saturated branchlessly.
2957-
use core::arch::x86_64::*;
2958-
unsafe {
2959-
let v = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
2960-
let clamped = _mm_min_epu8(_mm_abs_epi8(v), _mm_set1_epi8(0x7f_u8 as i8));
2961-
let mut o = [0i8; 16];
2962-
_mm_storeu_si128(o.as_mut_ptr() as *mut __m128i, clamped);
2963-
Self(o)
2946+
// SSSE3 (`_mm_abs_epi8`) is not in the x86_64 baseline, and this file
2947+
// compiles for every x86_64 build. With SSSE3 compiled in (the default
2948+
// `target-cpu=native` on any host since 2006, `config-v3`, `config-v4`)
2949+
// the intrinsic path runs; otherwise the scalar loop does, so no build
2950+
// of this method can SIGILL. Both give identical results.
2951+
#[cfg(target_feature = "ssse3")]
2952+
{
2953+
// SAFETY: SSSE3 is enabled at compile time (`cfg` above), so the
2954+
// whole build already assumes it; `_mm_min_epu8` is SSE2, which is
2955+
// baseline. The unaligned load/store match the `[i8; 16]` storage.
2956+
// VPABSB returns 0x80 for `i8::MIN` (the bit pattern of +128,
2957+
// which does not fit in i8); VPMINUB then clamps 0x80 (= 128
2958+
// unsigned) down to 0x7f (= 127 = `i8::MAX`), producing the
2959+
// saturating result bare VPABSB cannot — per the consumer
2960+
// contract's VPABSB correction.
2961+
use core::arch::x86_64::*;
2962+
unsafe {
2963+
let v = _mm_loadu_si128(self.0.as_ptr() as *const __m128i);
2964+
let clamped = _mm_min_epu8(_mm_abs_epi8(v), _mm_set1_epi8(0x7f_u8 as i8));
2965+
let mut o = [0i8; 16];
2966+
_mm_storeu_si128(o.as_mut_ptr() as *mut __m128i, clamped);
2967+
Self(o)
2968+
}
2969+
}
2970+
#[cfg(not(target_feature = "ssse3"))]
2971+
{
2972+
Self(core::array::from_fn(|i| self.0[i].saturating_abs()))
29642973
}
29652974
}
29662975
}

0 commit comments

Comments
 (0)