diff --git a/ext/crates/algebra/Cargo.toml b/ext/crates/algebra/Cargo.toml index 9a45a269fb..3b98825cb5 100644 --- a/ext/crates/algebra/Cargo.toml +++ b/ext/crates/algebra/Cargo.toml @@ -29,8 +29,9 @@ serde_json = "1.0.141" enum_dispatch = "0.3.13" [dev-dependencies] -bencher = "0.1.5" +criterion = { version = "0.5", features = ["html_reports"] } expect-test = "1.5.1" +pprof = { version = "0.15.0", features = ["criterion", "flamegraph"] } rstest = "0.25.0" [features] @@ -42,3 +43,15 @@ odd-primes = ["fp/odd-primes"] [[bench]] name = "milnor" harness = false + +[[bench]] +name = "multiplication" +harness = false + +[[bench]] +name = "module_action" +harness = false + +[[bench]] +name = "nassau_milnor" +harness = false diff --git a/ext/crates/algebra/benches/common/mod.rs b/ext/crates/algebra/benches/common/mod.rs new file mode 100644 index 0000000000..84e93bfb7c --- /dev/null +++ b/ext/crates/algebra/benches/common/mod.rs @@ -0,0 +1,328 @@ +//! Shared helpers for the algebra crate's criterion benchmarks. +//! +//! These are generic over *any* [`Algebra`] or [`Module`], so the same routines can benchmark the +//! Adem and Milnor bases (see `multiplication.rs`) or any of the predefined Steenrod modules in +//! `ext/steenrod_modules` (see `module_action.rs`). +//! +//! Every bench target that needs these includes the file with `mod common;`; each target compiles +//! its own copy, which is the standard criterion pattern for sharing code between benches. + +// Not every bench target uses every helper, and each compiles this module independently, so silence +// the unavoidable dead-code warnings. +#![allow(dead_code)] + +use std::{hint::black_box, path::PathBuf, sync::Arc}; + +use algebra::{ + Algebra, AlgebraType, SteenrodAlgebra, + module::{Module, SteenrodModule, steenrod_module}, +}; +use criterion::{BenchmarkGroup, Throughput, measurement::WallTime}; +use fp::{ + prime::{Prime, ValidPrime}, + vector::FpVector, +}; +use serde_json::Value; + +/// The directory shipping the predefined Steenrod modules. We read the JSON directly rather than +/// going through `ext::utils`, because the `ext` crate depends on `algebra` (a dependency cycle). +pub const STEENROD_MODULES_DIR: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../../steenrod_modules"); + +/// Build a dense element of the given length with every entry non-zero and reduced mod `p`. Used to +/// exercise the general-element multiplication and action code paths. +pub fn dense_vector(p: ValidPrime, len: usize) -> FpVector { + let mut v = FpVector::new(p, len); + let modulus = p.as_u32() - 1; + for i in 0..len { + // Entries in `1..p`, so the vector is genuinely dense. + v.set_entry(i, (i as u32 % modulus) + 1); + } + v +} + +// ------------------------------------------------------------------------------------------------- +// Algebra multiplication +// ------------------------------------------------------------------------------------------------- + +/// Benchmark multiplication in `algebra` for each `(r_degree, s_degree)` pair in `pairs`. +/// +/// For each pair we register two benchmarks: +/// - `basis_products`: multiply every pair of basis elements via +/// [`Algebra::multiply_basis_elements`], the required multiplication primitive. +/// - `element_product`: a single [`Algebra::multiply_element_by_element`] of two dense elements, +/// exercising the general-element path. +pub fn bench_algebra_multiplication( + group: &mut BenchmarkGroup, + label: &str, + algebra: &A, + pairs: &[(i32, i32)], +) { + let p = algebra.prime(); + for &(r_degree, s_degree) in pairs { + let out_degree = r_degree + s_degree; + // `compute_basis` is cumulative, so this readies every degree up to `out_degree`. + algebra.compute_basis(out_degree); + let r_dim = algebra.dimension(r_degree); + let s_dim = algebra.dimension(s_degree); + let out_dim = algebra.dimension(out_degree); + if r_dim == 0 || s_dim == 0 { + continue; + } + + group.throughput(Throughput::Elements((r_dim * s_dim) as u64)); + + { + let mut scratch = FpVector::new(p, out_dim); + group.bench_function( + format!("{label}/basis_products/{r_degree}x{s_degree}"), + |b| { + b.iter(|| { + scratch.set_to_zero(); + for i in 0..r_dim { + for j in 0..s_dim { + algebra.multiply_basis_elements( + scratch.as_slice_mut(), + 1, + r_degree, + i, + s_degree, + j, + ); + } + } + black_box(&scratch); + }); + }, + ); + } + + { + let r = dense_vector(p, r_dim); + let s = dense_vector(p, s_dim); + let mut scratch = FpVector::new(p, out_dim); + group.bench_function( + format!("{label}/element_product/{r_degree}x{s_degree}"), + |b| { + b.iter(|| { + scratch.set_to_zero(); + algebra.multiply_element_by_element( + scratch.as_slice_mut(), + 1, + r_degree, + r.as_slice(), + s_degree, + s.as_slice(), + ); + black_box(&scratch); + }); + }, + ); + } + } +} + +// ------------------------------------------------------------------------------------------------- +// Module action +// ------------------------------------------------------------------------------------------------- + +/// The lowest few positive operation degrees in which `algebra` is non-trivial, up to `max` of them +/// and bounded by `op_cap`. Keeps the number of benchmark cases per module small and bounded. +fn representative_op_degrees(algebra: &SteenrodAlgebra, op_cap: i32, max: usize) -> Vec { + let mut degrees = Vec::new(); + let mut d = 1; + while d <= op_cap && degrees.len() < max { + algebra.compute_basis(d); + if algebra.dimension(d) > 0 { + degrees.push(d); + } + d += 1; + } + degrees +} + +/// Benchmark the module action of `module` up to total degree `cap`. +/// +/// For a handful of representative operation degrees, we register two benchmarks that sweep over all +/// module degrees (bounded by `cap`, which keeps infinite modules such as `RP_inf` in check): +/// - `act_on_basis`: apply every operation basis element to every module basis element via the +/// required primitive [`Module::act_on_basis`]. +/// - `act`: apply every operation basis element to a dense element via [`Module::act`]. +pub fn bench_module_action( + group: &mut BenchmarkGroup, + label: &str, + module: &dyn Module, + cap: i32, +) { + let algebra = module.algebra(); + let p = module.prime(); + + let min_degree = module.min_degree(); + let max_degree = module.max_degree().map_or(cap, |m| m.min(cap)); + if max_degree < min_degree { + return; + } + + let op_cap = cap.min(12); + let op_degrees = representative_op_degrees(&algebra, op_cap, 4); + + for op_degree in op_degrees { + // Ready every module (and algebra) degree we might touch. + algebra.compute_basis(op_degree); + module.compute_basis(max_degree + op_degree); + let op_dim = algebra.dimension(op_degree); + if op_dim == 0 { + continue; + } + + // The module degrees that actually contribute work at this operation degree. + let sweep: Vec<(i32, usize, usize)> = (min_degree..=max_degree) + .filter_map(|mod_degree| { + let mod_dim = module.dimension(mod_degree); + let out_dim = module.dimension(mod_degree + op_degree); + (mod_dim > 0 && out_dim > 0).then_some((mod_degree, mod_dim, out_dim)) + }) + .collect(); + if sweep.is_empty() { + continue; + } + + let total_pairs: u64 = sweep + .iter() + .map(|&(_, mod_dim, _)| (op_dim * mod_dim) as u64) + .sum(); + group.throughput(Throughput::Elements(total_pairs)); + + { + let mut scratch = FpVector::new(p, 0); + group.bench_function(format!("{label}/act_on_basis/op{op_degree}"), |b| { + b.iter(|| { + for &(mod_degree, mod_dim, out_dim) in &sweep { + scratch.set_scratch_vector_size(out_dim); + scratch.set_to_zero(); + for op_idx in 0..op_dim { + for mod_idx in 0..mod_dim { + module.act_on_basis( + scratch.as_slice_mut(), + 1, + op_degree, + op_idx, + mod_degree, + mod_idx, + ); + } + } + black_box(&scratch); + } + }); + }); + } + + { + let inputs: Vec<(i32, FpVector, usize)> = sweep + .iter() + .map(|&(mod_degree, mod_dim, out_dim)| { + (mod_degree, dense_vector(p, mod_dim), out_dim) + }) + .collect(); + let mut scratch = FpVector::new(p, 0); + group.bench_function(format!("{label}/act/op{op_degree}"), |b| { + b.iter(|| { + // Match the `act_on_basis` loop nesting: size/zero `scratch` and `black_box` + // once per degree slice (not per operation), so both benchmarks pay identical + // harness overhead and stay directly comparable. + for (mod_degree, input, out_dim) in &inputs { + scratch.set_scratch_vector_size(*out_dim); + scratch.set_to_zero(); + for op_idx in 0..op_dim { + module.act( + scratch.as_slice_mut(), + 1, + op_degree, + op_idx, + *mod_degree, + input.as_slice(), + ); + } + black_box(&scratch); + } + }); + }); + } + } +} + +// ------------------------------------------------------------------------------------------------- +// Loading the predefined Steenrod modules +// ------------------------------------------------------------------------------------------------- + +/// All `*.json` module files shipped in [`STEENROD_MODULES_DIR`], sorted by file name for a stable +/// benchmark ordering. +pub fn module_json_files() -> Vec { + let mut files: Vec = std::fs::read_dir(STEENROD_MODULES_DIR) + .unwrap_or_else(|e| panic!("failed to read {STEENROD_MODULES_DIR}: {e}")) + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "json")) + .collect(); + files.sort(); + files +} + +/// A parsed module JSON together with its file stem (used as the benchmark label). +pub struct ModuleSpec { + pub name: String, + pub json: Value, +} + +/// Parse every module file, returning `(name, json)` for each. Files that fail to read or parse are +/// reported to stderr and skipped. +pub fn load_module_specs() -> Vec { + let mut specs = Vec::new(); + for path in module_json_files() { + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_owned(); + match std::fs::read_to_string(&path) + .map_err(anyhow::Error::from) + .and_then(|s| serde_json::from_str::(&s).map_err(anyhow::Error::from)) + { + Ok(json) => specs.push(ModuleSpec { name, json }), + Err(e) => eprintln!("skipping module {name}: could not parse JSON ({e})"), + } + } + specs +} + +/// Whether the module spec requires machinery only available in the `ext` crate. The one such case +/// is a `cofiber`, which is realized as a Yoneda representative via `Resolution` / +/// `yoneda_representative` — neither of which lives in the `algebra` crate. Such modules cannot be +/// constructed from raw JSON here and are skipped. +pub fn requires_ext_machinery(json: &Value) -> bool { + !json["cofiber"].is_null() +} + +/// Whether the module's optional `"algebra"` restriction permits the given basis. Modules without an +/// `"algebra"` field support both bases. +pub fn module_supports(json: &Value, ty: AlgebraType) -> bool { + match json["algebra"].as_array() { + None => true, + Some(list) => { + let wanted = ty.to_string(); + list.iter().any(|x| x.as_str() == Some(&wanted)) + } + } +} + +/// Build the algebra and module for a given spec and basis, using only the `algebra` crate's JSON +/// constructors. +pub fn load_named_module( + json: &Value, + ty: AlgebraType, +) -> anyhow::Result<(Arc, SteenrodModule)> { + let algebra = Arc::new(SteenrodAlgebra::from_json(json, ty, false)?); + let module = steenrod_module::from_json(Arc::clone(&algebra), json)?; + Ok((algebra, module)) +} diff --git a/ext/crates/algebra/benches/milnor.rs b/ext/crates/algebra/benches/milnor.rs index 86e1cd711a..6a38188e59 100644 --- a/ext/crates/algebra/benches/milnor.rs +++ b/ext/crates/algebra/benches/milnor.rs @@ -1,54 +1,95 @@ +//! Benchmarks for the low-level Milnor `PPartMultiplier` kernel. + use algebra::milnor_algebra::{PPartAllocation, PPartEntry, PPartMultiplier}; -use bencher::{Bencher, benchmark_group, benchmark_main}; -use fp::prime::{Prime, ValidPrime}; +use criterion::{ + BenchmarkGroup, Criterion, criterion_group, criterion_main, measurement::WallTime, +}; +use fp::prime::ValidPrime; +use pprof::criterion::{Output, PProfProfiler}; -fn ppart_inner( - bench: &mut Bencher, +fn bench_ppart( + g: &mut BenchmarkGroup, + name: &str, p: u32, r: Vec, s: Vec, ) { let p = ValidPrime::new(p); - - bench.iter(move || { - let m = PPartMultiplier::::new_from_allocation( - p, - &r, - &s, - PPartAllocation::default(), - 0, - 0, + g.bench_function(name, |bench| { + // Hoist the allocation into the (untimed) setup so only the multiplier's iteration is + // measured; `black_box` the yielded coefficients so the loop can't be optimized away. + bench.iter_batched( + PPartAllocation::default, + |alloc| { + let m = PPartMultiplier::::new_from_allocation(p, &r, &s, alloc, 0, 0); + for c in m { + std::hint::black_box(c); + } + }, + criterion::BatchSize::SmallInput, ); - - for c in m { - if MOD4 { - assert!(c < 4); - } else { - assert!(c < p.as_u32()); - } - } }); } -fn ppart_2(bench: &mut Bencher) { - ppart_inner::(bench, 2, vec![60, 30, 8, 2, 1], vec![20, 30, 20, 4, 1, 2]); - ppart_inner::(bench, 2, vec![35, 12, 20, 14, 1, 3], vec![60, 30, 0, 2, 1]); -} +fn ppart(c: &mut Criterion) { + let mut g = c.benchmark_group("milnor_ppart"); -fn ppart_4(bench: &mut Bencher) { - ppart_inner::(bench, 2, vec![60, 30, 8, 2, 1], vec![20, 30, 20, 4, 1, 2]); - ppart_inner::(bench, 2, vec![35, 12, 20, 14, 1, 3], vec![60, 30, 0, 2, 1]); -} + bench_ppart::( + &mut g, + "ppart_2/a", + 2, + vec![60, 30, 8, 2, 1], + vec![20, 30, 20, 4, 1, 2], + ); + bench_ppart::( + &mut g, + "ppart_2/b", + 2, + vec![35, 12, 20, 14, 1, 3], + vec![60, 30, 0, 2, 1], + ); -#[cfg(feature = "odd-primes")] -fn ppart_3(bench: &mut Bencher) { - ppart_inner::(bench, 3, vec![120, 70, 40, 2], vec![60, 35, 21, 6]); - ppart_inner::(bench, 3, vec![30, 12, 35, 24], vec![100, 80, 16, 2, 3]); -} + bench_ppart::( + &mut g, + "ppart_4/a", + 2, + vec![60, 30, 8, 2, 1], + vec![20, 30, 20, 4, 1, 2], + ); + bench_ppart::( + &mut g, + "ppart_4/b", + 2, + vec![35, 12, 20, 14, 1, 3], + vec![60, 30, 0, 2, 1], + ); -#[cfg(feature = "odd-primes")] -benchmark_group!(benches, ppart_2, ppart_4, ppart_3); + #[cfg(feature = "odd-primes")] + { + bench_ppart::( + &mut g, + "ppart_3/a", + 3, + vec![120, 70, 40, 2], + vec![60, 35, 21, 6], + ); + bench_ppart::( + &mut g, + "ppart_3/b", + 3, + vec![30, 12, 35, 24], + vec![100, 80, 16, 2, 3], + ); + } -#[cfg(not(feature = "odd-primes"))] -benchmark_group!(benches, ppart_2, ppart_4); -benchmark_main!(benches); + g.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .measurement_time(std::time::Duration::from_secs(3)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = ppart +} +criterion_main!(benches); diff --git a/ext/crates/algebra/benches/module_action.rs b/ext/crates/algebra/benches/module_action.rs new file mode 100644 index 0000000000..4cce3299fc --- /dev/null +++ b/ext/crates/algebra/benches/module_action.rs @@ -0,0 +1,60 @@ +//! Benchmarks for the module action, run against every predefined module in +//! `ext/steenrod_modules`. +//! +//! Each module is loaded directly from its JSON specification (via the `algebra` crate's own +//! constructors) and benchmarked under both the Milnor and Adem bases it supports. Modules that +//! require `ext`-level machinery (i.e. those defined via a `cofiber`) are skipped. +//! +//! To benchmark a single module, filter by name, e.g. +//! `cargo bench --bench module_action -- Joker`. + +mod common; + +use algebra::AlgebraType; +use criterion::{Criterion, criterion_group, criterion_main}; +use pprof::criterion::{Output, PProfProfiler}; + +/// Cap on total degree, so infinite or high-dimensional modules (`RP_inf`, finitely presented +/// modules, …) stay bounded. +const DEGREE_CAP: i32 = 20; + +fn module_action(c: &mut Criterion) { + for spec in common::load_module_specs() { + if common::requires_ext_machinery(&spec.json) { + eprintln!( + "skipping module {}: cofiber modules require ext-level resolution machinery", + spec.name + ); + continue; + } + + // One group per module so the Milnor and Adem bases share a report (side-by-side plot) + // instead of landing in separate same-named runs. + let mut g = c.benchmark_group(format!("module_action/{}", spec.name)); + for ty in [AlgebraType::Milnor, AlgebraType::Adem] { + if !common::module_supports(&spec.json, ty) { + continue; + } + let basis = ty.to_string(); + let (_algebra, module) = match common::load_named_module(&spec.json, ty) { + Ok(pair) => pair, + Err(e) => { + eprintln!("skipping module {} ({basis}): {e}", spec.name); + continue; + } + }; + + common::bench_module_action(&mut g, &basis, &*module, DEGREE_CAP); + } + g.finish(); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .measurement_time(std::time::Duration::from_secs(3)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = module_action +} +criterion_main!(benches); diff --git a/ext/crates/algebra/benches/multiplication.rs b/ext/crates/algebra/benches/multiplication.rs new file mode 100644 index 0000000000..cd30e46a43 --- /dev/null +++ b/ext/crates/algebra/benches/multiplication.rs @@ -0,0 +1,43 @@ +//! Benchmarks for algebra multiplication. +//! +//! Exercises both the Adem and Milnor bases at primes 2, 3 and 5 (odd primes require the +//! `odd-primes` feature). See `common::bench_algebra_multiplication` for what is measured. + +mod common; + +use algebra::{AdemAlgebra, MilnorAlgebra}; +use criterion::{Criterion, criterion_group, criterion_main}; +use fp::prime::ValidPrime; +use pprof::criterion::{Output, PProfProfiler}; + +/// Benchmark both bases at prime `p` over the given `(r_degree, s_degree)` pairs. Degrees that turn +/// out to be empty for a basis are skipped by the helper. +fn bench_prime(c: &mut Criterion, p: u32, pairs: &[(i32, i32)]) { + let prime = ValidPrime::new(p); + + let adem = AdemAlgebra::new(prime, false); + let mut g = c.benchmark_group(format!("multiplication/p{p}")); + common::bench_algebra_multiplication(&mut g, "adem", &adem, pairs); + let milnor = MilnorAlgebra::new(prime, false); + common::bench_algebra_multiplication(&mut g, "milnor", &milnor, pairs); + g.finish(); +} + +fn multiplication(c: &mut Criterion) { + bench_prime(c, 2, &[(8, 8), (16, 16), (24, 24), (16, 8)]); + #[cfg(feature = "odd-primes")] + { + // Odd-prime degrees are spaced by q = 2(p - 1), so we pick multiples of q. + bench_prime(c, 3, &[(8, 8), (12, 12), (16, 16), (12, 8)]); + bench_prime(c, 5, &[(8, 8), (16, 16), (24, 24), (16, 8)]); + } +} + +criterion_group! { + name = benches; + config = Criterion::default() + .measurement_time(std::time::Duration::from_secs(3)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = multiplication +} +criterion_main!(benches); diff --git a/ext/crates/algebra/benches/nassau_milnor.rs b/ext/crates/algebra/benches/nassau_milnor.rs new file mode 100644 index 0000000000..41021a792d --- /dev/null +++ b/ext/crates/algebra/benches/nassau_milnor.rs @@ -0,0 +1,125 @@ +//! Benchmarks the Milnor multiplication kernel in the exact regime Nassau's algorithm +//! exercises when resolving the sphere `S` at the prime 2. +//! +//! ## What Nassau actually calls +//! +//! Nassau (`ext/src/nassau.rs`) resolves over `FreeModule`, which is a *stable* +//! (`U = false`) free module built on `MilnorAlgebra::new(TWO, false)`. Building the masked +//! differential matrices (`get_partial_matrix` → `FreeModuleHomomorphism::apply_to_basis_element` +//! → `FreeModule::act`) issues, per generator block, one +//! +//! ```text +//! Algebra::multiply_basis_element_by_element(result, 1, op_degree, op_index, elt_degree, elt) +//! ``` +//! +//! — a fixed *operation* basis element times a general algebra *element*. Because the free module +//! is `U = false`, the `MuAlgebra` shim discards `excess` and routes to the plain **stable** +//! multiply; every basis-by-basis product funnels through `MilnorAlgebra::multiply_with_allocation` +//! (`excess = i32::MAX`, no instability truncation) into the `PPartMultiplier::` sweep — the +//! innermost kernel. The `*_unstable` entry points are never taken. +//! +//! ## Where the regime comes from +//! +//! The `(operation_degree, element_degree)` pairs in [`REGIME`] were captured by instrumenting +//! `multiply_with_allocation` during a real `construct_nassau(("S_2", "milnor"), None)` resolved +//! through bidegree (70, 70): ~3.77M basis-by-basis products over ~2200 distinct degree pairs. The +//! cost mass sits in target degrees `op + elt ≈ 40..52`; within that, products land heavily on +//! degree-24 and degree-32 element components, alongside a frequent "large operation × small +//! element" band (e.g. a degree-40+ operation applied to a degree-1 element). The table samples +//! those regions plus a couple of smaller pairs for the cheap end of the cost curve. To regenerate +//! after algorithm changes, re-instrument `multiply_with_allocation` to tally `(m1.degree, +//! m2.degree)` and rerun the resolution (see the plan/commit history). + +use std::hint::black_box; + +use algebra::{Algebra, MilnorAlgebra}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use fp::{prime::TWO, vector::FpVector}; +use pprof::criterion::{Output, PProfProfiler}; + +/// `(operation_degree, element_degree)` pairs sampled from Nassau's `S_2` @ p=2 regime. +const REGIME: &[(i32, i32)] = &[ + // cheap end of the curve + (8, 8), + (16, 16), + // operation × degree-24 element component (a hot band) + (8, 24), + (20, 24), + (24, 24), + (26, 24), + // operation × degree-32 element component + (16, 32), + (24, 32), + // operation applied to a smaller element + (24, 16), + (32, 8), + // large operation × small element (the frequent Sq^1-style band) + (40, 1), + (46, 1), +]; + +/// A dense element of the given length. At p=2 every present coordinate is 1, so this is the +/// all-ones vector — the densest element, giving a stable upper bound on the inner-loop cost. +fn dense_element(len: usize) -> FpVector { + let mut v = FpVector::new(TWO, len); + for i in 0..len { + v.set_entry(i, 1); + } + v +} + +fn nassau_milnor(c: &mut Criterion) { + // Exactly the algebra Nassau uses: the full Milnor algebra at p=2, stable (not unstable). + let algebra = MilnorAlgebra::new(TWO, false); + let mut g = c.benchmark_group("nassau_milnor"); + + for &(op_degree, elt_degree) in REGIME { + let out_degree = op_degree + elt_degree; + // `compute_basis` is cumulative, so this readies every degree up to `out_degree`. + algebra.compute_basis(out_degree); + let op_dim = algebra.dimension(op_degree); + let elt_dim = algebra.dimension(elt_degree); + let out_dim = algebra.dimension(out_degree); + if op_dim == 0 || elt_dim == 0 { + continue; + } + + // A dense element sends every basis product to the kernel: op_dim * elt_dim of them. + g.throughput(Throughput::Elements((op_dim * elt_dim) as u64)); + + let elt = dense_element(elt_dim); + let mut scratch = FpVector::new(TWO, out_dim); + g.bench_function( + format!("basis_by_element/op{op_degree}xel{elt_degree}"), + |b| { + b.iter(|| { + scratch.set_to_zero(); + // Sweep every operation basis element against the dense element — exactly the + // `multiply_basis_element_by_element` call `FreeModule::act` makes in Nassau. + for i in 0..op_dim { + algebra.multiply_basis_element_by_element( + scratch.as_slice_mut(), + 1, + op_degree, + i, + elt_degree, + elt.as_slice(), + ); + } + black_box(&scratch); + }); + }, + ); + } + + g.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .measurement_time(std::time::Duration::from_secs(3)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = nassau_milnor +} +criterion_main!(benches);