use super::ops::mul_log16;
use super::{LeopardGf16Tables, ORDER16};
pub(super) const SIMD_MIN_LEN: usize = 16;
const HAS_SIMD_KERNEL: bool = cfg!(all(
any(
all(feature = "std", target_arch = "x86_64"),
target_arch = "aarch64"
),
target_endian = "little"
));
pub(super) struct NibbleTables16 {
pub(super) lo: [[u8; 16]; 4],
pub(super) hi: [[u8; 16]; 4],
}
impl NibbleTables16 {
pub(super) fn build(
log_m: u16,
log_lut: &[u16; ORDER16],
exp_lut: &[u16; ORDER16 * 2],
) -> Self {
let mut lo = [[0u8; 16]; 4];
let mut hi = [[0u8; 16]; 4];
for i in 0..4 {
let shift = 4 * i as u32;
for nib in 0u16..16 {
let v = mul_log16(nib << shift, log_m, log_lut, exp_lut);
lo[i][nib as usize] = v as u8;
hi[i][nib as usize] = (v >> 8) as u8;
}
}
NibbleTables16 { lo, hi }
}
}
pub(super) fn mulgf16_tabled_scalar<const XOR: bool>(
out: &mut [u16],
input: &[u16],
t: &NibbleTables16,
) {
for (o, &v) in out.iter_mut().zip(input.iter()) {
let n0 = (v & 0xF) as usize;
let n1 = ((v >> 4) & 0xF) as usize;
let n2 = ((v >> 8) & 0xF) as usize;
let n3 = ((v >> 12) & 0xF) as usize;
let lo = t.lo[0][n0] ^ t.lo[1][n1] ^ t.lo[2][n2] ^ t.lo[3][n3];
let hi = t.hi[0][n0] ^ t.hi[1][n1] ^ t.hi[2][n2] ^ t.hi[3][n3];
let res = (lo as u16) | ((hi as u16) << 8);
if XOR {
*o ^= res;
} else {
*o = res;
}
}
}
pub(super) fn mulgf16_simd<const XOR: bool>(
out: &mut [u16],
input: &[u16],
log_m: u16,
tables: &LeopardGf16Tables,
) {
let t = NibbleTables16::build(log_m, &tables.log_lut, &tables.exp_lut);
#[cfg(all(feature = "std", target_arch = "x86_64", target_endian = "little"))]
{
if is_x86_feature_detected!("avx2") {
unsafe {
x86::mulgf16_avx2::<XOR>(out, input, &t);
}
return;
}
if is_x86_feature_detected!("ssse3") {
unsafe {
x86::mulgf16_ssse3::<XOR>(out, input, &t);
}
return;
}
}
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
unsafe {
aarch64::mulgf16_neon::<XOR>(out, input, &t);
}
#[cfg(not(all(target_arch = "aarch64", target_endian = "little")))]
mulgf16_tabled_scalar::<XOR>(out, input, &t);
}
#[inline]
pub(super) fn should_use_simd(len: usize) -> bool {
len >= SIMD_MIN_LEN && HAS_SIMD_KERNEL
}
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
mod aarch64 {
use super::{NibbleTables16, mulgf16_tabled_scalar};
use core::arch::aarch64::{
vandq_u8, vdupq_n_u8, veorq_u8, vld1q_u8, vqtbl1q_u8, vshrq_n_u8, vst1q_u8, vuzp1q_u8,
vuzp2q_u8, vzip1q_u8, vzip2q_u8,
};
#[target_feature(enable = "neon")]
pub(super) unsafe fn mulgf16_neon<const XOR: bool>(
out: &mut [u16],
input: &[u16],
t: &NibbleTables16,
) {
unsafe {
let lo0 = vld1q_u8(t.lo[0].as_ptr());
let lo1 = vld1q_u8(t.lo[1].as_ptr());
let lo2 = vld1q_u8(t.lo[2].as_ptr());
let lo3 = vld1q_u8(t.lo[3].as_ptr());
let hi0 = vld1q_u8(t.hi[0].as_ptr());
let hi1 = vld1q_u8(t.hi[1].as_ptr());
let hi2 = vld1q_u8(t.hi[2].as_ptr());
let hi3 = vld1q_u8(t.hi[3].as_ptr());
let mask = vdupq_n_u8(0x0f);
let n = input.len();
let chunks = n / 16;
let in_ptr = input.as_ptr().cast::<u8>();
let out_ptr = out.as_mut_ptr().cast::<u8>();
for c in 0..chunks {
let off = c * 32; let p0 = vld1q_u8(in_ptr.add(off));
let p1 = vld1q_u8(in_ptr.add(off + 16));
let plane_lo = vuzp1q_u8(p0, p1);
let plane_hi = vuzp2q_u8(p0, p1);
let n0 = vandq_u8(plane_lo, mask);
let n1 = vshrq_n_u8::<4>(plane_lo);
let n2 = vandq_u8(plane_hi, mask);
let n3 = vshrq_n_u8::<4>(plane_hi);
let out_lo = veorq_u8(
veorq_u8(vqtbl1q_u8(lo0, n0), vqtbl1q_u8(lo1, n1)),
veorq_u8(vqtbl1q_u8(lo2, n2), vqtbl1q_u8(lo3, n3)),
);
let out_hi = veorq_u8(
veorq_u8(vqtbl1q_u8(hi0, n0), vqtbl1q_u8(hi1, n1)),
veorq_u8(vqtbl1q_u8(hi2, n2), vqtbl1q_u8(hi3, n3)),
);
let mut r0 = vzip1q_u8(out_lo, out_hi);
let mut r1 = vzip2q_u8(out_lo, out_hi);
if XOR {
r0 = veorq_u8(r0, vld1q_u8(out_ptr.add(off)));
r1 = veorq_u8(r1, vld1q_u8(out_ptr.add(off + 16)));
}
vst1q_u8(out_ptr.add(off), r0);
vst1q_u8(out_ptr.add(off + 16), r1);
}
let done = chunks * 16;
if done < n {
mulgf16_tabled_scalar::<XOR>(&mut out[done..], &input[done..], t);
}
}
}
}
#[cfg(all(feature = "std", target_arch = "x86_64", target_endian = "little"))]
mod x86 {
use super::{NibbleTables16, mulgf16_tabled_scalar};
use core::arch::x86_64::{
__m128i, __m256i, _mm_and_si128, _mm_loadu_si128, _mm_packus_epi16, _mm_set1_epi8,
_mm_shuffle_epi8, _mm_srli_epi16, _mm_storeu_si128, _mm_unpackhi_epi8, _mm_unpacklo_epi8,
_mm_xor_si128, _mm256_and_si256, _mm256_broadcastsi128_si256, _mm256_loadu_si256,
_mm256_packus_epi16, _mm256_permute2x128_si256, _mm256_permute4x64_epi64, _mm256_set1_epi8,
_mm256_shuffle_epi8, _mm256_srli_epi16, _mm256_storeu_si256, _mm256_unpackhi_epi8,
_mm256_unpacklo_epi8, _mm256_xor_si256,
};
#[target_feature(enable = "ssse3")]
pub(super) unsafe fn mulgf16_ssse3<const XOR: bool>(
out: &mut [u16],
input: &[u16],
t: &NibbleTables16,
) {
unsafe {
let lo0 = _mm_loadu_si128(t.lo[0].as_ptr().cast());
let lo1 = _mm_loadu_si128(t.lo[1].as_ptr().cast());
let lo2 = _mm_loadu_si128(t.lo[2].as_ptr().cast());
let lo3 = _mm_loadu_si128(t.lo[3].as_ptr().cast());
let hi0 = _mm_loadu_si128(t.hi[0].as_ptr().cast());
let hi1 = _mm_loadu_si128(t.hi[1].as_ptr().cast());
let hi2 = _mm_loadu_si128(t.hi[2].as_ptr().cast());
let hi3 = _mm_loadu_si128(t.hi[3].as_ptr().cast());
let mask: __m128i = _mm_set1_epi8(0x0f);
let mask_lo: __m128i = _mm_srli_epi16::<8>(_mm_set1_epi8(-1i8));
let n = input.len();
let chunks = n / 16;
let in_ptr = input.as_ptr().cast::<u8>();
let out_ptr = out.as_mut_ptr().cast::<u8>();
for c in 0..chunks {
let off = c * 32;
let p0 = _mm_loadu_si128(in_ptr.add(off).cast());
let p1 = _mm_loadu_si128(in_ptr.add(off + 16).cast());
let plane_lo =
_mm_packus_epi16(_mm_and_si128(p0, mask_lo), _mm_and_si128(p1, mask_lo));
let plane_hi = _mm_packus_epi16(_mm_srli_epi16::<8>(p0), _mm_srli_epi16::<8>(p1));
let n0 = _mm_and_si128(plane_lo, mask);
let n1 = _mm_and_si128(_mm_srli_epi16::<4>(plane_lo), mask);
let n2 = _mm_and_si128(plane_hi, mask);
let n3 = _mm_and_si128(_mm_srli_epi16::<4>(plane_hi), mask);
let out_lo = _mm_xor_si128(
_mm_xor_si128(_mm_shuffle_epi8(lo0, n0), _mm_shuffle_epi8(lo1, n1)),
_mm_xor_si128(_mm_shuffle_epi8(lo2, n2), _mm_shuffle_epi8(lo3, n3)),
);
let out_hi = _mm_xor_si128(
_mm_xor_si128(_mm_shuffle_epi8(hi0, n0), _mm_shuffle_epi8(hi1, n1)),
_mm_xor_si128(_mm_shuffle_epi8(hi2, n2), _mm_shuffle_epi8(hi3, n3)),
);
let mut r0 = _mm_unpacklo_epi8(out_lo, out_hi);
let mut r1 = _mm_unpackhi_epi8(out_lo, out_hi);
if XOR {
r0 = _mm_xor_si128(r0, _mm_loadu_si128(out_ptr.add(off).cast()));
r1 = _mm_xor_si128(r1, _mm_loadu_si128(out_ptr.add(off + 16).cast()));
}
_mm_storeu_si128(out_ptr.add(off).cast(), r0);
_mm_storeu_si128(out_ptr.add(off + 16).cast(), r1);
}
let done = chunks * 16;
if done < n {
mulgf16_tabled_scalar::<XOR>(&mut out[done..], &input[done..], t);
}
}
}
#[target_feature(enable = "avx2")]
pub(super) unsafe fn mulgf16_avx2<const XOR: bool>(
out: &mut [u16],
input: &[u16],
t: &NibbleTables16,
) {
unsafe {
let lo0 = bcast(t.lo[0].as_ptr());
let lo1 = bcast(t.lo[1].as_ptr());
let lo2 = bcast(t.lo[2].as_ptr());
let lo3 = bcast(t.lo[3].as_ptr());
let hi0 = bcast(t.hi[0].as_ptr());
let hi1 = bcast(t.hi[1].as_ptr());
let hi2 = bcast(t.hi[2].as_ptr());
let hi3 = bcast(t.hi[3].as_ptr());
let mask: __m256i = _mm256_set1_epi8(0x0f);
let mask_lo: __m256i = _mm256_srli_epi16::<8>(_mm256_set1_epi8(-1i8));
let n = input.len();
let chunks = n / 32;
let in_ptr = input.as_ptr().cast::<u8>();
let out_ptr = out.as_mut_ptr().cast::<u8>();
for c in 0..chunks {
let off = c * 64; let a = _mm256_loadu_si256(in_ptr.add(off).cast());
let b = _mm256_loadu_si256(in_ptr.add(off + 32).cast());
let plane_lo = _mm256_permute4x64_epi64::<0xD8>(_mm256_packus_epi16(
_mm256_and_si256(a, mask_lo),
_mm256_and_si256(b, mask_lo),
));
let plane_hi = _mm256_permute4x64_epi64::<0xD8>(_mm256_packus_epi16(
_mm256_srli_epi16::<8>(a),
_mm256_srli_epi16::<8>(b),
));
let n0 = _mm256_and_si256(plane_lo, mask);
let n1 = _mm256_and_si256(_mm256_srli_epi16::<4>(plane_lo), mask);
let n2 = _mm256_and_si256(plane_hi, mask);
let n3 = _mm256_and_si256(_mm256_srli_epi16::<4>(plane_hi), mask);
let out_lo = _mm256_xor_si256(
_mm256_xor_si256(_mm256_shuffle_epi8(lo0, n0), _mm256_shuffle_epi8(lo1, n1)),
_mm256_xor_si256(_mm256_shuffle_epi8(lo2, n2), _mm256_shuffle_epi8(lo3, n3)),
);
let out_hi = _mm256_xor_si256(
_mm256_xor_si256(_mm256_shuffle_epi8(hi0, n0), _mm256_shuffle_epi8(hi1, n1)),
_mm256_xor_si256(_mm256_shuffle_epi8(hi2, n2), _mm256_shuffle_epi8(hi3, n3)),
);
let r_lo = _mm256_unpacklo_epi8(out_lo, out_hi);
let r_hi = _mm256_unpackhi_epi8(out_lo, out_hi);
let mut o0 = _mm256_permute2x128_si256::<0x20>(r_lo, r_hi); let mut o1 = _mm256_permute2x128_si256::<0x31>(r_lo, r_hi); if XOR {
o0 = _mm256_xor_si256(o0, _mm256_loadu_si256(out_ptr.add(off).cast()));
o1 = _mm256_xor_si256(o1, _mm256_loadu_si256(out_ptr.add(off + 32).cast()));
}
_mm256_storeu_si256(out_ptr.add(off).cast(), o0);
_mm256_storeu_si256(out_ptr.add(off + 32).cast(), o1);
}
let done = chunks * 32;
if done < n {
mulgf16_ssse3::<XOR>(&mut out[done..], &input[done..], t);
}
}
}
#[target_feature(enable = "avx2")]
unsafe fn bcast(ptr: *const u8) -> __m256i {
unsafe { _mm256_broadcastsi128_si256(_mm_loadu_si128(ptr.cast())) }
}
}
#[cfg(test)]
mod mul_simd_tests {
extern crate alloc;
use super::super::ops::mul_log16;
use super::super::tables::build_tables16;
use super::*;
use alloc::vec;
use alloc::vec::Vec;
const LENGTHS: [usize; 15] = [0, 1, 5, 16, 31, 32, 33, 48, 63, 64, 65, 80, 96, 1000, 32768];
fn ref_mul(out: &mut [u16], input: &[u16], log_m: u16, tables: &LeopardGf16Tables, xor: bool) {
for (o, &v) in out.iter_mut().zip(input.iter()) {
let p = if log_m == super::super::MODULUS16 as u16 {
v
} else {
mul_log16(v, log_m, &tables.log_lut, &tables.exp_lut)
};
if xor {
*o ^= p;
} else {
*o = p;
}
}
}
fn run_mul<const XOR: bool>(
out: &mut [u16],
input: &[u16],
log_m: u16,
tables: &LeopardGf16Tables,
) {
if log_m == super::super::MODULUS16 as u16 {
if XOR {
for (o, &v) in out.iter_mut().zip(input.iter()) {
*o ^= v;
}
} else {
out.copy_from_slice(input);
}
return;
}
if should_use_simd(input.len()) {
mulgf16_simd::<XOR>(out, input, log_m, tables);
} else {
let t = NibbleTables16::build(log_m, &tables.log_lut, &tables.exp_lut);
mulgf16_tabled_scalar::<XOR>(out, input, &t);
}
}
#[test]
fn tabled_matches_mul_log16() {
let tables = build_tables16();
for _ in 0..64 {
let log_m = rand::random::<u16>();
let t = NibbleTables16::build(log_m, &tables.log_lut, &tables.exp_lut);
let input: Vec<u16> = (0..300).map(|_| rand::random::<u16>()).collect();
let mut got = vec![0u16; input.len()];
mulgf16_tabled_scalar::<false>(&mut got, &input, &t);
let mut want = vec![0u16; input.len()];
ref_mul(&mut want, &input, log_m, &tables, false);
assert_eq!(got, want, "tabled != mul_log16 (log_m={log_m})");
}
}
#[test]
fn simd_matches_scalar_mul_and_xor() {
let tables = build_tables16();
let mut log_ms: Vec<u16> = vec![0, 1, 2, 255, 256, 0xFFFE, 0xFFFF];
for _ in 0..8 {
log_ms.push(rand::random::<u16>());
}
for &log_m in &log_ms {
for &len in &LENGTHS {
let input: Vec<u16> = (0..len).map(|_| rand::random::<u16>()).collect();
let mut simd = vec![0u16; len];
run_mul::<false>(&mut simd, &input, log_m, &tables);
let mut scal = vec![0u16; len];
ref_mul(&mut scal, &input, log_m, &tables, false);
assert_eq!(simd, scal, "mul mismatch len={len} log_m={log_m}");
let seed: Vec<u16> = (0..len).map(|i| (i as u16).wrapping_mul(40503)).collect();
let mut simd_x = seed.clone();
run_mul::<true>(&mut simd_x, &input, log_m, &tables);
let mut scal_x = seed.clone();
ref_mul(&mut scal_x, &input, log_m, &tables, true);
assert_eq!(simd_x, scal_x, "xor mismatch len={len} log_m={log_m}");
}
}
}
#[cfg(feature = "std")]
#[test]
#[ignore]
fn decision_gate_speedup() {
use std::hint::black_box;
use std::time::Instant;
let tables = build_tables16();
let len = 32768usize;
let log_m: u16 = 0x1234;
let input: Vec<u16> = (0..len).map(|i| (i as u16).wrapping_mul(2027)).collect();
let t = NibbleTables16::build(log_m, &tables.log_lut, &tables.exp_lut);
let mut out = vec![0u16; len];
let iters = 4000u32;
for _ in 0..64 {
mulgf16_simd::<false>(&mut out, &input, log_m, &tables);
}
let ts = Instant::now();
for _ in 0..iters {
mulgf16_simd::<false>(black_box(&mut out), black_box(&input), log_m, &tables);
}
let simd_ns = ts.elapsed().as_nanos() as f64 / iters as f64;
for _ in 0..64 {
mulgf16_tabled_scalar::<false>(&mut out, &input, &t);
}
let tc = Instant::now();
for _ in 0..iters {
mulgf16_tabled_scalar::<false>(black_box(&mut out), black_box(&input), &t);
}
let scalar_ns = tc.elapsed().as_nanos() as f64 / iters as f64;
println!(
"\n[#1233 decision gate] len={len} log_m={log_m:#06x}\n SIMD: {simd_ns:>9.1} ns/call\n scalar: {scalar_ns:>9.1} ns/call\n speedup: {:.2}x",
scalar_ns / simd_ns
);
}
#[test]
fn identity_and_modulus_are_input() {
let tables = build_tables16();
let input: Vec<u16> = (0..1000).map(|_| rand::random::<u16>()).collect();
let mut out = vec![0u16; input.len()];
run_mul::<false>(&mut out, &input, 0, &tables);
assert_eq!(out, input, "mul by g^0 must be identity");
}
#[cfg(all(feature = "std", target_arch = "x86_64", target_endian = "little"))]
#[test]
fn ssse3_kernel_matches_scalar() {
if !std::is_x86_feature_detected!("ssse3") {
return;
}
let tables = build_tables16();
for &log_m in &[0u16, 1, 2, 0xABCD, 0xFFFE] {
for &len in LENGTHS.iter().filter(|&&l| l >= 16) {
let input: Vec<u16> = (0..len).map(|_| rand::random::<u16>()).collect();
let t = NibbleTables16::build(log_m, &tables.log_lut, &tables.exp_lut);
let mut got = vec![0u16; len];
unsafe {
super::x86::mulgf16_ssse3::<false>(&mut got, &input, &t);
}
let mut want = vec![0u16; len];
ref_mul(&mut want, &input, log_m, &tables, false);
assert_eq!(got, want, "ssse3 mul len={len} log_m={log_m}");
let seed: Vec<u16> = (0..len).map(|i| (i as u16).wrapping_mul(7)).collect();
let mut got_x = seed.clone();
unsafe {
super::x86::mulgf16_ssse3::<true>(&mut got_x, &input, &t);
}
let mut want_x = seed.clone();
for (w, (&v, &s)) in want_x.iter_mut().zip(input.iter().zip(seed.iter())) {
let _ = s;
*w ^= mul_log16(v, log_m, &tables.log_lut, &tables.exp_lut);
}
assert_eq!(got_x, want_x, "ssse3 xor len={len} log_m={log_m}");
}
}
}
}