Skip to content

Commit 4c91834

Browse files
authored
Merge pull request #270 from AdaWorldAPI/claude/u32x16-exchange
simd: exchange<const G> on U32x16 — the transpose surface, all six backends
2 parents ceb1171 + c8dc217 commit 4c91834

7 files changed

Lines changed: 254 additions & 0 deletions

File tree

src/simd.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,68 @@ mod tests {
898898
);
899899
}
900900

901+
/// `exchange::<G>` composes a complete 16x16 transpose over four stages.
902+
///
903+
/// This is the control the codegen oracle's driver applies to
904+
/// `transpose_16x16_composed`, brought into the test suite so the library
905+
/// method is checked and not merely its probe-local twin. A packed-but-
906+
/// wrong shuffle network would pass a codegen histogram and fail here.
907+
///
908+
/// Note the semantics differ from `interleave_*` / `concat_*` above: those
909+
/// mirror x86's per-128-bit-lane `unpack`, whereas `exchange` pairs lane
910+
/// `c` with lane `c ^ G` across the whole vector. They are different
911+
/// permutations and neither substitutes for the other.
912+
#[test]
913+
fn u32x16_exchange_stages_compose_a_transpose() {
914+
fn stage<const G: usize>(m: &mut [U32x16; 16]) {
915+
for r in 0..16 {
916+
if r & G == 0 {
917+
let (lo, hi) = m[r].exchange::<G>(m[r | G]);
918+
m[r] = lo;
919+
m[r | G] = hi;
920+
}
921+
}
922+
}
923+
924+
// Row r, lane c = r*16 + c. A transpose must yield row r, lane c = c*16 + r.
925+
let mut m: [U32x16; 16] =
926+
core::array::from_fn(|r| U32x16::from_array(core::array::from_fn(|c| (r * 16 + c) as u32)));
927+
928+
stage::<1>(&mut m);
929+
stage::<2>(&mut m);
930+
stage::<4>(&mut m);
931+
stage::<8>(&mut m);
932+
933+
for (r, row) in m.iter().enumerate() {
934+
let want: [u32; 16] = core::array::from_fn(|c| (c * 16 + r) as u32);
935+
assert_eq!(row.to_array(), want, "row {r} after four exchange stages");
936+
}
937+
}
938+
939+
/// Each granularity in isolation, pinned lane-by-lane, so a backend that
940+
/// gets one stage wrong is not masked by the composition above.
941+
#[test]
942+
fn u32x16_exchange_is_lane_exact_per_granularity() {
943+
let a: [u32; 16] = core::array::from_fn(|i| 10 + i as u32);
944+
let b: [u32; 16] = core::array::from_fn(|i| 30 + i as u32);
945+
let (va, vb) = (U32x16::from_array(a), U32x16::from_array(b));
946+
947+
// lo[c] = a[c] when c & G == 0 else b[c ^ G]
948+
// hi[c] = b[c] when c & G != 0 else a[c ^ G]
949+
for g in [1usize, 2, 4, 8] {
950+
let (lo, hi) = match g {
951+
1 => va.exchange::<1>(vb),
952+
2 => va.exchange::<2>(vb),
953+
4 => va.exchange::<4>(vb),
954+
_ => va.exchange::<8>(vb),
955+
};
956+
let want_lo: [u32; 16] = core::array::from_fn(|c| if c & g == 0 { a[c] } else { b[c ^ g] });
957+
let want_hi: [u32; 16] = core::array::from_fn(|c| if c & g != 0 { b[c] } else { a[c ^ g] });
958+
assert_eq!(lo.to_array(), want_lo, "exchange::<{g}> lo");
959+
assert_eq!(hi.to_array(), want_hi, "exchange::<{g}> hi");
960+
}
961+
}
962+
901963
/// `U32x8`'s shuffle + rotate surface, checked against the REAL x86
902964
/// intrinsics it claims to reproduce.
903965
///

