Skip to content

Commit ba2db74

Browse files
committed
simd(W1a-#9): U64x8/U32x16 andnot + ternlog — the masking primitives
Adds the two operators the polyfill lacked for mask composition: set difference, and any 3-input boolean function selected by a truth-table immediate. No new type — U64x8 (8 x u64 = 512 bits) and U32x16 already exist in every backend and are re-exported from all six arms of simd.rs, so simd.rs needs no dispatch change; the methods ride along. Why these two: FieldMask-style bitsets had AND/OR/XOR and no and-not, which is the operator 'stack this prerequisite, exclude that one' needs, and no three-input form at all. Stacking N prerequisite masks cost N-1 ops; with ternlog it costs ceil((N-1)/2), and on AVX-512 each of those is one instruction over 512 positions regardless of how many bits are set. Semantics, identical on every backend: a.andnot(b) = a & !b -- NOTE the argument order differs from the raw intrinsic: _mm*_andnot_si*(a, b) computes !a & b. Documented at every definition; the AVX-512 path swaps its arguments accordingly. a.ternlog::<IMM>(b, c): per bit, index = (a<<2)|(b<<1)|c, result bit = (IMM >> index) & 1 (Intel VPTERNLOG convention). IMM is i32 to match the intrinsic; 0..=255 legal, enforced at compile time by the intrinsic's own static assert. Named immediates (AND3, AND2_ANDNOT, OR2_AND, MAJ3, ...) in the scalar backend's ternlog module. Total functions: no saturation, no overflow, no UB, no lane interaction. Backends. AVX-512 uses the native intrinsics; that module compiles only under a global target_feature = avx512f, which is the guard -- no added CPU check, no runtime detection. AVX2/NEON/wasm/scalar share ONE portable body: an element-wise loop over the repr(align(64)) backing array, the same idiom this file's existing BitAnd/BitOr/BitXor use. That is not a scalar fallback -- measured codegen below. Measured codegen (examples/w1a9_codegen_probe.rs, black_box'd inputs, release): v4 (config-avx512.toml): ternlog::<0x80> -> vpternlogq /bin/bashx80,%zmm2,%zmm1,%zmm0 (1 insn) ternlog::<0x40> -> vpternlogq /bin/bashx40,%zmm2,%zmm1,%zmm0 (1 insn) andnot -> vandnps %zmm0,%zmm1,%zmm0 (1 insn) v3 default (.cargo/config.toml): ternlog::<0x80> -> 2 x [vmovaps ymm; vandps; vandps; vmovaps] aligned moves, 512 bits in 8 insns The portable body auto-vectorises to real ymm work; repr(align(64)) is what earns the aligned vmovaps. Matches the storage documented for the HSW/ARL profiles in .claude/knowledge/agnostic-surface-cpu-matrix.md. Tests (5, at the simd.rs facade so they exercise whichever backend the build selected): all 256 immediates against an independent bit-by-bit truth-table reference; andnot direction with an anti-vacuity assertion that the self-minus-other vs not-self-and-other distinction is actually observable on the corpus; named-immediate meanings with a pairwise distinctness check so aliasing cannot pass; agreement with the existing BitAnd/BitOr/BitXor operators; the 32-bit-lane sibling. Fixed-seed SplitMix64 corpus with edge cases (0, MAX, 0x5555.., 0xAAAA..), no dev-dependency added. Verification: lib suite 2207 passed / 0 failed on the v3 arm; cargo check clean on the v4 arm; fmt clean; clippy adds no finding (3 pre-existing warnings remain in property_mask.rs / bitwise.rs / palette_codec.rs, untouched here). Known gap, stated rather than papered over: the v4 arm's *test* build is broken on main independently of this change (15 errors on a clean tree, missing I8x16/U64x8/U16x8 types in unrelated test modules), so the parity tests could not be RUN under AVX-512 -- only compiled. The native path is verified by the disassembly above, not by a test execution. Consumer site: AdaWorldAPI/lance-graph crates/lance-graph-contract/src/ class_view.rs -- FieldMask/WideFieldMask carry intersect/union/is_disjoint and lack difference/is_subset_of (D-MAR-1); and graph/blasgraph/ typed_graph.rs masked_traverse, which filters a materialised result per-entry instead of masking during the operation. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
1 parent 4612014 commit ba2db74

