diff --git a/README.md b/README.md index cefc6f0..f2d1e2e 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ Ramified model: | deg2 + deg2 (char2) | GF(2^8) | ~185 ns | | deg2 + deg2 (char2) | GF(2^16) | ~600 ns | | 2*deg2 (not_char2) | F_65521 | ~194 ns | +| deg2 + deg2 (not_char2) | F_p (56-bit) | ~741 ns | +| 2*deg2 (not_char2) | F_p (56-bit) | ~682 ns | Split model (degree-2 balanced divisors, negative basis): @@ -125,6 +127,63 @@ Run benchmarks with: cargo bench ``` +### Field-operation counts + +Wall-clock comparisons across implementations are confounded by field size, +operation definition, and language, so the explicit-formula literature compares +**field-operation counts** instead (field-size independent). Counts below are +for the generic-branch degree-2 `add`/`double` (negative basis), measured by +running the actual formulas over an instrumented field +(`cargo test --release g2::split::op_counts -- --nocapture`): + +| Operation | M (mul) | S (sqr) | I (inv) | A (add/sub/dbl) | +|-----------|--------:|--------:|--------:|----------------:| +| not_char2 — add | 26 | 2 | 1 | 37 | +| not_char2 — double | 31 | 3 | 1 | 38 | +| arbitrary — add | 30 | 1 | 1 | 36 | +| arbitrary — double | 38 | 2 | 1 | 44 | +| char2 — add | 27 | 1 | 1 | 34 | +| char2 — double | 29 | 2 | 1 | 31 | + +Each group operation uses exactly **one** field inversion (affine formulas). +These can be compared directly to the per-formula counts in Lange, Erickson– +Jacobson–Stein (real genus 2), and Costello–Lauter. + +### Wall-clock comparison with smalljac + +[smalljac](https://math.mit.edu/~drew/smalljac.html) (Andrew Sutherland) is a +highly optimized C library whose `hecurve_g2_compose` / `hecurve_g2_square` +implement the genus-2 **imaginary** (ramified, `deg f = 5`) group law — the same +model as this crate's `g2::ramified::not_char2`. Built and timed on the same +machine (smalljac v4.1.3 + ff_poly v1.2.7, ported to arm64), exercising the +affine path (`ctx = NULL`, one field inversion per op — matching this crate's +affine formulas): + +| field | operation | this crate (ramified nch2) | smalljac | +|-------|-----------|---------------------------:|---------:| +| p = 65521 (16-bit) | add / double | 149 / 194 ns | 767 / 873 ns | +| 56-bit prime (matched width) | add / double | 741 / 682 ns | 1470 / 1619 ns | + +**These numbers need context — cross-implementation wall-clock is confounded:** + +- **Field width dominates.** ff_poly is compiled for ≤57-bit primes and always + does 64-bit-wide Montgomery arithmetic, so smalljac barely changes from 16-bit + to 56-bit (767 → 1470 ns); this crate's `PrimeField` uses `u128`-multiply + + hardware modulo, much faster at 16-bit but scaling up with the modulus. **Only + the matched 56-bit row is a fair wall-clock comparison.** +- **Batched inversion is disabled.** smalljac's real strength for point counting + is amortizing one inversion across many group ops (Montgomery's trick, via its + `ctx` state machine). Forcing `ctx = NULL` measures its un-batched affine path + — the right comparison for a *single* op, but not how smalljac runs in anger. +- **Specialized vs general.** This crate's `add`/`double` are degree-2-specialized + explicit formulas (≈26 M, 1 I); `hecurve_g2_compose` is a general composition + routine that also handles the degenerate-degree cases. + +So the field-operation counts above remain the cleaner, field-size-independent +comparison; the matched-width wall-clock merely confirms the specialized +explicit formulas are competitive with a mature C implementation. The harness +and build notes are in [`benches/smalljac-compare/`](benches/smalljac-compare/). + ## Testing Run all tests: diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index a295b64..c21121f 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -477,10 +477,13 @@ fn not_char2_benchmarks(c: &mut Criterion) { bench_not_char2_deg2_add::>(c, "F7"); bench_not_char2_deg2_add::>(c, "F8191"); bench_not_char2_deg2_add::>(c, "F65521"); + // ~56-bit prime: matched-width comparison vs smalljac (built for 57-bit primes). + bench_not_char2_deg2_add::>(c, "Fp56"); bench_not_char2_deg2_dbl::>(c, "F7"); bench_not_char2_deg2_dbl::>(c, "F8191"); bench_not_char2_deg2_dbl::>(c, "F65521"); + bench_not_char2_deg2_dbl::>(c, "Fp56"); } fn arbitrary_benchmarks(c: &mut Criterion) { diff --git a/benches/smalljac-compare/README.md b/benches/smalljac-compare/README.md new file mode 100644 index 0000000..7a87a34 --- /dev/null +++ b/benches/smalljac-compare/README.md @@ -0,0 +1,61 @@ +# smalljac wall-clock comparison + +[`bench.c`](bench.c) times smalljac's genus-2 imaginary/ramified group law +(`hecurve_g2_compose` = add, `hecurve_g2_square` = double) on the affine path +(`ctx = NULL`, one field inversion per op), so it can be put next to this +crate's `g2::ramified::not_char2` `cargo bench` numbers. See the +"Wall-clock comparison with smalljac" section of the top-level +[`README.md`](../../README.md) for the results and the caveats. + +smalljac and ff_poly are **not vendored** here (they are GPL; this crate is +MIT). Download them yourself: + +- smalljac v4.1.3 and ff_poly v1.2.7: +- GMP (e.g. `brew install gmp` on macOS) + +## Building on x86-64 Linux + +ff_poly's hand-written x86-64 assembly works as-is there: + +```sh +# build libff_poly.a per its Makefile, then: +cc -O3 -std=gnu99 -DSMALLJAC_GENUS=2 \ + -I/path/to/ff_poly -I/path/to/smalljac bench.c \ + hecurve.c hecurve1.c hecurve2.c mpzutil.c prime.c \ + /path/to/libff_poly.a -lgmp -o sjbench +./sjbench +``` + +## Building on arm64 (Apple Silicon) + +ff_poly and smalljac use x86-64 inline asm (`mulq`, `bsrq`, `bsfq`, …). Replace +it with portable `__uint128_t` / `__builtin_*` equivalents (semantics identical; +clang lowers the 128-bit ops to native `mul`/`umulh`/`clz`/`ctz` on aarch64). + +**1. Replace `ff_poly/asm.h`** with these portable macros: + +```c +#define _asm_div_q_q(q,r,x,y) do { __uint128_t _n=((__uint128_t)(unsigned long)(r)<<64)|(unsigned long)(x); (q)=(unsigned long)(_n/(unsigned long)(y)); (r)=(unsigned long)(_n%(unsigned long)(y)); } while(0) +#define _asm_mult_1_1(z1,z0,x0,y0) do { __uint128_t _p=(__uint128_t)(unsigned long)(x0)*(unsigned long)(y0); (z0)=(unsigned long)_p; (z1)=(unsigned long)(_p>>64); } while(0) +#define _asm_mult_2_2_1(z1,z0,x1,x0,y0) do { __uint128_t _p=(__uint128_t)(unsigned long)(x0)*(unsigned long)(y0); (z0)=(unsigned long)_p; (z1)=(unsigned long)(_p>>64)+(unsigned long)(x1)*(unsigned long)(y0); } while(0) +#define _asm_addto_2_2(z1,z0,x1,x0) do { __uint128_t _s=(((__uint128_t)(unsigned long)(z1)<<64)|(unsigned long)(z0))+(((__uint128_t)(unsigned long)(x1)<<64)|(unsigned long)(x0)); (z0)=(unsigned long)_s; (z1)=(unsigned long)(_s>>64); } while(0) +#define _asm_addto_2_1(z1,z0,x0) do { __uint128_t _s=(((__uint128_t)(unsigned long)(z1)<<64)|(unsigned long)(z0))+(unsigned long)(x0); (z0)=(unsigned long)_s; (z1)=(unsigned long)(_s>>64); } while(0) +#define _asm_addto_3_3(z2,z1,z0,x2,x1,x0) do { __uint128_t _s=(__uint128_t)(unsigned long)(z0)+(unsigned long)(x0); (z0)=(unsigned long)_s; _s=(__uint128_t)(unsigned long)(z1)+(unsigned long)(x1)+(unsigned long)(_s>>64); (z1)=(unsigned long)_s; (z2)=(unsigned long)(z2)+(unsigned long)(x2)+(unsigned long)(_s>>64); } while(0) +#define _asm_addto_3_2(z2,z1,z0,x1,x0) do { __uint128_t _s=(__uint128_t)(unsigned long)(z0)+(unsigned long)(x0); (z0)=(unsigned long)_s; _s=(__uint128_t)(unsigned long)(z1)+(unsigned long)(x1)+(unsigned long)(_s>>64); (z1)=(unsigned long)_s; (z2)=(unsigned long)(z2)+(unsigned long)(_s>>64); } while(0) +#define _asm_subfrom_2_2(z1,z0,x1,x0) do { __uint128_t _d=(((__uint128_t)(unsigned long)(z1)<<64)|(unsigned long)(z0))-(((__uint128_t)(unsigned long)(x1)<<64)|(unsigned long)(x0)); (z0)=(unsigned long)_d; (z1)=(unsigned long)(_d>>64); } while(0) +#define _asm_inc_2(z1,z0) do { if(++(z0)==0UL) (z1)++; } while(0) +``` + +(plus the `_asm_mult_3_2_1` / `_asm_mult_3_2_2` / `_asm_square_3_2` helpers, +which are already written in terms of the macros above — keep them as-is.) + +**2. In both `cstd.h` files** (the ff_poly copy *and* the smalljac copy) replace +the two bit-scan helpers: + +```c +static inline unsigned long _asm_highbit (unsigned long x) { return 63UL - __builtin_clzl(x); } +static inline unsigned long _asm_lowbit (unsigned long x) { return __builtin_ctzl(x); } +``` + +Then build with the same `cc` line as above. ff_poly is then limited to +`FF_BITS = 57`, which is why the matched-width comparison uses a 56-bit prime. diff --git a/benches/smalljac-compare/bench.c b/benches/smalljac-compare/bench.c new file mode 100644 index 0000000..8128101 --- /dev/null +++ b/benches/smalljac-compare/bench.c @@ -0,0 +1,90 @@ +/* Micro-benchmark for smalljac's genus-2 Jacobian group law + * (hecurve_g2_compose = ADD, hecurve_g2_square = DBL), to compare against the + * Rust `divisor-arithmetic` cargo-bench numbers for g2::ramified::not_char2. + * + * Imaginary/ramified genus-2 model: y^2 = f(x), deg f = 5 (monic). + * Times the affine path (ctx = NULL => one field inversion per op), which is + * the same affine, one-inversion model the Rust explicit formulas use. + * + * Build: see README.md in this directory (requires an external smalljac + + * ff_poly checkout; on arm64 the x86-64 inline asm must be replaced — the + * README lists the two portable patches). + */ +#include +#include +#include +#include +#include "ff_poly.h" +#include "mpzutil.h" +#include "hecurve.h" +#include "cstd.h" + +#define POOL 256 + +static double now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1e9 + ts.tv_nsec; +} + +static void run(unsigned long p, const char *label) { + ff_setup_ui(p); + + /* monic degree-5 f with random coefficients f[0..4], f[5]=1, f[6]=0 */ + ff_t f[HECURVE_DEGREE + 1]; + for (int i = 0; i < HECURVE_DEGREE + 1; i++) _ff_set_zero(f[i]); + for (int i = 0; i < 5; i++) _ff_random(f[i]); + _ff_set_one(f[5]); + + /* pool of valid random weight-2 divisors */ + static ff_t U[POOL][HECURVE_GENUS + 1], V[POOL][HECURVE_GENUS]; + int n = 0; + for (int tries = 0; tries < POOL * 50 && n < POOL; tries++) { + ff_t u[HECURVE_GENUS + 1], v[HECURVE_GENUS]; + hecurve_random(u, v, f); + if (_ff_zero(u[2]) || !_ff_one(u[2])) continue; /* keep monic deg-2 u */ + if (!hecurve_verify(u, v, f)) continue; + for (int i = 0; i < HECURVE_GENUS + 1; i++) _ff_set(U[n][i], u[i]); + for (int i = 0; i < HECURVE_GENUS; i++) _ff_set(V[n][i], v[i]); + n++; + } + if (n < 8) { printf("%-12s p=%lu: only %d divisors, skipping\n", label, p, n); return; } + + ff_t ru[HECURVE_GENUS + 1], rv[HECURVE_GENUS]; + unsigned long sink = 0; + long iters = 2000000; + + /* ---- ADD (compose distinct divisors) ---- */ + for (int i = 0; i < n - 1; i++) hecurve_g2_compose(ru, rv, U[i], V[i], U[i+1], V[i+1], f, 0); + double t0 = now_ns(); + for (long k = 0; k < iters; k++) { + int i = k % (n - 1); + hecurve_g2_compose(ru, rv, U[i], V[i], U[i+1], V[i+1], f, 0); + sink += _ff_get_ui(ru[0]); + } + double add_ns = (now_ns() - t0) / iters; + + /* ---- DBL (square a divisor) ---- */ + for (int i = 0; i < n; i++) hecurve_g2_square(ru, rv, U[i], V[i], f, 0); + t0 = now_ns(); + for (long k = 0; k < iters; k++) { + int i = k % n; + hecurve_g2_square(ru, rv, U[i], V[i], f, 0); + sink += _ff_get_ui(ru[0]); + } + double dbl_ns = (now_ns() - t0) / iters; + + printf("%-12s p=%-20lu add=%7.1f ns double=%7.1f ns (n=%d, sink=%lu)\n", + label, p, add_ns, dbl_ns, n, sink); +} + +int main(void) { + run(8191UL, "13-bit"); + run(65521UL, "16-bit"); + run(1048583UL, "20-bit"); + run(1000000007UL, "30-bit"); + run((1UL << 31) - 1, "31-bit M31"); + /* 56-bit prime: matched-width comparison vs Rust PrimeField<72057594037927931> */ + run(72057594037927931UL, "56-bit Fp56"); + return 0; +} diff --git a/src/g2/split/mod.rs b/src/g2/split/mod.rs index b27f52b..f33f625 100644 --- a/src/g2/split/mod.rs +++ b/src/g2/split/mod.rs @@ -64,6 +64,8 @@ mod blackbox_tests; #[cfg(test)] mod char2_tests; #[cfg(test)] +mod op_counts; +#[cfg(test)] mod test_support; #[cfg(test)] mod wb_vectors; diff --git a/src/g2/split/op_counts.rs b/src/g2/split/op_counts.rs new file mode 100644 index 0000000..d9e9c93 --- /dev/null +++ b/src/g2/split/op_counts.rs @@ -0,0 +1,349 @@ +//! Field-operation counts (M = mul, S = square, I = inverse, A = add/sub/double) +//! for the generic-branch degree-2 `add` and `double` of each split variant. +//! +//! These are field-size-independent and are the metric the explicit-formula +//! literature uses for comparison (e.g. Lange; Erickson–Jacobson–Stein; +//! Costello–Lauter). Run with: +//! `cargo test --release g2::split::op_counts -- --nocapture` +//! +//! Counts are obtained by running the *actual* explicit formulas over a +//! `CountingField` wrapper on valid degree-2 inputs (so they reflect the code, +//! not a hand count). Setup (curve precompute + building the divisors) is not +//! counted — only the measured operation. + +// Builder return types are (CurveConstants, Divisor, Divisor) tuples. +#![allow(clippy::type_complexity)] + +use std::cell::Cell; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::field::{BinaryExtField, Field, PrimeField}; +use crate::generic::split::{self as gsplit, Divisor}; +use crate::poly::Poly; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +use super::test_support::{from_generic, try_sqrt}; +use super::{arbitrary as sarb, char2 as sch2, not_char2 as snch2}; + +// --- op counters (M, S, I, A) --- +thread_local! { + static OPS: Cell<[u64; 4]> = const { Cell::new([0; 4]) }; +} +fn bump(i: usize) { + OPS.with(|c| { + let mut a = c.get(); + a[i] += 1; + c.set(a); + }); +} +fn reset() { + OPS.with(|c| c.set([0; 4])); +} +fn snap() -> [u64; 4] { + OPS.with(|c| c.get()) +} + +/// A field wrapper that tallies field operations. Squaring, doubling, and +/// inversion are counted distinctly (the formulas call `.square()`/`.double()`/ +/// `.inv()`), so the tallies match how the literature reports M/S/I. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct Cf(F); + +impl Add for Cf { + type Output = Self; + fn add(self, r: Self) -> Self { + bump(3); + Cf(self.0 + r.0) + } +} +impl Sub for Cf { + type Output = Self; + fn sub(self, r: Self) -> Self { + bump(3); + Cf(self.0 - r.0) + } +} +impl Mul for Cf { + type Output = Self; + fn mul(self, r: Self) -> Self { + bump(0); + Cf(self.0 * r.0) + } +} +impl Div for Cf { + type Output = Self; + fn div(self, r: Self) -> Self { + bump(2); // an inversion + bump(0); // and a multiply + Cf(self.0 / r.0) + } +} +impl Neg for Cf { + type Output = Self; + fn neg(self) -> Self { + Cf(-self.0) // negation is free (XOR / sign flip) + } +} +impl AddAssign for Cf { + fn add_assign(&mut self, r: Self) { + *self = *self + r; + } +} +impl SubAssign for Cf { + fn sub_assign(&mut self, r: Self) { + *self = *self - r; + } +} +impl MulAssign for Cf { + fn mul_assign(&mut self, r: Self) { + *self = *self * r; + } +} +impl DivAssign for Cf { + fn div_assign(&mut self, r: Self) { + *self = *self / r; + } +} +impl Field for Cf { + fn zero() -> Self { + Cf(F::zero()) + } + fn one() -> Self { + Cf(F::one()) + } + fn is_zero(&self) -> bool { + self.0.is_zero() + } + fn inv(&self) -> Self { + bump(2); + Cf(self.0.inv()) + } + fn square(&self) -> Self { + bump(1); + Cf(self.0.square()) + } + fn double(&self) -> Self { + bump(3); + Cf(self.0.double()) + } + fn random(rng: &mut R) -> Self { + Cf(F::random(rng)) + } +} + +impl std::fmt::Display for Cf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// --- valid degree-2 divisor builders over an arbitrary (counted) field --- + +/// Lift point `(a,b)` to a reduced degree-1 divisor and compose two of them +/// into a degree-2 divisor via the generic oracle (negative basis). +fn deg2_from_points(f: &Poly, h: &Poly, vn: &Poly, p: &[(F, F)]) -> Divisor { + let deg1 = |a: F, b: F| { + let u = Poly::from_coeffs(vec![-a, F::one()]); + let r = (vn - &Poly::constant(b)).rem(&u); + let vhat = vn - &r; + let w = (f - &(&vhat * &(&vhat + h))).exact_div(&u); + Divisor::new(u, vhat, w, 0) + }; + gsplit::add_neg(°1(p[0].0, p[0].1), °1(p[1].0, p[1].1), f, h, vn, 2) +} + +// nch2 (odd characteristic, h = 0) +fn nch2_inputs() -> ( + snch2::CurveConstants>>, + Divisor>>, + Divisor>>, +) { + type F = Cf>; + let mut rng = StdRng::seed_from_u64(2024); + loop { + let cc = super::test_support::random_curve::(&mut rng); + let (f, vn, h) = (cc.f_poly(), cc.vn(), Poly::zero()); + let mut pts = Vec::new(); + for _ in 0..2000 { + let a = F::random(&mut rng); + if let Some(b) = try_sqrt(f.eval(a), &mut rng) { + pts.push((a, b)); + if pts.len() == 4 { + break; + } + } + } + if pts.len() < 4 { + continue; + } + let d1 = deg2_from_points(&f, &h, &vn, &pts[0..2]); + let d2 = deg2_from_points(&f, &h, &vn, &pts[2..4]); + if d1.u.deg() == 2 && d2.u.deg() == 2 { + return (cc, d1, d2); + } + } +} + +// arbitrary characteristic (h != 0), over an odd prime field +fn arb_inputs() -> ( + sarb::CurveConstants>>, + Divisor>>, + Divisor>>, +) { + type F = Cf>; + let mut rng = StdRng::seed_from_u64(2025); + let two_inv = (F::one() + F::one()).inv(); + loop { + let h = [ + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + ]; + let y3 = F::random(&mut rng); + if (y3.double() + h[3]).is_zero() { + continue; + } + let f6 = y3 * y3 + h[3] * y3; + if f6.is_zero() { + continue; + } + let f = [ + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + f6, + ]; + let cc = sarb::precompute(f, h, y3); + let (fp, hp, vn) = (cc.f_poly(), cc.h_poly(), cc.vn()); + let mut pts = Vec::new(); + for _ in 0..2000 { + let a = F::random(&mut rng); + let (ha, fa) = (hp.eval(a), fp.eval(a)); + if let Some(s) = try_sqrt(ha * ha + (fa + fa).double(), &mut rng) { + pts.push((a, (s - ha) * two_inv)); + if pts.len() == 4 { + break; + } + } + } + if pts.len() < 4 { + continue; + } + let d1 = deg2_from_points(&fp, &hp, &vn, &pts[0..2]); + let d2 = deg2_from_points(&fp, &hp, &vn, &pts[2..4]); + if d1.u.deg() == 2 && d2.u.deg() == 2 { + return (cc, d1, d2); + } + } +} + +// characteristic 2, over GF(2^8) +fn char2_inputs() -> ( + sch2::CurveConstants>>, + Divisor>>, + Divisor>>, +) { + type Inner = BinaryExtField<8>; + type F = Cf; + let mut rng = StdRng::seed_from_u64(2026); + let solve = |cc: &sch2::CurveConstants, a: F| -> Option { + let (ha, fa) = (cc.h_poly().eval(a), cc.f_poly().eval(a)); + (0..256u64) + .map(|i| Cf(Inner::new(i))) + .find(|&b| b * b + ha * b == fa) + }; + loop { + let beta = F::random(&mut rng); + let f6 = beta.square() + beta; + if f6.is_zero() { + continue; + } + let cc = sch2::precompute( + F::random(&mut rng), + F::random(&mut rng), + F::random(&mut rng), + f6, + F::random(&mut rng), + F::random(&mut rng), + beta, + ); + let (fp, hp, vn) = (cc.f_poly(), cc.h_poly(), cc.vn()); + let mut pts = Vec::new(); + for ai in 0..256u64 { + let a = Cf(Inner::new(ai)); + if let Some(b) = solve(&cc, a) { + pts.push((a, b)); + if pts.len() == 4 { + break; + } + } + } + if pts.len() < 4 { + continue; + } + let d1 = deg2_from_points(&fp, &hp, &vn, &pts[0..2]); + let d2 = deg2_from_points(&fp, &hp, &vn, &pts[2..4]); + if d1.u.deg() == 2 && d2.u.deg() == 2 { + return (cc, d1, d2); + } + } +} + +fn fmt_row(name: &str, [m, s, i, a]: [u64; 4]) -> String { + format!("| {name:<22} | {m:>3} | {s:>3} | {i:>2} | {a:>3} |") +} + +#[test] +fn op_count_table() { + let mut rows = Vec::new(); + let mut measure = |name: &str, counts: [u64; 4]| { + // The affine explicit formulas perform exactly one field inversion per + // group operation; if this ever changes, a branch is mis-selected or the + // formula regressed. + assert_eq!( + counts[2], 1, + "{name}: expected exactly 1 inversion, got {counts:?}" + ); + rows.push(fmt_row(name, counts)); + }; + + let (cc, d1, d2) = nch2_inputs(); + let (c1, c2) = (from_generic(&d1), from_generic(&d2)); + reset(); + let _ = snch2::add_neg(&c1, &c2, &cc); + measure("nch2 deg2 add", snap()); + reset(); + let _ = snch2::double_neg(&c1, &cc); + measure("nch2 deg2 double", snap()); + + let (cc, d1, d2) = arb_inputs(); + let (c1, c2) = (from_generic(&d1), from_generic(&d2)); + reset(); + let _ = sarb::add_neg(&c1, &c2, &cc); + measure("arb deg2 add", snap()); + reset(); + let _ = sarb::double_neg(&c1, &cc); + measure("arb deg2 double", snap()); + + let (cc, d1, d2) = char2_inputs(); + let (c1, c2) = (from_generic(&d1), from_generic(&d2)); + reset(); + let _ = sch2::add_neg(&c1, &c2, &cc); + measure("char2 deg2 add", snap()); + reset(); + let _ = sch2::double_neg(&c1, &cc); + measure("char2 deg2 double", snap()); + + println!("\nGeneric-branch degree-2 field-operation counts (negative basis):\n"); + println!("| operation | M | S | I | A |"); + println!("|------------------------|-----|-----|----|-----|"); + for r in &rows { + println!("{r}"); + } + println!(); +}