pub(super) const TAIL_32: u128 = 0x8d;
const XI_32: [u128; 5] = [
0x54fd_1265,
0xee55_98fa,
0x7a9d_86c2,
0xc890_6c73,
0xe87a_3f19,
];
pub(super) const TAIL_64: u128 = 0b1_1011;
pub(crate) const TAIL_128: u128 = 0b1000_0111;
const XI_64: [u128; 6] = [
0x19c9_369f_278a_dc03,
0xfc39_a481_a127_aa9d,
0xae4a_b740_40e7_9118,
0xed58_cafe_f8f2_2bc9,
0xea6b_20d9_15fd_bb77,
0x3f2a_7c50_1e89_8bcd,
];
const XI_128: [u128; 7] = [
0x295a_c0b1_f473_1af9_676a_ac9f_a4b2_0b08,
0x7959_d70c_e1ee_6942_53b8_5b64_02b1_e849,
0x6cfe_46e8_277b_2e9d_f167_7bc1_710c_5c54,
0xd748_3fda_e776_3b6f_f501_c92b_7a41_cc34,
0x681c_6a8c_3679_2c02_92d6_dad7_72f9_2df5,
0x496c_b27f_a3eb_e728_9596_0776_ef42_f90b,
0x9cfc_a256_33f9_993f_4e1f_5110_70b2_3e78,
];
pub(super) const fn poly_mul(a: u128, b: u128, bits: usize, tail: u128) -> u128 {
let mask = u128::MAX >> (128 - bits);
let top = 1u128 << (bits - 1);
let mut acc = 0u128;
let mut i = bits;
while i > 0 {
i -= 1;
let overflowed = acc & top != 0;
acc = (acc << 1) & mask;
if overflowed {
acc ^= tail;
}
if (b >> i) & 1 == 1 {
acc ^= a;
}
}
acc
}
const fn relations_hold(bits: usize, tail: u128, xi: &[u128]) -> bool {
let mut k = 0;
while k < xi.len() {
let previous = if k == 0 { 1 } else { xi[k - 1] };
if poly_mul(xi[k], xi[k], bits, tail) ^ poly_mul(previous, xi[k], bits, tail) ^ 1 != 0 {
return false;
}
k += 1;
}
true
}
const _: () = assert!(
relations_hold(32, TAIL_32, &XI_32),
"XI_32 violates the tower relations"
);
const _: () = assert!(
relations_hold(64, TAIL_64, &XI_64),
"XI_64 violates the tower relations"
);
const _: () = assert!(
relations_hold(128, TAIL_128, &XI_128),
"XI_128 violates the tower relations"
);
const fn columns(bits: usize, tail: u128, xi: &[u128]) -> [u128; 128] {
let mut cols = [0u128; 128];
cols[0] = 1;
let mut j = 1;
while j < bits {
let low = j.trailing_zeros() as usize;
cols[j] = poly_mul(cols[j ^ (1 << low)], xi[low], bits, tail);
j += 1;
}
cols
}
const fn invert(cols: &[u128; 128], bits: usize) -> [u128; 128] {
let mut mat = *cols;
let mut inv = [0u128; 128];
let mut j = 0;
while j < bits {
inv[j] = 1 << j;
j += 1;
}
let mut pivot = 0;
while pivot < bits {
let mut col = pivot;
while col < bits && (mat[col] >> pivot) & 1 == 0 {
col += 1;
}
assert!(col < bits, "the change-of-basis matrix is singular");
let swapped = mat[pivot];
mat[pivot] = mat[col];
mat[col] = swapped;
let swapped = inv[pivot];
inv[pivot] = inv[col];
inv[col] = swapped;
let mut col = 0;
while col < bits {
if col != pivot && (mat[col] >> pivot) & 1 == 1 {
mat[col] ^= mat[pivot];
inv[col] ^= inv[pivot];
}
col += 1;
}
pivot += 1;
}
inv
}
const fn byte_tables(cols: &[u128; 128], bits: usize) -> [[u128; 256]; 16] {
let mut tables = [[0u128; 256]; 16];
let mut byte = 0;
while byte < bits / 8 {
let mut value = 1usize;
while value < 256 {
let low = value.trailing_zeros() as usize;
tables[byte][value] = tables[byte][value ^ (1 << low)] ^ cols[byte * 8 + low];
value += 1;
}
byte += 1;
}
tables
}
const fn square_columns(bits: usize, tail: u128, cols: &[u128; 128]) -> [u128; 128] {
let inverse = invert(cols, bits);
let mut squared = [0u128; 128];
let mut j = 0;
while j < bits {
let image = poly_mul(cols[j], cols[j], bits, tail);
let mut column = 0u128;
let mut i = 0;
while i < bits {
if (image >> i) & 1 == 1 {
column ^= inverse[i];
}
i += 1;
}
squared[j] = column;
j += 1;
}
squared
}
const fn narrow(tables: &[[u128; 256]; 16]) -> [[u64; 256]; 8] {
let mut narrowed = [[0u64; 256]; 8];
let mut byte = 0;
while byte < 8 {
let mut value = 0;
while value < 256 {
narrowed[byte][value] = tables[byte][value] as u64;
value += 1;
}
byte += 1;
}
narrowed
}
const fn narrow_32(tables: &[[u128; 256]; 16]) -> [[u32; 256]; 4] {
let mut result = [[0; 256]; 4];
let mut i = 0;
while i < 4 {
let mut j = 0;
while j < 256 {
result[i][j] = tables[i][j] as u32;
j += 1;
}
i += 1;
}
result
}
const COLUMNS_32: [u128; 128] = columns(32, TAIL_32, &XI_32);
static TOWER_TO_POLY_32: [[u32; 256]; 4] = narrow_32(&byte_tables(&COLUMNS_32, 32));
static POLY_TO_TOWER_32: [[u32; 256]; 4] = narrow_32(&byte_tables(&invert(&COLUMNS_32, 32), 32));
#[inline]
fn apply_32(tables: &[[u32; 256]; 4], value: u32) -> u32 {
tables.iter().enumerate().fold(0, |acc, (i, row)| {
acc ^ row[(value >> (8 * i)) as u8 as usize]
})
}
#[inline]
pub(super) fn tower_to_poly_32(value: u32) -> u32 {
apply_32(&TOWER_TO_POLY_32, value)
}
#[inline]
pub(super) fn poly_to_tower_32(value: u32) -> u32 {
apply_32(&POLY_TO_TOWER_32, value)
}
const COLUMNS_64: [u128; 128] = columns(64, TAIL_64, &XI_64);
const COLUMNS_128: [u128; 128] = columns(128, TAIL_128, &XI_128);
static TOWER_TO_POLY_64: [[u64; 256]; 8] = narrow(&byte_tables(&COLUMNS_64, 64));
static POLY_TO_TOWER_64: [[u64; 256]; 8] = narrow(&byte_tables(&invert(&COLUMNS_64, 64), 64));
static SQUARE_64: [[u64; 256]; 8] =
narrow(&byte_tables(&square_columns(64, TAIL_64, &COLUMNS_64), 64));
static TOWER_TO_POLY_128: [[u128; 256]; 16] = byte_tables(&COLUMNS_128, 128);
static POLY_TO_TOWER_128: [[u128; 256]; 16] = byte_tables(&invert(&COLUMNS_128, 128), 128);
pub(crate) const fn tower_image_128(v: u128) -> u128 {
image(v, 128, &COLUMNS_128)
}
pub(crate) const fn tower_image_64(v: u64) -> u64 {
image(v as u128, 64, &COLUMNS_64) as u64
}
const fn image(v: u128, bits: usize, columns: &[u128; 128]) -> u128 {
let mut acc = 0;
let mut i = 0;
while i < bits {
if (v >> i) & 1 == 1 {
acc ^= columns[i];
}
i += 1;
}
acc
}
#[inline]
fn apply_64(tables: &[[u64; 256]; 8], v: u64) -> u64 {
let mut acc = 0;
for (byte, table) in tables.iter().enumerate() {
acc ^= table[(v >> (8 * byte)) as u8 as usize];
}
acc
}
#[inline]
fn apply_128(tables: &[[u128; 256]; 16], v: u128) -> u128 {
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
{
use core::arch::aarch64::{uint8x16_t, vdupq_n_u8, veorq_u8, vld1q_u8};
unsafe {
let mut acc = vdupq_n_u8(0);
for (byte, table) in tables.iter().enumerate() {
let entry = &table[(v >> (8 * byte)) as u8 as usize];
acc = veorq_u8(acc, vld1q_u8(core::ptr::from_ref(entry).cast()));
}
core::mem::transmute::<uint8x16_t, u128>(acc)
}
}
#[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
{
use core::arch::x86_64::{__m128i, _mm_loadu_si128, _mm_setzero_si128, _mm_xor_si128};
unsafe {
let mut acc = _mm_setzero_si128();
for (byte, table) in tables.iter().enumerate() {
let entry = &table[(v >> (8 * byte)) as u8 as usize];
acc = _mm_xor_si128(acc, _mm_loadu_si128(core::ptr::from_ref(entry).cast()));
}
core::mem::transmute::<__m128i, u128>(acc)
}
}
#[cfg(not(any(
all(target_arch = "aarch64", target_feature = "neon"),
all(target_arch = "x86_64", target_feature = "sse2")
)))]
{
let mut acc = 0;
for (byte, table) in tables.iter().enumerate() {
acc ^= table[(v >> (8 * byte)) as u8 as usize];
}
acc
}
}
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
mod blocked {
use core::arch::x86_64::{
__m512i, _mm512_gf2p8affine_epi64_epi8, _mm512_loadu_si512, _mm512_set1_epi64,
_mm512_storeu_si512, _mm512_ternarylogic_epi64, _mm512_unpackhi_epi8,
_mm512_unpackhi_epi16, _mm512_unpackhi_epi32, _mm512_unpackhi_epi64, _mm512_unpacklo_epi8,
_mm512_unpacklo_epi16, _mm512_unpacklo_epi32, _mm512_unpacklo_epi64, _mm512_xor_si512,
};
use super::{COLUMNS_128, invert};
pub(super) const BLOCK: usize = 64;
const XOR3: i32 = 0x96;
const BITREV4: [usize; 16] = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
pub(super) struct PreparedMap {
pub(super) columns: [u128; 128],
blocks: [[u64; 16]; 16],
}
const fn affine_blocks(cols: &[u128; 128]) -> [[u64; 16]; 16] {
let mut blocks = [[0u64; 16]; 16];
let mut k = 0;
while k < 16 {
let mut j = 0;
while j < 16 {
let mut quadword = 0u64;
let mut i = 0;
while i < 8 {
let mut row = 0u64;
let mut b = 0;
while b < 8 {
if (cols[8 * j + b] >> (8 * k + i)) & 1 == 1 {
row |= 1 << b;
}
b += 1;
}
quadword |= row << (8 * (7 - i));
i += 1;
}
blocks[k][j] = quadword;
j += 1;
}
k += 1;
}
blocks
}
#[inline]
const fn transpose8(mut value: u64) -> u64 {
let mut exchange = (value ^ (value >> 7)) & 0x00aa_00aa_00aa_00aa;
value ^= exchange ^ (exchange << 7);
exchange = (value ^ (value >> 14)) & 0x0000_cccc_0000_cccc;
value ^= exchange ^ (exchange << 14);
exchange = (value ^ (value >> 28)) & 0x0000_0000_f0f0_f0f0;
value ^ exchange ^ (exchange << 28)
}
#[inline]
const fn affine_blocks_runtime(cols: &[u128; 128]) -> [[u64; 16]; 16] {
let mut blocks = [[0u64; 16]; 16];
let mut j = 0;
while j < 16 {
let c0 = cols[8 * j].to_le_bytes();
let c1 = cols[8 * j + 1].to_le_bytes();
let c2 = cols[8 * j + 2].to_le_bytes();
let c3 = cols[8 * j + 3].to_le_bytes();
let c4 = cols[8 * j + 4].to_le_bytes();
let c5 = cols[8 * j + 5].to_le_bytes();
let c6 = cols[8 * j + 6].to_le_bytes();
let c7 = cols[8 * j + 7].to_le_bytes();
let mut k = 0;
while k < 16 {
let packed =
u64::from_le_bytes([c0[k], c1[k], c2[k], c3[k], c4[k], c5[k], c6[k], c7[k]]);
blocks[k][j] = transpose8(packed).swap_bytes();
k += 1;
}
j += 1;
}
blocks
}
impl PreparedMap {
#[inline]
pub(super) const fn new(columns: [u128; 128]) -> Self {
Self {
blocks: affine_blocks_runtime(&columns),
columns,
}
}
}
static TOWER_TO_POLY: [[u64; 16]; 16] = affine_blocks(&COLUMNS_128);
static POLY_TO_TOWER: [[u64; 16]; 16] = affine_blocks(&invert(&COLUMNS_128, 128));
#[inline(always)]
unsafe fn interleave<const STEP: usize>(a: &mut [__m512i; 16]) {
let mut i = 0;
while i < 16 {
if i & STEP == 0 {
let (x, y) = (a[i], a[i + STEP]);
let (lo, hi) = unsafe {
match STEP {
1 => (_mm512_unpacklo_epi8(x, y), _mm512_unpackhi_epi8(x, y)),
2 => (_mm512_unpacklo_epi16(x, y), _mm512_unpackhi_epi16(x, y)),
4 => (_mm512_unpacklo_epi32(x, y), _mm512_unpackhi_epi32(x, y)),
_ => (_mm512_unpacklo_epi64(x, y), _mm512_unpackhi_epi64(x, y)),
}
};
a[i] = lo;
a[i + STEP] = hi;
}
i += 1;
}
}
#[inline(always)]
unsafe fn transpose(a: [__m512i; 16]) -> [__m512i; 16] {
let mut a = a;
unsafe {
interleave::<1>(&mut a);
interleave::<2>(&mut a);
interleave::<4>(&mut a);
interleave::<8>(&mut a);
}
core::array::from_fn(|j| a[BITREV4[j]])
}
#[inline(always)]
unsafe fn plane(input: &[__m512i; 16], row: &[u64; 16]) -> __m512i {
unsafe {
let t: [__m512i; 16] = core::array::from_fn(|j| {
_mm512_gf2p8affine_epi64_epi8::<0>(input[j], _mm512_set1_epi64(row[j] as i64))
});
let a = _mm512_ternarylogic_epi64::<XOR3>(t[0], t[1], t[2]);
let b = _mm512_ternarylogic_epi64::<XOR3>(t[3], t[4], t[5]);
let c = _mm512_ternarylogic_epi64::<XOR3>(t[6], t[7], t[8]);
let d = _mm512_ternarylogic_epi64::<XOR3>(t[9], t[10], t[11]);
let e = _mm512_ternarylogic_epi64::<XOR3>(t[12], t[13], t[14]);
let left = _mm512_ternarylogic_epi64::<XOR3>(a, b, c);
let right = _mm512_ternarylogic_epi64::<XOR3>(d, e, t[15]);
_mm512_xor_si512(left, right)
}
}
#[inline(never)]
fn apply(blocks: &[[u64; 16]; 16], values: &mut [u128]) -> usize {
let (chunks, _) = values.as_chunks_mut::<BLOCK>();
for chunk in chunks.iter_mut() {
unsafe {
let raw: [__m512i; 16] =
core::array::from_fn(|r| _mm512_loadu_si512(chunk.as_ptr().add(4 * r).cast()));
let input = transpose(raw);
let output: [__m512i; 16] = core::array::from_fn(|k| plane(&input, &blocks[k]));
let output = transpose(output);
for (r, &value) in output.iter().enumerate() {
_mm512_storeu_si512(chunk.as_mut_ptr().add(4 * r).cast(), value);
}
}
}
chunks.len() * BLOCK
}
#[inline(never)]
pub(super) fn apply_out_of_place(
prepared: &PreparedMap,
input: &[u128],
output: &mut [u128],
) -> usize {
assert_eq!(
input.len(),
output.len(),
"input and output must have equal lengths"
);
let (input_chunks, _) = input.as_chunks::<BLOCK>();
let (output_chunks, _) = output.as_chunks_mut::<BLOCK>();
for (input_chunk, output_chunk) in input_chunks.iter().zip(output_chunks.iter_mut()) {
unsafe {
let raw: [__m512i; 16] = core::array::from_fn(|r| {
_mm512_loadu_si512(input_chunk.as_ptr().add(4 * r).cast())
});
let input = transpose(raw);
let mapped: [__m512i; 16] =
core::array::from_fn(|k| plane(&input, &prepared.blocks[k]));
let mapped = transpose(mapped);
for (r, &value) in mapped.iter().enumerate() {
_mm512_storeu_si512(output_chunk.as_mut_ptr().add(4 * r).cast(), value);
}
}
}
input_chunks.len() * BLOCK
}
#[inline]
pub(super) fn tower_to_poly(values: &mut [u128]) -> usize {
apply(&TOWER_TO_POLY, values)
}
#[inline]
pub(super) fn poly_to_tower(values: &mut [u128]) -> usize {
apply(&POLY_TO_TOWER, values)
}
#[cfg(test)]
mod runtime_constructor_tests {
use super::{affine_blocks, affine_blocks_runtime};
fn next_word(state: &mut u128) -> u128 {
*state ^= *state << 7;
*state ^= *state >> 9;
*state ^= *state << 8;
*state
}
#[test]
fn runtime_constructor_matches_reference_for_singletons_and_random_maps() {
for column in 0..128 {
for output_bit in 0..128 {
let mut columns = [0u128; 128];
columns[column] = 1u128 << output_bit;
assert_eq!(
affine_blocks_runtime(&columns),
affine_blocks(&columns),
"column {column}, output bit {output_bit}"
);
}
}
let mut state = 0xC011_8A8E_51ED_5EED_1234_5678_9ABC_DEF0u128;
for map in 0..8 {
let columns = core::array::from_fn(|_| next_word(&mut state));
assert_eq!(
affine_blocks_runtime(&columns),
affine_blocks(&columns),
"random map {map}"
);
}
}
}
}
#[cfg(not(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
)))]
mod blocked {
pub(super) const BLOCK: usize = usize::MAX;
#[inline]
pub(super) const fn tower_to_poly(_values: &mut [u128]) -> usize {
0
}
#[inline]
pub(super) const fn poly_to_tower(_values: &mut [u128]) -> usize {
0
}
}
pub(crate) const DYNAMIC_MAP_THRESHOLD: usize = 4096;
#[inline]
pub(crate) fn try_map_tower_coordinates_into(
columns: &[u128; 128],
input: &[u128],
output: &mut [u128],
) -> bool {
assert_eq!(
input.len(),
output.len(),
"input and output must have equal lengths"
);
if input.len() < DYNAMIC_MAP_THRESHOLD {
return false;
}
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
{
let prepared = blocked::PreparedMap::new(*columns);
let processed = blocked::apply_out_of_place(&prepared, input, output);
for (source, destination) in input[processed..].iter().zip(&mut output[processed..]) {
*destination = image(*source, 128, &prepared.columns);
}
true
}
#[cfg(not(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
)))]
{
let _ = (columns, input, output);
false
}
}
#[inline]
pub(crate) fn tower_to_poly_64(v: u64) -> u64 {
apply_64(&TOWER_TO_POLY_64, v)
}
#[inline]
pub(crate) fn poly_to_tower_64(v: u64) -> u64 {
apply_64(&POLY_TO_TOWER_64, v)
}
#[inline]
pub(super) fn tower_square_64(v: u64) -> u64 {
apply_64(&SQUARE_64, v)
}
#[inline]
pub(crate) fn tower_to_poly_128(v: u128) -> u128 {
apply_128(&TOWER_TO_POLY_128, v)
}
#[inline]
pub(crate) fn poly_to_tower_128(v: u128) -> u128 {
apply_128(&POLY_TO_TOWER_128, v)
}
#[inline]
pub(crate) fn tower_to_poly_128_slice(values: &mut [u128]) -> usize {
let blocked = if values.len() < blocked::BLOCK {
0
} else {
blocked::tower_to_poly(values)
};
for value in &mut values[blocked..] {
*value = apply_128(&TOWER_TO_POLY_128, *value);
}
blocked
}
#[inline]
pub(crate) fn poly_to_tower_128_slice(values: &mut [u128]) -> usize {
let blocked = if values.len() < blocked::BLOCK {
0
} else {
blocked::poly_to_tower(values)
};
for value in &mut values[blocked..] {
*value = apply_128(&POLY_TO_TOWER_128, *value);
}
blocked
}
#[cfg(test)]
mod tests {
extern crate std;
use alloc::vec::Vec;
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
use std::hint::black_box;
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
use std::time::Instant;
use p3_field::PrimeCharacteristicRing;
use proptest::prelude::*;
use super::*;
use crate::tower::TowerLevel;
use crate::{BinaryField64, BinaryField128};
const BLOCKED_BUILD: bool = cfg!(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
));
const _: () = assert!(if BLOCKED_BUILD {
super::blocked::BLOCK == 64
} else {
super::blocked::BLOCK == usize::MAX
});
const SENTINELS: usize = 2 * 64;
const SENTINEL: u128 = 0x5a5a_5a5a_5a5a_5a5a_5a5a_5a5a_5a5a_5a5a;
fn column_walk(cols: &[u128; 128], v: u128) -> u128 {
(0..128)
.filter(|i| (v >> i) & 1 == 1)
.fold(0, |acc, i| acc ^ cols[i])
}
fn inverse_columns() -> [u128; 128] {
invert(&COLUMNS_128, 128)
}
#[test]
fn byte_maps_match_scalar_evaluation() {
let inverse = inverse_columns();
for (tables, cols) in [
(&TOWER_TO_POLY_128, &COLUMNS_128),
(&POLY_TO_TOWER_128, &inverse),
] {
for byte in 0..16 {
for value in 0..256u128 {
let input = !(255u128 << (8 * byte)) | (value << (8 * byte));
let folded = tables.iter().enumerate().fold(0, |acc, (i, table)| {
acc ^ table[(input >> (8 * i)) as u8 as usize]
});
assert_eq!(apply_128(tables, input), folded);
assert_eq!(apply_128(tables, input), column_walk(cols, input));
}
}
}
}
fn sample(len: usize) -> Vec<u128> {
(0..len)
.map(|i| (i as u128 + 1).wrapping_mul(0x9e37_79b9_7f4a_7c15_f39c_c060_5ced_c835))
.collect()
}
fn padded(values: &[u128], offset: usize) -> Vec<u128> {
let lead = SENTINELS + offset;
let mut buffer = Vec::with_capacity(lead + values.len() + SENTINELS);
buffer.extend(core::iter::repeat_n(SENTINEL, lead));
buffer.extend_from_slice(values);
buffer.extend(core::iter::repeat_n(SENTINEL, SENTINELS));
buffer
}
fn slices_agree(values: &[u128], offset: usize) -> Result<(), TestCaseError> {
let (len, start) = (values.len(), SENTINELS + offset);
let inverse = inverse_columns();
let covered = len - len % super::blocked::BLOCK;
let mut buffer = padded(values, offset);
prop_assert_eq!(
tower_to_poly_128_slice(&mut buffer[start..start + len]),
covered
);
prop_assert!(buffer[..start].iter().all(|&v| v == SENTINEL));
prop_assert!(buffer[start + len..].iter().all(|&v| v == SENTINEL));
let forward: Vec<u128> = values
.iter()
.map(|&v| column_walk(&COLUMNS_128, v))
.collect();
prop_assert_eq!(&buffer[start..start + len], &forward[..]);
prop_assert_eq!(
poly_to_tower_128_slice(&mut buffer[start..start + len]),
covered
);
prop_assert!(buffer[..start].iter().all(|&v| v == SENTINEL));
prop_assert!(buffer[start + len..].iter().all(|&v| v == SENTINEL));
prop_assert_eq!(&buffer[start..start + len], values);
let mut buffer = padded(values, offset);
prop_assert_eq!(
poly_to_tower_128_slice(&mut buffer[start..start + len]),
covered
);
prop_assert!(buffer[..start].iter().all(|&v| v == SENTINEL));
prop_assert!(buffer[start + len..].iter().all(|&v| v == SENTINEL));
let backward: Vec<u128> = values.iter().map(|&v| column_walk(&inverse, v)).collect();
prop_assert_eq!(&buffer[start..start + len], &backward[..]);
Ok(())
}
#[test]
fn the_blocked_prefix_and_the_per_element_tail_agree_at_every_length() {
for len in 0..=200 {
let values = sample(len);
for offset in 0..4 {
slices_agree(&values, offset)
.unwrap_or_else(|e| panic!("len {len}, offset {offset}: {e}"));
}
}
}
#[test]
fn the_dispatchers_hand_every_whole_block_to_the_blocked_kernel() {
let block = super::blocked::BLOCK;
for len in 0..=256 {
let want = len - len % block;
let mut values = sample(len);
assert_eq!(tower_to_poly_128_slice(&mut values), want, "len {len}");
assert_eq!(poly_to_tower_128_slice(&mut values), want, "len {len}");
}
}
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
#[test]
fn dynamic_prepared_map_reports_prefix_and_leaves_tail_untouched() {
let columns = core::array::from_fn(|index| 1u128 << index);
let input = sample(4097);
let poison = SENTINEL;
let mut output = alloc::vec![poison; input.len()];
let prepared = blocked::PreparedMap::new(columns);
let processed = blocked::apply_out_of_place(&prepared, &input, &mut output);
assert_eq!(processed, 4096);
assert_eq!(&output[..processed], &input[..processed]);
assert!(output[processed..].iter().all(|&value| value == poison));
}
#[cfg(all(
target_arch = "x86_64",
target_feature = "gfni",
target_feature = "avx512f",
target_feature = "avx512bw"
))]
#[test]
#[ignore = "native GFNI benchmark; run with --ignored --nocapture"]
fn dynamic_prepared_map_forced_kernel_benchmark() {
for len in [64, 256, 1024, 4096, 16384] {
let input = sample(len);
let mut output = alloc::vec![0u128; len];
let mut construct = 0u128;
let mut apply = 0u128;
let mut combined = 0u128;
for iteration in 0..7 {
let started = Instant::now();
let prepared = blocked::PreparedMap::new(black_box(COLUMNS_128));
let construct_elapsed = started.elapsed().as_nanos();
let started_apply = Instant::now();
let processed = blocked::apply_out_of_place(
&prepared,
black_box(&input),
black_box(&mut output),
);
let apply_elapsed = started_apply.elapsed().as_nanos();
let started_combined = Instant::now();
let prepared = blocked::PreparedMap::new(black_box(COLUMNS_128));
let combined_processed = blocked::apply_out_of_place(
&prepared,
black_box(&input),
black_box(&mut output),
);
let combined_elapsed = started_combined.elapsed().as_nanos();
assert_eq!(processed, len - len % super::blocked::BLOCK);
assert_eq!(combined_processed, processed);
if iteration >= 2 {
construct += construct_elapsed;
apply += apply_elapsed;
combined += combined_elapsed;
}
}
let samples = 5;
std::println!(
"dynamic_prepared_map len={len} construct_ns={} apply_ns={} construct_and_apply_ns={}",
construct / samples,
apply / samples,
combined / samples
);
}
}
fn poly_square(a: u128, bits: usize, tail: u128) -> u128 {
poly_mul(a, a, bits, tail)
}
fn poly_pow(a: u128, mut e: u128, bits: usize, tail: u128) -> u128 {
let mut acc = 1;
let mut cur = a;
while e != 0 {
if e & 1 == 1 {
acc = poly_mul(acc, cur, bits, tail);
}
cur = poly_square(cur, bits, tail);
e >>= 1;
}
acc
}
fn poly_inverse(a: u128, bits: usize, tail: u128) -> u128 {
assert_ne!(a, 0, "zero is not invertible");
let order = if bits == 128 {
u128::MAX
} else {
(1u128 << bits) - 1
};
poly_pow(a, order - 1, bits, tail)
}
fn poly_trace(a: u128, bits: usize, tail: u128) -> u128 {
let mut acc = 0;
let mut cur = a;
for _ in 0..bits {
acc ^= cur;
cur = poly_square(cur, bits, tail);
}
assert!(acc <= 1, "the trace must land in GF(2)");
acc
}
fn trace_one_element(bits: usize, tail: u128) -> u128 {
(0..bits)
.map(|i| 1u128 << i)
.find(|&t| poly_trace(t, bits, tail) == 1)
.expect("the trace form cannot vanish on every basis element")
}
fn solve_quadratic(c: u128, bits: usize, tail: u128) -> u128 {
assert_eq!(poly_trace(c, bits, tail), 0, "u² + u = c has no solution");
let mut acc = 0;
let mut partial_trace = 0;
let mut t_power = trace_one_element(bits, tail);
let mut c_power = c;
for _ in 0..bits {
partial_trace ^= t_power;
acc ^= poly_mul(partial_trace, c_power, bits, tail);
t_power = poly_square(t_power, bits, tail);
c_power = poly_square(c_power, bits, tail);
}
assert_eq!(
poly_square(acc, bits, tail) ^ acc,
c,
"quadratic solve failed"
);
acc
}
fn derive_generator_images(bits: usize, tail: u128, levels: usize) -> Vec<u128> {
let mut images = Vec::with_capacity(levels);
let mut previous = 1u128;
for _ in 0..levels {
let c = poly_inverse(poly_square(previous, bits, tail), bits, tail);
let u = solve_quadratic(c, bits, tail);
previous = poly_mul(previous, u, bits, tail);
images.push(previous);
}
images
}
#[test]
fn derivation_reproduces_the_generator_images() {
assert_eq!(derive_generator_images(32, TAIL_32, 5), XI_32.to_vec());
assert_eq!(derive_generator_images(64, TAIL_64, 6), XI_64.to_vec());
assert_eq!(derive_generator_images(128, TAIL_128, 7), XI_128.to_vec());
}
#[test]
fn generator_images_satisfy_the_tower_relations() {
for (bits, tail, xi) in [
(64, TAIL_64, XI_64.as_slice()),
(128, TAIL_128, XI_128.as_slice()),
] {
let mut previous = 1u128;
for (k, &x) in xi.iter().enumerate() {
let relation =
poly_square(x, bits, tail) ^ poly_mul(previous, x, bits, tail) ^ 1u128;
assert_eq!(
relation, 0,
"X_{k}² + X_{{k−1}}·X_{k} + 1 ≠ 0 at {bits} bits"
);
previous = x;
}
}
}
#[test]
fn the_change_of_basis_maps_the_generators_to_their_images() {
assert_eq!(tower_to_poly_64(1), 1);
assert_eq!(tower_to_poly_128(1), 1);
for (k, &x) in XI_64.iter().enumerate() {
assert_eq!(u128::from(tower_to_poly_64(1u64 << (1 << k))), x);
}
for (k, &x) in XI_128.iter().enumerate() {
assert_eq!(tower_to_poly_128(1u128 << (1 << k)), x);
}
}
#[test]
fn squaring_the_first_generator_flips_one_bit_of_its_image() {
let image = tower_to_poly_128(0b10);
assert_eq!(image, XI_128[0]);
assert_eq!(poly_square(image, 128, TAIL_128), image ^ 1);
assert_eq!(poly_to_tower_128(image ^ 1), 0b11);
assert_eq!(
BinaryField128::from_repr(0b10)
.reference_mul(BinaryField128::from_repr(0b10))
.to_repr(),
0b11
);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn the_change_of_basis_round_trips(a: u64, b: u128) {
prop_assert_eq!(poly_to_tower_64(tower_to_poly_64(a)), a);
prop_assert_eq!(tower_to_poly_64(poly_to_tower_64(a)), a);
prop_assert_eq!(poly_to_tower_128(tower_to_poly_128(b)), b);
prop_assert_eq!(tower_to_poly_128(poly_to_tower_128(b)), b);
}
#[test]
fn the_change_of_basis_is_a_ring_isomorphism(a: u128, b: u128) {
let product_64 = BinaryField64::from_repr(a as u64)
.reference_mul(BinaryField64::from_repr(b as u64))
.to_repr();
prop_assert_eq!(
poly_mul(
u128::from(tower_to_poly_64(a as u64)),
u128::from(tower_to_poly_64(b as u64)),
64,
TAIL_64,
),
u128::from(tower_to_poly_64(product_64)),
);
let product_128 = BinaryField128::from_repr(a)
.reference_mul(BinaryField128::from_repr(b))
.to_repr();
prop_assert_eq!(
poly_mul(tower_to_poly_128(a), tower_to_poly_128(b), 128, TAIL_128),
tower_to_poly_128(product_128),
);
}
#[test]
fn the_slice_kernels_agree_with_the_matrix(
values in prop::collection::vec(any::<u128>(), 0..300),
offset in 0usize..4,
) {
slices_agree(&values, offset)?;
}
#[test]
fn the_slice_kernels_agree_with_the_per_element_maps(
values in prop::collection::vec(any::<u128>(), 0..300),
) {
let mut forward = values.clone();
tower_to_poly_128_slice(&mut forward);
let want: Vec<u128> = values.iter().map(|&v| tower_to_poly_128(v)).collect();
prop_assert_eq!(&forward, &want);
let mut backward = values.clone();
poly_to_tower_128_slice(&mut backward);
let want: Vec<u128> = values.iter().map(|&v| poly_to_tower_128(v)).collect();
prop_assert_eq!(&backward, &want);
}
#[test]
fn the_change_of_basis_is_additive(a: u128, b: u128) {
prop_assert_eq!(
tower_to_poly_128(a ^ b),
tower_to_poly_128(a) ^ tower_to_poly_128(b),
);
prop_assert_eq!(
tower_to_poly_64(a as u64 ^ b as u64),
tower_to_poly_64(a as u64) ^ tower_to_poly_64(b as u64),
);
}
}
#[test]
fn reference_multiplication_matches_independent_vectors() {
const VECTORS_64: [(u64, u64, u64); 4] = [
(
0xf2ed_66ff_dcc9_9396,
0x21ba_de02_6a6a_e768,
0x4948_cf04_a001_e0dd,
),
(
0x9dd8_904f_0748_9671,
0x6102_dd70_63e8_540e,
0xd4da_4e01_a290_3ef6,
),
(
0x466d_e486_522c_4f8d,
0x83fa_ac57_2f56_4652,
0xd2e1_b9fb_8a75_b624,
),
(
0x0620_f087_7e5f_e381,
0x781b_9a43_d04c_e50b,
0x4dfa_5e46_61fb_eb68,
),
];
const VECTORS_128: [(u128, u128, u128); 4] = [
(
0x21ba_de02_6a6a_e768_f2ed_66ff_dcc9_9396,
0x6102_dd70_63e8_540e_9dd8_904f_0748_9671,
0xe979_0238_73d0_74c5_03a0_3109_a53d_e616,
),
(
0x83fa_ac57_2f56_4652_466d_e486_522c_4f8d,
0x781b_9a43_d04c_e50b_0620_f087_7e5f_e381,
0xa67d_71dc_b60e_3b54_ce1c_34e0_e36d_c1c2,
),
(
0xc35d_7d3b_92e4_016e_27e4_7ffc_284a_2d4f,
0x06e7_df8e_1eb1_c66e_79f7_4d60_ac03_031e,
0x7f0d_3f85_5277_2bd0_ff4f_ffdd_83bf_f1c5,
),
(
0x2e09_e4b8_245e_debc_817a_f708_2074_73b7,
0xb7c0_3984_2be3_8ecc_1f07_a223_563e_bc38,
0x88b0_9d4e_93c3_1775_be08_2cb4_3a09_1df5,
),
];
for (a, b, want) in VECTORS_64 {
let got = BinaryField64::from_repr(a).reference_mul(BinaryField64::from_repr(b));
assert_eq!(got.to_repr(), want, "GF(2^64): {a:#x} * {b:#x}");
}
for (a, b, want) in VECTORS_128 {
let got = BinaryField128::from_repr(a).reference_mul(BinaryField128::from_repr(b));
assert_eq!(got.to_repr(), want, "GF(2^128): {a:#x} * {b:#x}");
}
assert_eq!(
BinaryField128::from_repr(1 << 64).square().to_repr(),
0x0000_0001_0000_0000_0000_0000_0000_0001,
);
assert_eq!(
BinaryField128::from_repr(1 << 64).mul_alpha().to_repr(),
0x0000_0001_0000_0000_0000_0000_0000_0001,
);
}
}