5 files changed

Lines changed: 598 additions & 0 deletions

File tree

examples/w1a9_codegen_probe.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use ndarray::simd::U64x8;
2+
use std::hint::black_box;
3+
4+
#[inline(never)]
5+
pub fn probe_and3(a: U64x8, b: U64x8, c: U64x8) -> U64x8 {
6+
a.ternlog::<0x80>(b, c)
7+
}
8+
9+
#[inline(never)]
10+
pub fn probe_and2_andnot(a: U64x8, b: U64x8, c: U64x8) -> U64x8 {
11+
a.ternlog::<0x40>(b, c)
12+
}
13+
14+
#[inline(never)]
15+
pub fn probe_andnot(a: U64x8, b: U64x8) -> U64x8 {
16+
a.andnot(b)
17+
}
18+
19+
fn main() {
20+
let a = black_box(U64x8::splat(0xF0F0_F0F0_F0F0_F0F0));
21+
let b = black_box(U64x8::splat(0xCCCC_CCCC_CCCC_CCCC));
22+
let c = black_box(U64x8::splat(0xAAAA_AAAA_AAAA_AAAA));
23+
println!("{:x}", black_box(probe_and3(a, b, c)).to_array()[0]);
24+
println!("{:x}", black_box(probe_and2_andnot(a, b, c)).to_array()[0]);
25+
println!("{:x}", black_box(probe_andnot(a, b)).to_array()[0]);
26+
}

