Skip to main content

p3_binary_field/
poly_basis.rs

1//! Raw polynomial coordinates for `GF(2^128)` modulo `x^128 + x^7 + x^2 + x + 1`.
2//!
3//! Prefer the typed field for ordinary arithmetic.
4//! These helpers are for buffers whose representation the caller manages.
5//!
6//! The coordinates stay a bare integer on purpose.
7//! Wrapped, the two bases look alike, and a value in the wrong one is silently wrong.
8
9use alloc::vec::Vec;
10
11use crate::tower::TowerLevel;
12use crate::{BinaryField128, clmul, poly_slice};
13
14/// Whether multiplication and squaring use hardware carryless multiplication.
15///
16/// Otherwise multiplication uses masked integer products and squaring uses bit spreading.
17pub const HAS_HARDWARE_CLMUL: bool = clmul::HAS_HARDWARE_CLMUL;
18
19/// The polynomial-basis coordinates of a tower element.
20#[must_use]
21#[inline]
22pub fn from_tower(x: BinaryField128) -> u128 {
23    clmul::tower_to_poly_128(x.to_repr())
24}
25
26/// The tower element with the given polynomial-basis coordinates.
27#[inline]
28pub fn to_tower(v: u128) -> BinaryField128 {
29    BinaryField128::from_repr(clmul::poly_to_tower_128(v))
30}
31
32/// The polynomial-basis coordinates of a whole slice of tower-basis bit patterns.
33///
34/// # Performance
35///
36/// Whole blocks of 64 elements cross over together on an `x86_64` build with `gfni`, `avx512f`
37/// and `avx512bw`, several times faster than the sixteen table lookups per element that the
38/// remainder falls back to. Every other build takes the per-element path throughout.
39///
40/// The choice is the build's own target features, made at compile time; nothing here detects
41/// the running processor. `-C target-cpu=x86-64-v4` gives `avx512bw` but not `gfni`, so a v4
42/// build is one of the per-element ones.
43///
44/// Naming the three features and nothing else does reach the kernel, but leaves the compiler
45/// no scheduling model to hold a block in registers with. On Zen 5 it then spills every block
46/// through memory, which costs about a third of the throughput a `-C target-cpu` build gets.
47#[inline]
48pub fn from_tower_slice(values: &mut [u128]) {
49    // The count of elements the blocked kernel took is of interest only to its own tests.
50    clmul::tower_to_poly_128_slice(values);
51}
52
53/// The tower-basis bit patterns of a whole slice of polynomial-basis coordinates.
54///
55/// This undoes [`from_tower_slice`], and blocks the same way under the same target features.
56#[inline]
57pub fn to_tower_slice(values: &mut [u128]) {
58    clmul::poly_to_tower_128_slice(values);
59}
60
61/// Multiply two elements expressed in polynomial coordinates.
62// Only the software backend is `const`, so leaving this one out keeps the signature the same
63// on every target.
64#[allow(clippy::missing_const_for_fn)]
65#[must_use]
66#[inline]
67pub fn mul(a: u128, b: u128) -> u128 {
68    clmul::poly_mul_128(a, b)
69}
70
71/// The square of an element, given and returned in the polynomial basis.
72#[allow(clippy::missing_const_for_fn)]
73#[must_use]
74#[inline]
75pub fn square(a: u128) -> u128 {
76    clmul::poly_square_128(a)
77}
78
79/// Multiply every polynomial-basis element by the same scalar.
80#[inline]
81pub fn mul_slice(values: &mut [u128], scalar: u128) {
82    // A lone element has no loop to amortize any setup over, so it takes the plain product.
83    if let [value] = values {
84        *value = mul(*value, scalar);
85        return;
86    }
87    poly_slice::scale(values, scalar);
88}
89
90/// Apply `(lo, hi) -> (lo + scalar*hi, lo + (scalar + 1)*hi)` in place.
91///
92/// # Panics
93/// Panics if the slice lengths differ.
94#[inline]
95pub fn butterfly_forward(lo: &mut [u128], hi: &mut [u128], scalar: u128) {
96    assert_eq!(lo.len(), hi.len(), "butterfly lengths differ");
97    // A zero scalar kills the product, leaving an addition that needs no multiply at all.
98    if scalar == 0 {
99        for (lo, hi) in lo.iter().zip(hi) {
100            *hi ^= *lo;
101        }
102        return;
103    }
104    // A lone pair has no loop to amortize any setup over, so it takes the plain product.
105    if lo.len() == 1 {
106        lo[0] ^= mul(scalar, hi[0]);
107        hi[0] ^= lo[0];
108        return;
109    }
110    poly_slice::butterfly_forward(lo, hi, scalar);
111}
112
113/// Undo [`butterfly_forward`] with the same scalar.
114///
115/// # Panics
116/// Panics if the slice lengths differ.
117#[inline]
118pub fn butterfly_inverse(lo: &mut [u128], hi: &mut [u128], scalar: u128) {
119    assert_eq!(lo.len(), hi.len(), "butterfly lengths differ");
120    // A zero scalar kills the product, leaving an addition that needs no multiply at all.
121    if scalar == 0 {
122        for (lo, hi) in lo.iter().zip(hi) {
123            *hi ^= *lo;
124        }
125        return;
126    }
127    // A lone pair has no loop to amortize any setup over, so it takes the plain product.
128    if lo.len() == 1 {
129        hi[0] ^= lo[0];
130        lo[0] ^= mul(scalar, hi[0]);
131        return;
132    }
133    poly_slice::butterfly_inverse(lo, hi, scalar);
134}
135
136/// Stages [`LowStageTwiddles::forward`] runs together, over runs of `2^LOW_STAGES` elements.
137pub const LOW_STAGES: usize = poly_slice::LOW_STAGES;
138
139/// A value beside its product with `x^64`, the companion the packed multiply scales by.
140///
141/// The product is linear in the value, so a sum of pairs is the pair of the sum.
142type Pair = [u128; 2];
143
144/// The sum of two values, each beside its companion.
145#[inline(always)]
146const fn add(a: Pair, b: Pair) -> Pair {
147    [a[0] ^ b[0], a[1] ^ b[1]]
148}
149
150/// The twiddles of the lowest [`LOW_STAGES`] stages of a single-column additive transform.
151///
152/// Stage `j` pairs elements `2^j` apart, and block `b` of that stage, its `2^(j+1)` elements
153/// from `2^(j+1) b` on, scales by one twiddle, affine in `b`:
154///
155/// ```text
156///     twiddle(j, b) = shifts[j] + Σ basis[k]    over the set bits k of b
157/// ```
158///
159/// A run of `2^LOW_STAGES` elements is closed under all of those stages, so they run together
160/// while the run sits in registers, instead of in one sweep of the slice each.
161#[derive(Clone, Debug)]
162pub struct LowStageTwiddles {
163    /// `shifts[j]` for each low stage.
164    shifts: [Pair; LOW_STAGES],
165    /// `prefix[n]` is the sum of the first `n` basis elements.
166    prefix: Vec<Pair>,
167    /// `steps[t][j]` is what stage `j`'s twiddle moves by past a run index with `t` trailing
168    /// ones, for every `t` a run inside the basis can have.
169    steps: Vec<[Pair; LOW_STAGES]>,
170}
171
172impl LowStageTwiddles {
173    /// The twiddles of a transform with these stage shifts and this block basis.
174    ///
175    /// # Panics
176    /// Panics if `shifts` holds fewer than [`LOW_STAGES`] stages, or `basis` holds fewer than
177    /// `LOW_STAGES - 1` elements.
178    pub fn new(shifts: &[u128], basis: &[u128]) -> Self {
179        assert!(
180            basis.len() >= LOW_STAGES - 1,
181            "the low stages need at least three basis elements"
182        );
183        let pair = |value: u128| [value, clmul::poly_mul_128(value, 1 << 64)];
184        let prefix: Vec<Pair> = core::iter::once(0)
185            .chain(basis.iter().scan(0, |sum, &element| {
186                *sum ^= element;
187                Some(*sum)
188            }))
189            .map(pair)
190            .collect();
191        // Stepping a run index with `t` trailing ones flips its `t + 1` lowest bits, which stage
192        // `j` reads from `basis[LOW_STAGES - 1 - j]` on. Stage 0 reads the highest, so it is
193        // what bounds `t`.
194        let steps = (0..(basis.len() + 1).saturating_sub(LOW_STAGES))
195            .map(|trailing| {
196                core::array::from_fn(|j| {
197                    let from = LOW_STAGES - 1 - j;
198                    add(prefix[from + trailing + 1], prefix[from])
199                })
200            })
201            .collect();
202        Self {
203            shifts: core::array::from_fn(|j| pair(shifts[j])),
204            prefix,
205            steps,
206        }
207    }
208
209    /// Runs the low stages of the forward transform over `values`.
210    ///
211    /// `values` starts at element `2^LOW_STAGES · first_run` of the transform, and ends on a
212    /// whole run of `2^LOW_STAGES` elements.
213    ///
214    /// # Panics
215    /// Panics if `values` ends inside a run, or if a run lies past the basis the twiddles hold.
216    pub fn forward(&self, values: &mut [u128], first_run: usize) {
217        assert_eq!(
218            values.len() % (1 << LOW_STAGES),
219            0,
220            "the low stages run over whole runs"
221        );
222        poly_slice::forward_low_stages(values, first_run, self);
223    }
224
225    /// The sum of the basis elements the set bits of `bits` select, counted from `basis[from]`.
226    #[inline(always)]
227    pub(crate) fn span(&self, mut bits: usize, from: usize) -> Pair {
228        let mut sum = [0; 2];
229        while bits != 0 {
230            let k = from + bits.trailing_zeros() as usize;
231            sum = add(sum, add(self.prefix[k + 1], self.prefix[k]));
232            bits &= bits - 1;
233        }
234        sum
235    }
236
237    /// Each low stage's twiddle for the first pair of run `run`.
238    ///
239    /// Stage `j` sees the run as `2^(LOW_STAGES-1-j)` of its blocks, so the run index reaches
240    /// its twiddle shifted up by that many basis elements.
241    #[inline(always)]
242    pub(crate) fn run_twiddles(&self, run: usize) -> [Pair; LOW_STAGES] {
243        core::array::from_fn(|j| add(self.shifts[j], self.span(run, LOW_STAGES - 1 - j)))
244    }
245
246    /// What each low stage's twiddle moves by from run `run` to the run after it.
247    ///
248    /// Stepping `run` flips its trailing ones and the zero above them.
249    /// So each stage's twiddle moves by the sum of that many consecutive basis elements.
250    #[inline(always)]
251    pub(crate) fn step(&self, run: usize) -> &[Pair; LOW_STAGES] {
252        &self.steps[run.trailing_ones() as usize]
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use alloc::vec::Vec;
259
260    use p3_field::PrimeCharacteristicRing;
261    use proptest::prelude::*;
262
263    use super::{LOW_STAGES, LowStageTwiddles, from_tower, mul, square, to_tower};
264    use crate::BinaryField128;
265
266    /// Elements in one run of the low stages.
267    const LOW_RUN: usize = 1 << LOW_STAGES;
268
269    /// Basis elements the low-stage tests draw, enough for every run index they reach.
270    const LOW_BASIS: usize = 40;
271
272    /// Building an element from a 128-bit pattern, which every pattern is a valid one of.
273    fn element(bits: u128) -> BinaryField128 {
274        BinaryField128::from_le_bytes(bits.to_le_bytes())
275    }
276
277    /// The low stages one stage at a time over the whole slice, each pair scaled by the twiddle
278    /// its block index selects, summed straight from the basis.
279    fn reference_low_stages(
280        values: &mut [u128],
281        first_run: usize,
282        shifts: &[u128],
283        basis: &[u128],
284    ) {
285        let first = first_run << LOW_STAGES;
286        for j in (0..LOW_STAGES).rev() {
287            let half = 1 << j;
288            for (index, pair) in values.chunks_exact_mut(2 * half).enumerate() {
289                let block = (first >> (j + 1)) + index;
290                let twiddle = (0..basis.len())
291                    .filter(|k| (block >> k) & 1 == 1)
292                    .fold(shifts[j], |twiddle, k| twiddle ^ basis[k]);
293                let (lo, hi) = pair.split_at_mut(half);
294                for (lo, hi) in lo.iter_mut().zip(hi) {
295                    *lo ^= mul(twiddle, *hi);
296                    *hi ^= *lo;
297                }
298            }
299        }
300    }
301
302    /// Run indices whose successors carry across many bits, beside small and random ones.
303    fn run_index() -> impl Strategy<Value = usize> {
304        prop_oneof![
305            0usize..64,
306            Just((1 << 20) - 1),
307            Just((1 << 24) - 2),
308            0usize..1 << 24,
309        ]
310    }
311
312    proptest! {
313        /// Invariant: running the low stages together within each run reorders nothing that
314        /// depends on order, so every element matches the stage-at-a-time schedule.
315        #[test]
316        fn the_low_stages_match_one_stage_at_a_time(
317            shifts in any::<[u128; LOW_STAGES]>(),
318            basis in prop::collection::vec(any::<u128>(), LOW_BASIS),
319            values in prop::collection::vec(any::<u128>(), 5 * LOW_RUN),
320            runs in 0usize..=5,
321            first_run in run_index(),
322        ) {
323            let mut actual: Vec<u128> = values[..runs * LOW_RUN].to_vec();
324            let mut expected = actual.clone();
325            reference_low_stages(&mut expected, first_run, &shifts, &basis);
326            LowStageTwiddles::new(&shifts, &basis).forward(&mut actual, first_run);
327            prop_assert_eq!(actual, expected);
328        }
329
330        #[test]
331        fn the_change_of_basis_round_trips(bits in any::<u128>()) {
332            let x = element(bits);
333            prop_assert_eq!(to_tower(from_tower(x)), x);
334        }
335
336        /// Addition is `XOR` in both bases, so the conversion commutes with it.
337        #[test]
338        fn the_change_of_basis_is_additive(a in any::<u128>(), b in any::<u128>()) {
339            let (x, y) = (element(a), element(b));
340            prop_assert_eq!(from_tower(x + y), from_tower(x) ^ from_tower(y));
341        }
342
343        /// The polynomial-basis product is the tower product, seen in the other basis.
344        #[test]
345        fn the_product_agrees_with_the_tower_product(a in any::<u128>(), b in any::<u128>()) {
346            let (x, y) = (element(a), element(b));
347            prop_assert_eq!(to_tower(mul(from_tower(x), from_tower(y))), x * y);
348        }
349
350        #[test]
351        fn the_square_agrees_with_the_tower_square(a in any::<u128>()) {
352            let x = element(a);
353            prop_assert_eq!(to_tower(square(from_tower(x))), x.square());
354        }
355    }
356
357    #[test]
358    fn slice_operations_match_reference_tower_arithmetic() {
359        use alloc::vec::Vec;
360        // A packed kernel covers whole registers of two or four elements.
361        //
362        // The lengths below therefore hit every prefix and tail combination.
363        for len in [0, 1, 2, 3, 4, 5, 7, 8, 15, 16, 17, 64, 257] {
364            // A zero scalar takes the addition-only shortcut and one is the identity.
365            for scalar in [
366                0,
367                1,
368                0x87,
369                1 << 127,
370                0xfeed_9876_0123_4567_89ab_cdef_9876_5432,
371            ] {
372                let t = element(scalar);
373                let xs: Vec<_> = (0..len)
374                    .map(|i| {
375                        element(
376                            (i as u128 + 1).wrapping_mul(0xfeed_8765_dead_beef_cafe_0123_4567_89ab),
377                        )
378                    })
379                    .collect();
380                let ys: Vec<_> = xs.iter().rev().copied().collect();
381                let mut lo: Vec<_> = xs.iter().copied().map(from_tower).collect();
382                let mut hi: Vec<_> = ys.iter().copied().map(from_tower).collect();
383                let original = (lo.clone(), hi.clone());
384                super::butterfly_forward(&mut lo, &mut hi, from_tower(t));
385                for i in 0..len {
386                    let expected = xs[i] + ys[i].reference_mul(t);
387                    assert_eq!(to_tower(lo[i]), expected);
388                    assert_eq!(to_tower(hi[i]), expected + ys[i]);
389                }
390                super::butterfly_inverse(&mut lo, &mut hi, from_tower(t));
391                assert_eq!((&lo, &hi), (&original.0, &original.1));
392                super::mul_slice(&mut lo, from_tower(t));
393                for i in 0..len {
394                    assert_eq!(to_tower(lo[i]), xs[i].reference_mul(t));
395                }
396            }
397        }
398    }
399
400    #[test]
401    #[should_panic = "the low stages run over whole runs"]
402    fn the_low_stages_reject_a_partial_run() {
403        let twiddles = LowStageTwiddles::new(&[1; LOW_STAGES], &[1; LOW_BASIS]);
404        twiddles.forward(&mut [0; LOW_RUN + 1], 0);
405    }
406
407    #[test]
408    #[should_panic(expected = "the low stages need at least three basis elements")]
409    fn the_low_stages_reject_a_short_basis() {
410        LowStageTwiddles::new(&[1; LOW_STAGES], &[1; LOW_STAGES - 2]);
411    }
412
413    #[test]
414    #[should_panic = "butterfly lengths differ"]
415    fn forward_rejects_mismatched_lengths() {
416        super::butterfly_forward(&mut [0], &mut [], 0);
417    }
418
419    #[test]
420    #[should_panic = "butterfly lengths differ"]
421    fn inverse_rejects_mismatched_lengths() {
422        super::butterfly_inverse(&mut [], &mut [0], 1);
423    }
424
425    /// The basis change fixes zero and one, as any field isomorphism must.
426    #[test]
427    fn the_change_of_basis_fixes_the_constants() {
428        assert_eq!(from_tower(BinaryField128::ZERO), 0);
429        assert_eq!(from_tower(BinaryField128::ONE), 1);
430        assert_eq!(to_tower(0), BinaryField128::ZERO);
431        assert_eq!(to_tower(1), BinaryField128::ONE);
432    }
433}