src/simd_avx2.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,38 @@ impl U32x16 {
17151715
}
17161716
Self::from_array(o)
17171717
}
1718+
1719+
/// One butterfly exchange at block granularity `G` elements — the general
1720+
/// form of the whole unpack / lane-exchange family, parameterized by
1721+
/// granularity instead of one method per width.
1722+
///
1723+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
1724+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
1725+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
1726+
/// `G` is a const parameter, so every shuffle pattern is compile-time
1727+
/// constant — the same property a hand-written intrinsic has, and the
1728+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
1729+
///
1730+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
1731+
/// compose a complete 16x16 transpose. Measured as
1732+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
1733+
/// them real shuffles. The same transpose written as one monolithic index
1734+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
1735+
/// scalar copy. The spelling is the entire difference. See
1736+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
1737+
///
1738+
/// No intrinsic override is earned: the generic form does not fail.
1739+
#[inline(always)]
1740+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
1741+
let (l, h) = (self.to_array(), other.to_array());
1742+
let mut nl = [0u32; 16];
1743+
let mut nh = [0u32; 16];
1744+
for c in 0..16 {
1745+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
1746+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
1747+
}
1748+
(Self::from_array(nl), Self::from_array(nh))
1749+
}
17181750
}
17191751

17201752
impl U32x16 {

src/simd_avx512.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,38 @@ impl U32x16 {
15211521
}
15221522
Self::from_array(o)
15231523
}
1524+
1525+
/// One butterfly exchange at block granularity `G` elements — the general
1526+
/// form of the whole unpack / lane-exchange family, parameterized by
1527+
/// granularity instead of one method per width.
1528+
///
1529+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
1530+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
1531+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
1532+
/// `G` is a const parameter, so every shuffle pattern is compile-time
1533+
/// constant — the same property a hand-written intrinsic has, and the
1534+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
1535+
///
1536+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
1537+
/// compose a complete 16x16 transpose. Measured as
1538+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
1539+
/// them real shuffles. The same transpose written as one monolithic index
1540+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
1541+
/// scalar copy. The spelling is the entire difference. See
1542+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
1543+
///
1544+
/// No intrinsic override is earned: the generic form does not fail.
1545+
#[inline(always)]
1546+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
1547+
let (l, h) = (self.to_array(), other.to_array());
1548+
let mut nl = [0u32; 16];
1549+
let mut nh = [0u32; 16];
1550+
for c in 0..16 {
1551+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
1552+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
1553+
}
1554+
(Self::from_array(nl), Self::from_array(nh))
1555+
}
15241556
}
15251557

15261558
impl U32x16 {

src/simd_neon.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,38 @@ impl U32x16 {
17261726
}
17271727
Self::from_array(o)
17281728
}
1729+
1730+
/// One butterfly exchange at block granularity `G` elements — the general
1731+
/// form of the whole unpack / lane-exchange family, parameterized by
1732+
/// granularity instead of one method per width.
1733+
///
1734+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
1735+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
1736+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
1737+
/// `G` is a const parameter, so every shuffle pattern is compile-time
1738+
/// constant — the same property a hand-written intrinsic has, and the
1739+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
1740+
///
1741+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
1742+
/// compose a complete 16x16 transpose. Measured as
1743+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
1744+
/// them real shuffles. The same transpose written as one monolithic index
1745+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
1746+
/// scalar copy. The spelling is the entire difference. See
1747+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
1748+
///
1749+
/// No intrinsic override is earned: the generic form does not fail.
1750+
#[inline(always)]
1751+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
1752+
let (l, h) = (self.to_array(), other.to_array());
1753+
let mut nl = [0u32; 16];
1754+
let mut nh = [0u32; 16];
1755+
for c in 0..16 {
1756+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
1757+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
1758+
}
1759+
(Self::from_array(nl), Self::from_array(nh))
1760+
}
17291761
}
17301762

17311763
#[cfg(target_arch = "aarch64")]