src/simd.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,4 +1266,220 @@ mod tests {
12661266
assert!(v.is_finite(), "exp(200) must saturate, got {}", v);
12671267
}
12681268
}
1269+
1270+
// ── W1a-#9 parity: U64x8/U32x16 andnot + ternlog ────────────────────────
1271+
//
1272+
// The backends are compile-time exclusive, so "all three agree" is proven
1273+
// by asserting the ACTIVE backend against an independent scalar reference
1274+
// computed inline here, then running this suite under each cargo config
1275+
// (.cargo/config.toml = AVX2, config-avx512.toml = AVX-512, and the
1276+
// aarch64/wasm configs, which resolve U64x8 to the scalar backend).
1277+
// The reference below is written from the Intel truth-table definition,
1278+
// NOT by calling the primitive it checks.
1279+
1280+
/// SplitMix64 — fixed seed, deterministic corpus (no dev-dependency).
1281+
fn splitmix64(state: &mut u64) -> u64 {
1282+
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
1283+
let mut z = *state;
1284+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1285+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1286+
z ^ (z >> 31)
1287+
}
1288+
1289+
/// Independent reference: bit i of the result is `(imm >> idx) & 1` where
1290+
/// `idx = (a_i << 2) | (b_i << 1) | c_i`. Computed bit-by-bit on purpose —
1291+
/// deliberately NOT the same shape as the implementation.
1292+
fn ref_ternlog_u64(a: u64, b: u64, c: u64, imm: i32) -> u64 {
1293+
let mut out = 0u64;
1294+
for bit in 0..64 {
1295+
let ab = (a >> bit) & 1;
1296+
let bb = (b >> bit) & 1;
1297+
let cb = (c >> bit) & 1;
1298+
let idx = (ab << 2) | (bb << 1) | cb;
1299+
if (imm as u64 >> idx) & 1 == 1 {
1300+
out |= 1u64 << bit;
1301+
}
1302+
}
1303+
out
1304+
}
1305+
1306+
/// Corpus: edge cases first, then a fixed-seed random tail. 8 lanes each.
1307+
fn corpus_u64(n: usize) -> Vec<[u64; 8]> {
1308+
let mut v: Vec<[u64; 8]> = vec![
1309+
[0u64; 8],
1310+
[u64::MAX; 8],
1311+
[0x5555_5555_5555_5555; 8],
1312+
[0xAAAA_AAAA_AAAA_AAAA; 8],
1313+
[1, 0, u64::MAX, 0x8000_0000_0000_0000, 0xFFFF_FFFF, 0, 1 << 63, 7],
1314+
];
1315+
let mut st = 0x0DDB_1A5E_5EED_1234u64;
1316+
while v.len() < n {
1317+
let mut lanes = [0u64; 8];
1318+
for l in lanes.iter_mut() {
1319+
*l = splitmix64(&mut st);
1320+
}
1321+
v.push(lanes);
1322+
}
1323+
v
1324+
}
1325+
1326+
/// G1 — `andnot` is set difference in the documented direction, and it is
1327+
/// NOT the raw intrinsic's `!a & b`. The anti-half is the asymmetry
1328+
/// assertion: a constant-zero or argument-swapped implementation fails.
1329+
#[test]
1330+
fn w1a9_andnot_is_self_minus_other_u64x8() {
1331+
let corpus = corpus_u64(40);
1332+
let mut asymmetric_seen = 0usize;
1333+
for w in corpus.windows(2) {
1334+
let (a, b) = (w[0], w[1]);
1335+
let got = U64x8::from_array(a).andnot(U64x8::from_array(b)).to_array();
1336+
for i in 0..8 {
1337+
assert_eq!(got[i], a[i] & !b[i], "andnot lane {i}: {a:?} \\ {b:?}");
1338+
}
1339+
// Direction check: `self & !other` differs from `!self & other`
1340+
// whenever the two masks are not equal-and-symmetric.
1341+
let swapped: Vec<u64> = (0..8).map(|i| !a[i] & b[i]).collect();
1342+
if (0..8).any(|i| got[i] != swapped[i]) {
1343+
asymmetric_seen += 1;
1344+
}
1345+
}
1346+
// Anti-vacuity: the direction must actually be observable on this
1347+
// corpus, or the test above proves nothing about argument order.
1348+
assert!(
1349+
asymmetric_seen * 3 > corpus.len(),
1350+
"andnot direction is unobservable on this corpus ({asymmetric_seen} asymmetric)"
1351+
);
1352+
// Identities.
1353+
let x = U64x8::from_array(corpus[4]);
1354+
assert!(x.andnot(x).to_array().iter().all(|&v| v == 0));
1355+
assert_eq!(x.andnot(U64x8::splat(0)).to_array(), corpus[4]);
1356+
}
1357+
1358+
/// G2 — `ternlog` matches the independent truth-table reference for ALL
1359+
/// 256 immediates over the whole corpus. Any collapsed arm, wrong index
1360+
/// order, or dropped term fails.
1361+
#[test]
1362+
fn w1a9_ternlog_matches_truth_table_reference_all_256_imms() {
1363+
let corpus = corpus_u64(24);
1364+
// A hand-written subset of immediates is not enough — sweep all 256
1365+
// via a macro-expanded const, since IMM is a const generic.
1366+
macro_rules! sweep {
1367+
($($imm:literal),* $(,)?) => {$({
1368+
for w in corpus.windows(3) {
1369+
let (a, b, c) = (w[0], w[1], w[2]);
1370+
let got = U64x8::from_array(a)
1371+
.ternlog::<$imm>(U64x8::from_array(b), U64x8::from_array(c))
1372+
.to_array();
1373+
for i in 0..8 {
1374+
assert_eq!(
1375+
got[i],
1376+
ref_ternlog_u64(a[i], b[i], c[i], $imm),
1377+
"ternlog imm={} lane={}", $imm, i
1378+
);
1379+
}
1380+
}
1381+
})*};
1382+
}
1383+
// All 256 truth tables.
1384+
sweep!(
1385+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
1386+
29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
1387+
56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
1388+
83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
1389+
108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128,
1390+
129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
1391+
150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170,
1392+
171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191,
1393+
192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212,
1394+
213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
1395+
234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
1396+
255
1397+
);
1398+
}
1399+
1400+
/// G3 — the named truth-table constants mean what their docs say, and each
1401+
/// one is distinguishable from the others on real input (anti-vacuity: a
1402+
/// table of aliases would pass a weaker test).
1403+
#[test]
1404+
fn w1a9_named_immediates_have_their_documented_meaning() {
1405+
let a = U64x8::from_array([0xF0F0_F0F0_F0F0_F0F0; 8]);
1406+
let b = U64x8::from_array([0xCCCC_CCCC_CCCC_CCCC; 8]);
1407+
let c = U64x8::from_array([0xAAAA_AAAA_AAAA_AAAA; 8]);
1408+
let (av, bv, cv) = (0xF0F0_F0F0_F0F0_F0F0u64, 0xCCCC_CCCC_CCCC_CCCCu64, 0xAAAA_AAAA_AAAA_AAAAu64);
1409+
1410+
let and3 = a.ternlog::<0x80>(b, c).to_array()[0];
1411+
assert_eq!(and3, av & bv & cv, "AND3");
1412+
let and2_andnot = a.ternlog::<0x40>(b, c).to_array()[0];
1413+
assert_eq!(and2_andnot, av & bv & !cv, "AND2_ANDNOT");
1414+
let and_andnot2 = a.ternlog::<0x10>(b, c).to_array()[0];
1415+
assert_eq!(and_andnot2, av & !bv & !cv, "AND_ANDNOT2");
1416+
let or2_and = a.ternlog::<0xA8>(b, c).to_array()[0];
1417+
assert_eq!(or2_and, (av | bv) & cv, "OR2_AND");
1418+
let xor3 = a.ternlog::<0x96>(b, c).to_array()[0];
1419+
assert_eq!(xor3, av ^ bv ^ cv, "XOR3");
1420+
let maj3 = a.ternlog::<0xE8>(b, c).to_array()[0];
1421+
assert_eq!(maj3, (av & bv) | (av & cv) | (bv & cv), "MAJ3");
1422+
let and2 = a.ternlog::<0xC0>(b, c).to_array()[0];
1423+
assert_eq!(and2, av & bv, "AND2 (c ignored)");
1424+
let or3 = a.ternlog::<0xFE>(b, c).to_array()[0];
1425+
assert_eq!(or3, av | bv | cv, "OR3");
1426+
1427+
// Anti-vacuity: all eight are pairwise distinct on this input, so the
1428+
// assertions above cannot be passing by aliasing.
1429+
let all = [and3, and2_andnot, and_andnot2, or2_and, xor3, maj3, and2, or3];
1430+
for i in 0..all.len() {
1431+
for j in (i + 1)..all.len() {
1432+
assert_ne!(all[i], all[j], "named immediates {i} and {j} alias");
1433+
}
1434+
}
1435+
}
1436+
1437+
/// G4 — `ternlog::<0xC0>` (AND2, c ignored) equals plain `BitAnd`, and
1438+
/// `ternlog` composed with `andnot` agrees with the two-step form. This is
1439+
/// the bridge assertion: the new three-input primitive must not disagree
1440+
/// with the operators already shipping on these types.
1441+
#[test]
1442+
fn w1a9_ternlog_agrees_with_existing_operators() {
1443+
let corpus = corpus_u64(30);
1444+
for w in corpus.windows(3) {
1445+
let (a, b, c) = (U64x8::from_array(w[0]), U64x8::from_array(w[1]), U64x8::from_array(w[2]));
1446+
assert_eq!(a.ternlog::<0xC0>(b, c).to_array(), (a & b).to_array(), "AND2 vs BitAnd");
1447+
assert_eq!(a.ternlog::<0xFE>(b, c).to_array(), (a | b | c).to_array(), "OR3 vs BitOr");
1448+
assert_eq!(a.ternlog::<0x96>(b, c).to_array(), (a ^ b ^ c).to_array(), "XOR3 vs BitXor");
1449+
// Three-layer stack: (a & b) \ c, one instruction vs two steps.
1450+
assert_eq!(
1451+
a.ternlog::<0x40>(b, c).to_array(),
1452+
(a & b).andnot(c).to_array(),
1453+
"AND2_ANDNOT vs (a & b).andnot(c)"
1454+
);
1455+
}
1456+
}
1457+
1458+
/// G5 — the 32-bit-lane sibling carries the same semantics.
1459+
#[test]
1460+
fn w1a9_u32x16_andnot_and_ternlog() {
1461+
let mut st = 0xC0FF_EE00_1234_5678u64;
1462+
for _ in 0..40 {
1463+
let mut a = [0u32; 16];
1464+
let mut b = [0u32; 16];
1465+
let mut c = [0u32; 16];
1466+
for i in 0..16 {
1467+
a[i] = splitmix64(&mut st) as u32;
1468+
b[i] = splitmix64(&mut st) as u32;
1469+
c[i] = splitmix64(&mut st) as u32;
1470+
}
1471+
let got = U32x16::from_array(a)
1472+
.andnot(U32x16::from_array(b))
1473+
.to_array();
1474+
for i in 0..16 {
1475+
assert_eq!(got[i], a[i] & !b[i], "u32 andnot lane {i}");
1476+
}
1477+
let t = U32x16::from_array(a)
1478+
.ternlog::<0x40>(U32x16::from_array(b), U32x16::from_array(c))
1479+
.to_array();
1480+
for i in 0..16 {
1481+
assert_eq!(t[i], a[i] & b[i] & !c[i], "u32 ternlog lane {i}");
1482+
}
1483+
}
1484+
}
12691485
}

