#![cfg_attr(
not(all(
target_arch = "x86_64",
target_feature = "vpclmulqdq",
any(target_feature = "avx2", target_feature = "avx512f")
)),
allow(dead_code)
)]
pub(crate) const LOW_BY_LOW: i32 = 0x00;
pub(crate) const HIGH_BY_HIGH: i32 = 0x11;
pub(crate) const HIGH_BY_LOW: i32 = 0x01;
pub(crate) const TOP_NIBBLE_FOLD: [u8; 16] = {
let mut table = [0u8; 16];
let mut s = 0;
while s < 16 {
let spill = s ^ (s >> 1) ^ (s >> 3);
table[s] = (spill ^ (spill << 1) ^ (spill << 3) ^ (spill << 4)) as u8;
s += 1;
}
table
};
pub(crate) trait Lanes64: Copy {
fn zero() -> Self;
fn xor(self, other: Self) -> Self;
#[inline(always)]
fn xor3(self, b: Self, c: Self) -> Self {
self.xor(b).xor(c)
}
fn clmul<const IMM: i32>(self, other: Self) -> Self;
fn unpack_low(self, other: Self) -> Self;
fn unpack_high(self, other: Self) -> Self;
fn shl<const N: i32>(self) -> Self;
fn shr<const N: i32>(self) -> Self;
#[inline(always)]
fn fold_top_nibble(self) -> Self {
let spill = self.shr::<63>().xor3(self.shr::<61>(), self.shr::<60>());
spill
.xor3(spill.shl::<1>(), spill.shl::<3>())
.xor(spill.shl::<4>())
}
#[inline(always)]
fn reduce_lane(self) -> Self {
fold(self, self.unpack_high(self))
}
#[inline(always)]
fn reduce_wide(even: Self, odd: Self) -> Self {
fold(even.unpack_low(odd), even.unpack_high(odd))
}
}
#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
#[inline(always)]
pub(crate) fn reduce_by_multiply<L: Lanes64>(product: L, tail: L) -> L {
let folded = product.clmul::<HIGH_BY_HIGH>(tail);
let spill = folded.clmul::<HIGH_BY_HIGH>(tail);
product.xor3(folded, spill)
}
#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
pub(crate) const TAIL_64: u64 = 0b1_1011;
#[inline(always)]
pub(crate) fn fold<L: Lanes64>(low: L, high: L) -> L {
let shifted = high.shl::<1>().xor3(high.shl::<3>(), high.shl::<4>());
low.xor3(high, high.fold_top_nibble()).xor(shifted)
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Wide<L> {
pub(crate) even: L,
pub(crate) odd: L,
}
impl<L: Lanes64> Wide<L> {
#[inline(always)]
pub(crate) fn zero() -> Self {
Self {
even: L::zero(),
odd: L::zero(),
}
}
#[inline(always)]
pub(crate) fn mul(a: L, b: L) -> Self {
Self {
even: a.clmul::<LOW_BY_LOW>(b),
odd: a.clmul::<HIGH_BY_HIGH>(b),
}
}
#[inline(always)]
pub(crate) fn xor(self, other: Self) -> Self {
Self {
even: self.even.xor(other.even),
odd: self.odd.xor(other.odd),
}
}
#[inline(always)]
pub(crate) fn xor3(self, b: Self, c: Self) -> Self {
Self {
even: self.even.xor3(b.even, c.even),
odd: self.odd.xor3(b.odd, c.odd),
}
}
#[inline(always)]
pub(crate) fn reduce(self) -> L {
L::reduce_wide(self.even, self.odd)
}
}
#[inline(always)]
pub(crate) fn cubic_mul<L: Lanes64>(a: [L; 3], b: [L; 3]) -> [Wide<L>; 3] {
let [a0, a1, a2] = a;
let [b0, b1, b2] = b;
let c0 = Wide::mul(a0, b0);
let c1 = Wide::mul(a1, b1);
let c2 = Wide::mul(a2, b2);
let d01 = Wide::mul(a0.xor(a1), b0.xor(b1));
let d02 = Wide::mul(a0.xor(a2), b0.xor(b2));
let d12 = Wide::mul(a1.xor(a2), b1.xor(b2));
fold_cubic([c0, c1, c2, d01, d02, d12], Wide::xor, Wide::xor3)
}
#[inline(always)]
pub(crate) fn fold_cubic<V: Copy>(
terms: [V; 6],
xor: impl Fn(V, V) -> V,
xor3: impl Fn(V, V, V) -> V,
) -> [V; 3] {
let [c0, c1, c2, d01, d02, d12] = terms;
let shared = xor(c0, d12);
[xor3(shared, c1, c2), xor(shared, d01), xor3(c0, c1, d02)]
}
#[inline(always)]
pub(crate) fn cubic_square<L: Lanes64>(a: [L; 3]) -> [Wide<L>; 3] {
let [s0, s1, s2] = a.map(|x| Wide::mul(x, x));
[s0, s2, s1.xor(s2)]
}
#[inline(always)]
pub(crate) fn cubic_mul_base<L: Lanes64>(a: [L; 3], k: L) -> [Wide<L>; 3] {
a.map(|x| Wide::mul(x, k))
}
#[cfg(test)]
pub(crate) mod model {
use super::Lanes64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Model<const LANES: usize>(pub(crate) [[u64; 2]; LANES]);
impl<const LANES: usize> Model<LANES> {
fn map(self, f: impl Fn(u64) -> u64) -> Self {
Self(self.0.map(|lane| lane.map(&f)))
}
fn zip(self, other: Self, f: impl Fn([u64; 2], [u64; 2]) -> [u64; 2]) -> Self {
Self(core::array::from_fn(|i| f(self.0[i], other.0[i])))
}
}
impl<const LANES: usize> Lanes64 for Model<LANES> {
fn zero() -> Self {
Self([[0; 2]; LANES])
}
fn xor(self, other: Self) -> Self {
self.zip(other, |x, y| [x[0] ^ y[0], x[1] ^ y[1]])
}
fn clmul<const IMM: i32>(self, other: Self) -> Self {
self.zip(other, |x, y| {
let product = super::super::scalar_clmul_64x64(
x[(IMM & 0x01) as usize],
y[((IMM >> 4) & 0x01) as usize],
);
[product as u64, (product >> 64) as u64]
})
}
fn unpack_low(self, other: Self) -> Self {
self.zip(other, |x, y| [x[0], y[0]])
}
fn unpack_high(self, other: Self) -> Self {
self.zip(other, |x, y| [x[1], y[1]])
}
fn shl<const N: i32>(self) -> Self {
self.map(|v| v << N)
}
fn shr<const N: i32>(self) -> Self {
self.map(|v| v >> N)
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::model::Model;
use super::{
Lanes64, TAIL_64, TOP_NIBBLE_FOLD, Wide, cubic_mul, cubic_mul_base, cubic_square, fold,
reduce_by_multiply,
};
use crate::clmul::scalar_clmul_64x64;
const LANES: usize = 2;
fn schoolbook(a: u64, b: u64) -> u64 {
let mut acc = 0u64;
for i in (0..64).rev() {
let overflowed = acc >> 63 == 1;
acc <<= 1;
if overflowed {
acc ^= 0b1_1011;
}
if (b >> i) & 1 == 1 {
acc ^= a;
}
}
acc
}
fn cubic_schoolbook(a: [u64; 3], b: [u64; 3]) -> [u64; 3] {
let mut raw = [0u64; 5];
for i in 0..3 {
for j in 0..3 {
raw[i + j] ^= schoolbook(a[i], b[j]);
}
}
[raw[0] ^ raw[3], raw[1] ^ raw[3] ^ raw[4], raw[2] ^ raw[4]]
}
fn model(values: [u64; 2 * LANES]) -> Model<LANES> {
Model(core::array::from_fn(|i| [values[2 * i], values[2 * i + 1]]))
}
fn values(m: Model<LANES>) -> [u64; 2 * LANES] {
core::array::from_fn(|k| m.0[k / 2][k % 2])
}
const CORNERS: [u64; 6] = [0, 1, u64::MAX, 1 << 63, 0x1b, 0xf << 60];
#[test]
fn the_nibble_table_matches_the_shifted_fold() {
for s in 0..16u64 {
let lanes = Model::<1>([[s << 60, 0]]);
let default = <Model<1> as Lanes64>::fold_top_nibble(lanes);
assert_eq!(
u64::from(TOP_NIBBLE_FOLD[s as usize]),
default.0[0][0],
"{s}"
);
}
}
#[test]
fn the_fold_is_exact_on_the_extremes() {
for a in CORNERS {
for b in CORNERS {
let product = scalar_clmul_64x64(a, b);
let got = fold(
Model::<1>([[product as u64, 0]]),
Model::<1>([[(product >> 64) as u64, 0]]),
);
assert_eq!(got.0[0][0], schoolbook(a, b), "{a:#x} * {b:#x}");
}
}
}
#[test]
fn the_multiply_fold_is_exact_on_the_extremes() {
for a in CORNERS {
for b in CORNERS {
let product = scalar_clmul_64x64(a, b);
let lane = [product as u64, (product >> 64) as u64];
let tail = Model::<2>([[TAIL_64; 2]; 2]);
let got = reduce_by_multiply(Model::<2>([lane, [0, lane[1]]]), tail);
let raised = schoolbook(schoolbook(lane[1], 1 << 63), 2);
assert_eq!(got.0[0][0], schoolbook(a, b), "{a:#x} * {b:#x}");
assert_eq!(got.0[1][0], raised, "{a:#x} * {b:#x}, high half");
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn the_lane_product_matches_the_schoolbook_one(
a in any::<[u64; 2 * LANES]>(),
b in any::<[u64; 2 * LANES]>(),
) {
let got = values(Wide::mul(model(a), model(b)).reduce());
let want: [u64; 2 * LANES] = core::array::from_fn(|k| schoolbook(a[k], b[k]));
prop_assert_eq!(got, want);
}
#[test]
fn a_sum_of_wide_products_reduces_to_the_sum_of_products(
a in any::<[u64; 2 * LANES]>(),
b in any::<[u64; 2 * LANES]>(),
c in any::<[u64; 2 * LANES]>(),
) {
let wide = Wide::mul(model(a), model(b))
.xor3(Wide::mul(model(b), model(c)), Wide::mul(model(c), model(a)));
let want: [u64; 2 * LANES] = core::array::from_fn(|k| {
schoolbook(a[k], b[k]) ^ schoolbook(b[k], c[k]) ^ schoolbook(c[k], a[k])
});
prop_assert_eq!(values(wide.reduce()), want);
}
#[test]
fn the_cubic_kernels_match_the_schoolbook_ones(
a in any::<[[u64; 2 * LANES]; 3]>(),
b in any::<[[u64; 2 * LANES]; 3]>(),
k in any::<[u64; 2 * LANES]>(),
) {
let (x, y) = (a.map(model), b.map(model));
let product = cubic_mul(x, y).map(|w| values(w.reduce()));
let square = cubic_square(x).map(|w| values(w.reduce()));
let scaled = cubic_mul_base(x, model(k)).map(|w| values(w.reduce()));
for j in 0..2 * LANES {
let (aj, bj) = ([a[0][j], a[1][j], a[2][j]], [b[0][j], b[1][j], b[2][j]]);
let at = |r: [[u64; 2 * LANES]; 3]| [r[0][j], r[1][j], r[2][j]];
prop_assert_eq!(at(product), cubic_schoolbook(aj, bj));
prop_assert_eq!(at(square), cubic_schoolbook(aj, aj));
prop_assert_eq!(at(scaled), cubic_schoolbook(aj, [k[j], 0, 0]));
}
}
}
}