src/simd_nightly/u_word_types.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,38 @@ impl U32x16 {
459459
}
460460
Self::from_array(o)
461461
}
462+
463+
/// One butterfly exchange at block granularity `G` elements — the general
464+
/// form of the whole unpack / lane-exchange family, parameterized by
465+
/// granularity instead of one method per width.
466+
///
467+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
468+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
469+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
470+
/// `G` is a const parameter, so every shuffle pattern is compile-time
471+
/// constant — the same property a hand-written intrinsic has, and the
472+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
473+
///
474+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
475+
/// compose a complete 16x16 transpose. Measured as
476+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
477+
/// them real shuffles. The same transpose written as one monolithic index
478+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
479+
/// scalar copy. The spelling is the entire difference. See
480+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
481+
///
482+
/// No intrinsic override is earned: the generic form does not fail.
483+
#[inline(always)]
484+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
485+
let (l, h) = (self.to_array(), other.to_array());
486+
let mut nl = [0u32; 16];
487+
let mut nh = [0u32; 16];
488+
for c in 0..16 {
489+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
490+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
491+
}
492+
(Self::from_array(nl), Self::from_array(nh))
493+
}
462494
}
463495

464496
impl U32x16 {

src/simd_scalar.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,38 @@ impl U32x16 {
12691269
}
12701270
Self::from_array(o)
12711271
}
1272+
1273+
/// One butterfly exchange at block granularity `G` elements — the general
1274+
/// form of the whole unpack / lane-exchange family, parameterized by
1275+
/// granularity instead of one method per width.
1276+
///
1277+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
1278+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
1279+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
1280+
/// `G` is a const parameter, so every shuffle pattern is compile-time
1281+
/// constant — the same property a hand-written intrinsic has, and the
1282+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
1283+
///
1284+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
1285+
/// compose a complete 16x16 transpose. Measured as
1286+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
1287+
/// them real shuffles. The same transpose written as one monolithic index
1288+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
1289+
/// scalar copy. The spelling is the entire difference. See
1290+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
1291+
///
1292+
/// No intrinsic override is earned: the generic form does not fail.
1293+
#[inline(always)]
1294+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
1295+
let (l, h) = (self.to_array(), other.to_array());
1296+
let mut nl = [0u32; 16];
1297+
let mut nh = [0u32; 16];
1298+
for c in 0..16 {
1299+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
1300+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
1301+
}
1302+
(Self::from_array(nl), Self::from_array(nh))
1303+
}
12721304
}
12731305

12741306
impl U32x16 {

src/simd_wasm.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,38 @@ pub mod wasm32_simd {
10521052
}
10531053
Self::from_array(o)
10541054
}
1055+
1056+
/// One butterfly exchange at block granularity `G` elements — the general
1057+
/// form of the whole unpack / lane-exchange family, parameterized by
1058+
/// granularity instead of one method per width.
1059+
///
1060+
/// `G = 1` is the 32-bit unpack, `G = 2` the 64-bit unpack, `G = 4` the
1061+
/// 128-bit lane exchange, and `G = 8` the 256-bit half exchange that a
1062+
/// 512-bit lane additionally needs and for which no AVX2 intrinsic exists.
1063+
/// `G` is a const parameter, so every shuffle pattern is compile-time
1064+
/// constant — the same property a hand-written intrinsic has, and the
1065+
/// precondition for LLVM to select a shuffle rather than an indexed copy.
1066+
///
1067+
/// Four stages over this (`G` = 1, 2, 4, 8, pairing row `r` with `r | G`)
1068+
/// compose a complete 16x16 transpose. Measured as
1069+
/// `transpose_16x16_composed`: **79 packed / 0 scalar-lane-arith**, 19 of
1070+
/// them real shuffles. The same transpose written as one monolithic index
1071+
/// loop measures **0 packed** — 1088 bytes of stack and a 256-iteration
1072+
/// scalar copy. The spelling is the entire difference. See
1073+
/// `.claude/knowledge/blake3-on-ndarray-simd.md`.
1074+
///
1075+
/// No intrinsic override is earned: the generic form does not fail.
1076+
#[inline(always)]
1077+
pub fn exchange<const G: usize>(self, other: Self) -> (Self, Self) {
1078+
let (l, h) = (self.to_array(), other.to_array());
1079+
let mut nl = [0u32; 16];
1080+
let mut nh = [0u32; 16];
1081+
for c in 0..16 {
1082+
nl[c] = if c & G == 0 { l[c] } else { h[c ^ G] };
1083+
nh[c] = if c & G != 0 { h[c] } else { l[c ^ G] };
1084+
}
1085+
(Self::from_array(nl), Self::from_array(nh))
1086+
}
10551087
}
10561088

10571089
impl U32x16 {

0 commit comments

Comments
 (0)