src/simd_avx2.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,3 +3500,135 @@ mod f16_precision_tests {
35003500
}
35013501
}
35023502
}
3503+
3504+
// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (AVX2 backend) ───────────────
3505+
//
3506+
// Written in the same idiom as this file's `avx2_int_type!` bitwise operators:
3507+
// an element-wise loop over the `#[repr(align(64))]` backing array, no
3508+
// intrinsics and no `unsafe`. Under this crate's x86-64-v3 baseline LLVM
3509+
// auto-vectorises these to `vpand`/`vpandn`/`vpor` on `ymm` — the alignment
3510+
// attribute is what lets it emit aligned moves. The same source lowers to
3511+
// `vandq_u64`/`vbicq_u64` on NEON and `v128_and`/`v128_andnot` on wasm, which
3512+
// is why one portable body serves all three of those profiles.
3513+
3514+
/// Evaluate a ternlog truth table for one lane. `imm` is a compile-time
3515+
/// constant at every call site, so only the minterms the table selects survive
3516+
/// const-folding — `AND3` (0x80) reduces to two ANDs.
3517+
#[inline(always)]
3518+
const fn ternlog_lane_u64_avx2(a: u64, b: u64, c: u64, imm: i32) -> u64 {
3519+
let mut r = 0u64;
3520+
if imm & 0x01 != 0 {
3521+
r |= !a & !b & !c;
3522+
}
3523+
if imm & 0x02 != 0 {
3524+
r |= !a & !b & c;
3525+
}
3526+
if imm & 0x04 != 0 {
3527+
r |= !a & b & !c;
3528+
}
3529+
if imm & 0x08 != 0 {
3530+
r |= !a & b & c;
3531+
}
3532+
if imm & 0x10 != 0 {
3533+
r |= a & !b & !c;
3534+
}
3535+
if imm & 0x20 != 0 {
3536+
r |= a & !b & c;
3537+
}
3538+
if imm & 0x40 != 0 {
3539+
r |= a & b & !c;
3540+
}
3541+
if imm & 0x80 != 0 {
3542+
r |= a & b & c;
3543+
}
3544+
r
3545+
}
3546+
3547+
#[inline(always)]
3548+
const fn ternlog_lane_u32_avx2(a: u32, b: u32, c: u32, imm: i32) -> u32 {
3549+
let mut r = 0u32;
3550+
if imm & 0x01 != 0 {
3551+
r |= !a & !b & !c;
3552+
}
3553+
if imm & 0x02 != 0 {
3554+
r |= !a & !b & c;
3555+
}
3556+
if imm & 0x04 != 0 {
3557+
r |= !a & b & !c;
3558+
}
3559+
if imm & 0x08 != 0 {
3560+
r |= !a & b & c;
3561+
}
3562+
if imm & 0x10 != 0 {
3563+
r |= a & !b & !c;
3564+
}
3565+
if imm & 0x20 != 0 {
3566+
r |= a & !b & c;
3567+
}
3568+
if imm & 0x40 != 0 {
3569+
r |= a & b & !c;
3570+
}
3571+
if imm & 0x80 != 0 {
3572+
r |= a & b & c;
3573+
}
3574+
r
3575+
}
3576+
3577+
impl U64x8 {
3578+
/// Set difference: `self & !other`, lane-wise.
3579+
///
3580+
/// **Argument order differs from the raw Intel intrinsic.**
3581+
/// `_mm256_andnot_si256(a, b)` computes `!a & b`; this method computes
3582+
/// `self & !other` — "self minus other". Every backend implements this
3583+
/// same direction.
3584+
///
3585+
/// Total function: no saturation, no overflow, no UB. `x.andnot(x)` is
3586+
/// zero; `x.andnot(U64x8::splat(0))` is `x`.
3587+
#[inline(always)]
3588+
pub fn andnot(self, other: Self) -> Self {
3589+
let mut o = [0u64; 8];
3590+
for i in 0..8 {
3591+
o[i] = self.0[i] & !other.0[i];
3592+
}
3593+
Self(o)
3594+
}
3595+
3596+
/// Any 3-input boolean function of `self`, `b` and `c`, selected by the
3597+
/// const truth-table immediate `IMM`.
3598+
///
3599+
/// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit =
3600+
/// `(IMM >> index) & 1` — Intel's VPTERNLOG convention, matched exactly by
3601+
/// every backend. `IMM` is `i32` to mirror the intrinsic's signature; only
3602+
/// `0..=255` is legal and the AVX-512 backend rejects wider values at
3603+
/// compile time. Within that domain: total function, no lane interaction.
3604+
#[inline(always)]
3605+
pub fn ternlog<const IMM: i32>(self, b: Self, c: Self) -> Self {
3606+
let mut o = [0u64; 8];
3607+
for i in 0..8 {
3608+
o[i] = ternlog_lane_u64_avx2(self.0[i], b.0[i], c.0[i], IMM);
3609+
}
3610+
Self(o)
3611+
}
3612+
}
3613+
3614+
impl U32x16 {
3615+
/// Set difference: `self & !other`, lane-wise. See [`U64x8::andnot`].
3616+
#[inline(always)]
3617+
pub fn andnot(self, other: Self) -> Self {
3618+
let mut o = [0u32; 16];
3619+
for i in 0..16 {
3620+
o[i] = self.0[i] & !other.0[i];
3621+
}
3622+
Self(o)
3623+
}
3624+
3625+
/// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`].
3626+
#[inline(always)]
3627+
pub fn ternlog<const IMM: i32>(self, b: Self, c: Self) -> Self {
3628+
let mut o = [0u32; 16];
3629+
for i in 0..16 {
3630+
o[i] = ternlog_lane_u32_avx2(self.0[i], b.0[i], c.0[i], IMM);
3631+
}
3632+
Self(o)
3633+
}
3634+
}

0 commit comments

Comments
 (0)