From 479d65b4f5a245e3dd36eb97ef778e62a4a25d45 Mon Sep 17 00:00:00 2001 From: "David R. MacIver" Date: Sun, 31 May 2026 08:52:11 +0100 Subject: [PATCH] Add integer-bench: a standalone bigint comparison benchmark crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `integer-bench`, a separate crate (excluded from the workspace) that benchmarks dashu-int against other bigint libraries with criterion. Keeping it out of the workspace means dashu-int's `--all-features` builds and MSRV check never pull in the comparison libraries, and the bench crate can carry its own newer toolchain requirement (malachite needs Rust 1.90). Each criterion bench body is written once, generic over a `Backend` (see integer-bench/src/lib.rs), and run for every backend with the backend name as a `BenchmarkId` dimension — so one `cargo bench` run reports all libraries side-by-side in one group. This revives the trait-based, multi-library approach of the top-level `benchmark/` harness while emitting criterion measurements. Backends: dashu, ibig, num-bigint and malachite are always built (pure Rust); rug (GNU GMP) is added under the `gmp` feature. Every backend samples by drawing a dashu value and converting it, so magnitudes line up point-for-point across libraries. The abstraction: * `BenchInt` — by-ref ops shared by the unsigned and signed types. Several libraries' by-ref operators return lazy incomplete values, so each op is a method (finalised to an owned value) rather than a std `Add`/`Sub`/... bound. The `*_assign` family has portable defaults; backends with a native `+=` override. * `UnsignedInt` / `SignedInt` — primitive constructors / `+=` / `TryInto`. dashu/num/malachite/ibig have a `UBig`/`IBig`-style split; rug uses one signed type for both. * `Backend` — picks the types + samplers, plus the `magnitude` (`unsigned_abs`/`abs`) and `unsigned_to_signed` bridges. * `PrimitiveInt` / `PrimitiveBackend` — the bit-width sweep's extra surface (gcd, extended-gcd, pow, radix; each backend uses its native routine) plus modular arithmetic. The modular ops are like-for-like: `mod_mul` is plain multiply-then-reduce and `mod_pow` is each library's native one-shot modpow, with nothing precomputed. Benches: `primitive` (bit-width sweep up to 10^4 bits), `small_int`, `workload`, `shrinker`. The bit-width sweep stops at 10^4 bits — enough to show the crossover where GMP-backed libraries pull ahead, without the very large sizes. The workload/shrinker benches model a property-based-testing generator and shrinker; their shapes were drawn from profiling hegel-rust (https://github.com/DRMacIver/hegel) but are written to stand on their own. CI gets a `smoke-test-integer-bench` job that builds the benches and runs each once via criterion `--test` (no measurement, no numbers reported, since CI is too noisy for real benchmarking), so the crate can't silently rot. --- .github/workflows/tests.yml | 18 + .gitignore | 2 + Cargo.toml | 2 +- integer-bench/Cargo.toml | 60 ++ integer-bench/README.md | 63 ++ integer-bench/benches/primitive.rs | 247 ++++++ integer-bench/benches/shrinker.rs | 890 +++++++++++++++++++ integer-bench/benches/small_int.rs | 689 +++++++++++++++ integer-bench/benches/workload.rs | 349 ++++++++ integer-bench/src/lib.rs | 1271 ++++++++++++++++++++++++++++ 10 files changed, 3590 insertions(+), 1 deletion(-) create mode 100644 integer-bench/Cargo.toml create mode 100644 integer-bench/README.md create mode 100644 integer-bench/benches/primitive.rs create mode 100644 integer-bench/benches/shrinker.rs create mode 100644 integer-bench/benches/small_int.rs create mode 100644 integer-bench/benches/workload.rs create mode 100644 integer-bench/src/lib.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 82e17cd8..6348f878 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -127,6 +127,24 @@ jobs: - run: cargo build --features gmp working-directory: benchmark + smoke-test-integer-bench: + name: Smoke-test integer benchmarks + runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + # Build every bench and run each one once (criterion `--test`): no + # measurement and no numbers reported, since CI is far too noisy for real + # benchmarking. This only checks the benches still compile and run. + # Pure-Rust backends only — the optional `gmp`/rug backend needs the GMP + # toolchain and is left out to keep this fast and dependency-free. + - run: cargo bench -- --test + working-directory: integer-bench + build-aarch64: name: Build aarch64 runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 861f28ef..3e7bb1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /.vscode/settings.json benchmark/Cargo.lock benchmark/target +integer-bench/Cargo.lock +integer-bench/target diff --git a/Cargo.toml b/Cargo.toml index cb56f25f..826f34d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ members = [ "python", "rational", ] -exclude = ["benchmark"] +exclude = ["benchmark", "integer-bench"] default-members = ["base", "integer", "float", "rational", "macros"] [features] diff --git a/integer-bench/Cargo.toml b/integer-bench/Cargo.toml new file mode 100644 index 00000000..75b20b79 --- /dev/null +++ b/integer-bench/Cargo.toml @@ -0,0 +1,60 @@ +# Standalone benchmark crate comparing dashu-int against other bigint +# libraries. Kept out of the dashu workspace (see the root `exclude`) so it +# never gets pulled into dashu-int's `--all-features` builds or its MSRV check +# — this crate can require a newer toolchain (malachite needs Rust 1.90). +# +# Run (pure-Rust backends, no toolchain needed): +# cargo bench --manifest-path integer-bench/Cargo.toml +# Add the rug (GNU GMP) backend (needs the GMP toolchain): +# cargo bench --manifest-path integer-bench/Cargo.toml --features gmp +[package] +name = "integer-bench" +version = "0.0.0" +edition = "2021" +publish = false +license = "MIT OR Apache-2.0" +rust-version = "1.90" # malachite 0.9 is edition 2024 / MSRV 1.90 + +# Don't run libtest on the lib target under `cargo bench` — only the criterion +# `[[bench]]` targets below should run, and they take criterion's CLI args. +[lib] +bench = false + +[features] +default = [] +# Adds rug (links libgmp/libmpfr/libmpc) as an extra backend. +gmp = ["dep:rug"] + +[dependencies] +dashu-int = { path = "../integer", features = ["rand"] } +rand_v08 = { version = "0.8.3", package = "rand" } + +# Comparison libraries (pure Rust; always built). +ibig = "0.3.6" +num-bigint = "0.4.6" +num-integer = "0.1.46" +num-traits = "0.2.19" +malachite-nz = "0.9.1" +malachite-base = "0.9.1" + +# rug links the system/GMP toolchain, so it is optional behind `gmp`. +rug = { version = "1.30", optional = true } + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + +[[bench]] +name = "primitive" +harness = false + +[[bench]] +name = "small_int" +harness = false + +[[bench]] +name = "workload" +harness = false + +[[bench]] +name = "shrinker" +harness = false diff --git a/integer-bench/README.md b/integer-bench/README.md new file mode 100644 index 00000000..b773873a --- /dev/null +++ b/integer-bench/README.md @@ -0,0 +1,63 @@ +Criterion benchmarks comparing [`dashu-int`](../integer) against other Rust +big-integer libraries. Each bench body is written once, generic over a +`Backend`, and run for every library, with the backend name as a `BenchmarkId` +dimension — so a single run reports all libraries side-by-side in one group. + +This is a standalone crate, deliberately kept out of the dashu workspace (see +the root `Cargo.toml` `exclude`). That means it never gets pulled into +`dashu-int`'s `--all-features` builds or its MSRV check, and it is free to +require a newer toolchain than dashu (malachite needs Rust 1.90). + +## Libraries + +| Library | Version | Notes | +| ------- | ------- | ----- | +| [dashu-int](https://crates.io/crates/dashu-int) | (path) | The library under test. Pure Rust, no_std | +| [ibig](https://crates.io/crates/ibig) | 0.3 | Pure Rust, no_std. dashu-int's ancestor | +| [num-bigint](https://crates.io/crates/num-bigint)| 0.4 | Pure Rust. The de-facto standard | +| [malachite](https://crates.io/crates/malachite) | 0.9 | Pure Rust, LGPL, derived from GMP and FLINT | +| [rug](https://crates.io/crates/rug) | 1.30 | Links [GMP](https://gmplib.org/); `gmp` feature | + +The pure-Rust backends (dashu, ibig, num-bigint, malachite) are always built. +rug is added under the `gmp` feature, which needs the GMP toolchain. + +## Benchmarks + +| Bench | What it covers | +| ----- | ------------- | +| `primitive` | Bit-width sweep (10^1..10^4 bits): add/sub/mul/div, gcd, pow, radix, modular | +| `small_int` | Small / inline-magnitude values (≤ 128 bits) the bit-width sweep under-covers | +| `workload` | Generator/shrinker-style scenarios: running sums, string round-trips, op mixes | +| `shrinker` | The bigint operations a property-based-testing shrinker performs | + +The `workload` and `shrinker` shapes were drawn from profiling +[hegel-rust](https://github.com/hegeldev/hegel-rust) but are written to stand on +their own. + +The `primitive` `ubig_modulo_*` benches measure each library's plain +multiply-then-reduce and native modpow (nothing precomputed), for a +like-for-like comparison. + +## Usage + +Run from the repository root with `--manifest-path`, or from inside this +directory: + +```sh +# All pure-Rust backends (no toolchain needed): +cargo bench --manifest-path integer-bench/Cargo.toml + +# A single bench, quickly: +cargo bench --manifest-path integer-bench/Cargo.toml --bench small_int -- --quick + +# Include the rug (GMP) backend (needs the GMP toolchain): +cargo bench --manifest-path integer-bench/Cargo.toml --features gmp + +# Smoke test only — build and run each bench once, no measurement: +cargo bench --manifest-path integer-bench/Cargo.toml -- --test +``` + +## License + +Part of the [dashu](..) project; dual-licensed under +[MIT](../LICENSE-MIT) or [Apache-2.0](../LICENSE-APACHE). diff --git a/integer-bench/benches/primitive.rs b/integer-bench/benches/primitive.rs new file mode 100644 index 00000000..ee505659 --- /dev/null +++ b/integer-bench/benches/primitive.rs @@ -0,0 +1,247 @@ +//! Bit-width-sweep arithmetic benchmarks (the dashu `primitive.rs` suite, +//! generalised to compare against other libraries). +//! +//! Run: +//! cargo bench --manifest-path integer-bench/Cargo.toml --bench primitive -- --quick +//! Include the rug backend too (needs the GMP toolchain): add `--features gmp`. +//! +//! Note: these don't work on 16-bit machines. +//! +//! Each bench body is generic over [`PrimitiveBackend`] and run for every +//! backend, so dashu and the comparison libraries land in one criterion report +//! under the backend name. The pure-Rust backends (dashu, ibig, num-bigint, +//! malachite) are always built; rug is added with `--features gmp`. +//! +//! The `ubig_modulo_*` benches measure each library's plain multiply-then-reduce +//! and native modpow (nothing precomputed), so they're a like-for-like +//! comparison. + +use criterion::measurement::WallTime; +use criterion::{ + criterion_group, criterion_main, AxisScale, BenchmarkGroup, BenchmarkId, Criterion, + PlotConfiguration, +}; +use integer_bench::{ + BenchInt, Dashu, Ibig, Malachite, Num, PrimitiveBackend, PrimitiveInt, UnsignedInt, +}; +use rand_v08::prelude::*; +use std::fmt::Write; + +#[cfg(feature = "gmp")] +use integer_bench::Rug; + +const SEED: u64 = 1; + +/// Define a criterion entry point `$name` that opens group `$group` and runs +/// the generic body `$body` for every backend (dashu, ibig, num and malachite +/// always; rug when the `gmp` feature is on). +macro_rules! per_backend { + ($name:ident, $group:literal, $body:ident) => { + fn $name(c: &mut Criterion) { + let mut group = c.benchmark_group($group); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + #[cfg(feature = "gmp")] + $body::(&mut group); + group.finish(); + } + }; +} + +/// Arithmetic binop bench bodies sharing the "b is built > a" setup, so that +/// subtraction never underflows. +macro_rules! binop_body { + ($body:ident, $method:ident, $max_log_bits:literal) => { + fn $body(group: &mut BenchmarkGroup) + where + B::Unsigned: PrimitiveInt, + { + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=$max_log_bits { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let b = B::sample_unsigned_bits(bits, &mut rng).add_ref(&a); // b > a + group.bench_with_input( + BenchmarkId::new(B::NAME, bits), + &(a, b), + |bencher, (ta, tb)| bencher.iter(|| tb.$method(ta)), + ); + } + } + }; +} + +binop_body!(add_body, add_ref, 4); +binop_body!(sub_body, sub_ref, 4); +binop_body!(mul_body, mul_ref, 4); +binop_body!(div_body, div_ref, 4); +binop_body!(gcd_body, gcd, 4); + +fn gcd_ext_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let b = B::sample_unsigned_bits(bits, &mut rng).add_ref(&a); + group.bench_with_input(BenchmarkId::new(B::NAME, bits), &(a, b), |bencher, (ta, tb)| { + bencher.iter(|| tb.gcd_ext_blackbox(ta)) + }); + } +} + +per_backend!(ubig_add, "ubig_add", add_body); +per_backend!(ubig_sub, "ubig_sub", sub_body); +per_backend!(ubig_mul, "ubig_mul", mul_body); +per_backend!(ubig_div, "ubig_div", div_body); +per_backend!(ubig_gcd, "ubig_gcd", gcd_body); +per_backend!(ubig_gcd_ext, "ubig_gcd_ext", gcd_ext_body); + +fn to_hex_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let mut out = String::with_capacity(bits / 4 + 1); + group.bench_with_input(BenchmarkId::new(B::NAME, bits), &a, |bencher, ta| { + bencher.iter(|| { + out.clear(); + ta.write_hex(&mut out); + out.len() + }) + }); + } +} + +fn to_dec_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let mut out = String::with_capacity(bits / 3 + 1); + group.bench_with_input(BenchmarkId::new(B::NAME, bits), &a, |bencher, ta| { + bencher.iter(|| { + out.clear(); + write!(&mut out, "{}", ta).unwrap(); + out.len() + }) + }); + } +} + +fn from_hex_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let s = a.to_radix_string(16); + group.bench_with_input(BenchmarkId::new(B::NAME, bits), &s, |bencher, ts| { + bencher.iter(|| B::Unsigned::from_radix(ts, 16)) + }); + } +} + +fn from_dec_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let a = B::sample_unsigned_bits(bits, &mut rng); + let s = a.to_radix_string(10); + group.bench_with_input(BenchmarkId::new(B::NAME, bits), &s, |bencher, ts| { + bencher.iter(|| B::Unsigned::from_radix(ts, 10)) + }); + } +} + +fn pow_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + for log_power in 1..=4 { + let p = 10usize.pow(log_power); + group.bench_with_input(BenchmarkId::new(B::NAME, p), &p, |bencher, p| { + bencher.iter(|| B::Unsigned::from_u64(3).pow_exp(*p)) + }); + } +} + +per_backend!(ubig_to_hex, "ubig_to_hex", to_hex_body); +per_backend!(ubig_to_dec, "ubig_to_dec", to_dec_body); +per_backend!(ubig_from_hex, "ubig_from_hex", from_hex_body); +per_backend!(ubig_from_dec, "ubig_from_dec", from_dec_body); +per_backend!(ubig_pow, "ubig_pow", pow_body); + +fn modulo_mul_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let m = B::sample_unsigned_bits(bits, &mut rng); + let a = B::sample_unsigned_bits(bits, &mut rng); + let b = B::sample_unsigned_bits(bits, &mut rng); + group.bench_with_input( + BenchmarkId::new(B::NAME, bits), + &(a, b, m), + |bencher, (a, b, m)| bencher.iter(|| B::mod_mul(a, b, m)), + ); + } +} + +fn modulo_pow_body(group: &mut BenchmarkGroup) +where + B::Unsigned: PrimitiveInt, +{ + let mut rng = StdRng::seed_from_u64(SEED); + for log_bits in 1..=4 { + let bits = 10usize.pow(log_bits); + let m = B::sample_unsigned_bits(bits, &mut rng); + let a = B::sample_unsigned_bits(2048, &mut rng); + let b = B::sample_unsigned_bits(bits, &mut rng); // exponent + group.bench_with_input( + BenchmarkId::new(B::NAME, bits), + &(a, b, m), + |bencher, (a, b, m)| bencher.iter(|| B::mod_pow(a, b, m)), + ); + } +} + +per_backend!(ubig_modulo_mul, "ubig_modulo_mul", modulo_mul_body); +per_backend!(ubig_modulo_pow, "ubig_modulo_pow", modulo_pow_body); + +criterion_group!( + benches, + ubig_add, + ubig_sub, + ubig_mul, + ubig_div, + ubig_gcd, + ubig_gcd_ext, + ubig_to_hex, + ubig_to_dec, + ubig_from_hex, + ubig_from_dec, + ubig_pow, + ubig_modulo_mul, + ubig_modulo_pow, +); + +criterion_main!(benches); diff --git a/integer-bench/benches/shrinker.rs b/integer-bench/benches/shrinker.rs new file mode 100644 index 00000000..1a8b0983 --- /dev/null +++ b/integer-bench/benches/shrinker.rs @@ -0,0 +1,890 @@ +//! Property-based-testing shrinker workload. +//! +//! These benchmarks are derived from profiling specific hot paths that show +//! up when profiling the test suite of +//! [hegel-rust](https://github.com/hegeldev/hegel-rust), but should mostly +//! just be taken as interesting examples of realistic workloads. These ones +//! are particularly focused on hot paths that occur during shrinking. +//! +//! Each bench body is generic over [`Backend`] and run for every backend, so +//! dashu and the comparison libraries land in one criterion report under the +//! backend name. The pure-Rust backends (dashu, ibig, num-bigint, malachite) +//! are always built; rug is added with `--features gmp` (needs the GMP +//! toolchain). Where dashu uses a truly-unsigned `UBig` for a sort-key +//! magnitude, single-signed-type backends model it via `Backend::magnitude` +//! (`abs`). +//! +//! Run: +//! cargo bench --manifest-path integer-bench/Cargo.toml --bench shrinker +//! Include the rug backend too (needs the GMP toolchain): add `--features gmp`. + +use criterion::measurement::WallTime; +use criterion::{ + black_box, criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, +}; +use integer_bench::{ + seeded_rng, Backend, BenchInt, Dashu, Ibig, Malachite, Num, SignedInt, UnsignedInt, ValueClass, +}; + +#[cfg(feature = "gmp")] +use integer_bench::Rug; + +/// A node-shaped tuple: (min, max, shrink-target, value), all signed. +type Node4 = ( + ::Signed, + ::Signed, + ::Signed, + ::Signed, +); + +/// A value→index scenario: (label, min, shrink-target, max). +type Scenario = ( + &'static str, + ::Signed, + ::Signed, + ::Signed, +); + +/// Define a criterion entry point `$name` that opens group `$group` and runs +/// the generic body `$body` for every backend (dashu, ibig, num and malachite +/// always; rug when the `gmp` feature is on). +macro_rules! per_backend { + ($name:ident, $group:literal, $body:ident) => { + fn $name(c: &mut Criterion) { + let mut group = c.benchmark_group($group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + #[cfg(feature = "gmp")] + $body::(&mut group); + group.finish(); + } + }; +} + +// --------------------------------------------------------------------------- +// 1. Clone — typically the dominant cost. +// +// A shrinker holds a sequence of choice nodes, each carrying four integers +// (min, max, shrink-target, value), and clones the whole sequence for every +// candidate it evaluates, so per-integer clone cost is multiplied by +// 4 * n_nodes * n_candidates. +// --------------------------------------------------------------------------- + +fn ibig_clone_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let inputs: Vec = (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &inputs, |b, v| { + let mut i = 0usize; + b.iter(|| { + let x = &v[i & 31]; + i = i.wrapping_add(1); + black_box(x).clone() + }) + }); + } +} + +/// Clone a node-shaped struct: 4 integer fields (min, max, shrink-target, +/// value). This is the atomic unit the shrinker clones; measuring it directly +/// captures the aggregate overhead better than per-field clones. +fn choice_node_clone(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + let nodes: Vec> = (0..32) + .map(|_| { + let min = B::sample_signed(class, &mut rng); + let max = B::sample_signed(class, &mut rng); + let towards = B::Signed::from_i64(0); + let value = B::sample_signed(class, &mut rng); + (min, max, towards, value) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &nodes, |b, n| { + let mut i = 0usize; + b.iter(|| { + let (min, max, towards, value) = &n[i & 31]; + i = i.wrapping_add(1); + ( + black_box(min).clone(), + black_box(max).clone(), + black_box(towards).clone(), + black_box(value).clone(), + ) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 2. Drop — paired with clone: every cloned value is eventually dropped. +// The shrinker clones the node sequence, evaluates it, then drops it. +// --------------------------------------------------------------------------- + +fn ibig_drop_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let templates: Vec = + (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &templates, |b, t| { + let mut i = 0usize; + b.iter(|| { + let x = t[i & 31].clone(); + i = i.wrapping_add(1); + drop(black_box(x)); + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 3. sort_key pattern: `(value - target).magnitude()`. +// Computed once per node per candidate, so ~n_nodes * n_candidates times +// per shrink run. +// --------------------------------------------------------------------------- + +fn ibig_sub_magnitude(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let pairs: Vec<(B::Signed, B::Signed)> = (0..32) + .map(|_| (B::sample_signed(class, &mut rng), B::sample_signed(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (value, target) = &p[i & 31]; + i = i.wrapping_add(1); + B::magnitude(black_box(value).sub_ref(black_box(target))) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 4. Clamp a value into a range: `value.clamp(min, max)`. +// Uses Ord::clamp, which does two comparisons plus one clone. +// --------------------------------------------------------------------------- + +fn ibig_clamp(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + let triples: Vec<(B::Signed, B::Signed, B::Signed)> = (0..32) + .map(|_| { + let mut vals = [ + B::sample_signed(class, &mut rng), + B::sample_signed(class, &mut rng), + B::sample_signed(class, &mut rng), + ]; + vals.sort(); + let [min, value, max] = vals; + (min, value, max) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &triples, |b, t| { + let mut i = 0usize; + b.iter(|| { + let (min, value, max) = &t[i & 31]; + i = i.wrapping_add(1); + black_box(value).clone().clamp(min.clone(), max.clone()) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 5. Range validation: `min <= value && value <= max`. +// Two comparisons per check, run on every candidate. +// --------------------------------------------------------------------------- + +fn ibig_double_cmp(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let triples: Vec<(B::Signed, B::Signed, B::Signed)> = (0..32) + .map(|_| { + let a = B::sample_signed(class, &mut rng); + let b = B::sample_signed(class, &mut rng); + let c = B::sample_signed(class, &mut rng); + (a, b, c) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &triples, |b, t| { + let mut i = 0usize; + b.iter(|| { + let (min, value, max) = &t[i & 31]; + i = i.wrapping_add(1); + black_box(min) <= black_box(value) && black_box(value) <= black_box(max) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 6. Construct integers from small primitives — `from(0)`, `from(1)`, +// `from(n)` — done constantly throughout a shrink run. +// --------------------------------------------------------------------------- + +fn ibig_from_small_consts(group: &mut BenchmarkGroup) { + group.bench_function(format!("{}/zero", B::NAME), |b| { + b.iter(|| B::Signed::from_i64(black_box(0))) + }); + group.bench_function(format!("{}/one", B::NAME), |b| { + b.iter(|| B::Signed::from_i64(black_box(1))) + }); + group.bench_function(format!("{}/minus_one", B::NAME), |b| { + b.iter(|| B::Signed::from_i64(black_box(-1))) + }); +} + +// --------------------------------------------------------------------------- +// 7. Unsigned compare — comparing sort-key magnitudes (unsigned distances) +// while ordering candidate sequences. +// --------------------------------------------------------------------------- + +fn ubig_cmp_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).cmp(black_box(c)) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 8. Shift-right descent — a binary search that probes +// `lo + (dist >> k as usize)` where k grows geometrically. +// --------------------------------------------------------------------------- + +fn ibig_shift_right_descent(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + let pairs: Vec<(B::Signed, B::Signed)> = (0..32) + .map(|_| { + let lo = B::sample_signed(class, &mut rng); + let dist = B::magnitude(B::sample_signed(class, &mut rng)); + (lo, B::unsigned_to_signed(dist)) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (lo, dist) = &p[i & 31]; + i = i.wrapping_add(1); + // Inner loop of the descent search: lo + (dist >> k as usize) + // for k = 1, 2, 4, 8, 16 + let mut last = lo.clone(); + for k in [1usize, 2, 4, 8, 16] { + last = lo.add_ref(&black_box(dist).shr_ref(k)); + } + last + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 9. Combined shrinker step — one candidate evaluation's hot path: clone n +// nodes (each 4 integers), compute a sort key for each (sub + magnitude), +// then compare the sort-key sequences lexicographically. +// +// The top-level scenario bench that combines all of the above. +// --------------------------------------------------------------------------- + +fn shrinker_consider_workload(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for n_nodes in [4, 16, 64] { + // Build n nodes: each has (min, max, shrink-target, value). + let nodes: Vec> = (0..n_nodes) + .map(|_| { + let class = if rand_v08::Rng::gen::(&mut rng) { + ValueClass::OneWord + } else { + ValueClass::TwoWord + }; + let a = B::sample_signed(class, &mut rng); + let b = B::sample_signed(class, &mut rng); + let (min, max) = if a <= b { (a, b) } else { (b, a) }; + let towards = B::Signed::from_i64(0); + let value = B::sample_signed(class, &mut rng); + (min, max, towards, value) + }) + .collect(); + + group.bench_with_input(BenchmarkId::new(B::NAME, n_nodes), &nodes, |b, nodes| { + b.iter(|| { + // Phase 1: clone all nodes (usually the dominant cost). + let cloned: Vec<_> = nodes + .iter() + .map(|(min, max, towards, value)| { + (min.clone(), max.clone(), towards.clone(), value.clone()) + }) + .collect(); + + // Phase 2: compute sort_key for each: sub + magnitude. + let sort_keys: Vec<(B::Unsigned, bool)> = cloned + .iter() + .map(|(_min, _max, towards, value)| { + let target = towards.clone(); + let distance = B::magnitude(black_box(value).sub_ref(&target)); + let below = *value < target; + (distance, below) + }) + .collect(); + + // Phase 3: lexicographic comparison of sort key sequences. + let mut total_order = std::cmp::Ordering::Equal; + for i in 0..sort_keys.len() { + let cmp = sort_keys[i].cmp(black_box(&sort_keys[sort_keys.len() - 1 - i])); + if cmp != std::cmp::Ordering::Equal { + total_order = cmp; + break; + } + } + (cloned, sort_keys, total_order) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 10. Index→value binary-search step — maps an index to a value with +// unsigned arithmetic: mid = lo + ((hi - lo) >> 1), then +// min(mid, above) + min(mid, below) comparisons. +// --------------------------------------------------------------------------- + +fn ubig_binary_search_step(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let triples: Vec<(B::Unsigned, B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| { + let a = B::sample_unsigned(class, &mut rng); + let b = B::sample_unsigned(class, &mut rng); + let (lo, hi) = if a <= b { (a, b) } else { (b, a) }; + let above = B::sample_unsigned(class, &mut rng); + (lo, hi, above) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &triples, |b, t| { + let mut i = 0usize; + b.iter(|| { + let (lo, hi, above) = &t[i & 31]; + i = i.wrapping_add(1); + let mid = lo.add_ref(&hi.sub_ref(lo).shr_ref(1)); + let total = std::cmp::min(&mid, black_box(above)) + .add_ref(std::cmp::min(&mid, black_box(above))); + (mid, total) + }) + }); + } +} + +// --------------------------------------------------------------------------- +// 11. HashMap insert+lookup workload — signed integers are sometimes used as +// deterministic-id keys in shrinker-adjacent data structures. Exercises +// the Hash + Eq impls on inline values. +// --------------------------------------------------------------------------- + +fn ibig_hashmap_keys(group: &mut BenchmarkGroup) { + use std::collections::HashMap; + + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + let keys: Vec = (0..128) + .map(|_| B::sample_signed(class, &mut rng)) + .collect(); + // Pre-populate the map. + let mut map: HashMap = HashMap::with_capacity(keys.len()); + for (i, k) in keys.iter().enumerate() { + map.insert(k.clone(), i as u32); + } + group.bench_with_input( + BenchmarkId::new(B::NAME, class.label()), + &(keys, map), + |b, (ks, m)| { + let mut i = 0usize; + b.iter(|| { + let k = &ks[i & 127]; + i = i.wrapping_add(1); + m.get(black_box(k)).copied() + }) + }, + ); + } +} + +// --------------------------------------------------------------------------- +// 12. Full index→value binary search — often the single most expensive +// operation. Maps an index to a value over a full i128-range choice: +// binary search with mid = lo + ((hi - lo) >> 1), +// total = min(mid, above) + min(mid, below), ~128 iterations for +// i128::MIN..i128::MAX. +// --------------------------------------------------------------------------- + +fn from_index_full_search(group: &mut BenchmarkGroup) { + // i128 range: above = i128::MAX, below = i128::MIN.abs() = i128::MAX + 1 + // The common case: a choice over {min: i128::MIN+1, max: i128::MAX, shrink-target: 0}. + let above = B::Unsigned::from_u128(i128::MAX as u128); + let below = B::Unsigned::from_u128(i128::MAX as u128 + 1); + + for target_frac in [0.0f64, 0.25, 0.5, 0.75, 1.0] { + let target_idx = { + let max_idx = above.add_ref(&below); + let frac_bits = (target_frac * 1000.0) as u128; + max_idx + .mul_ref(&B::Unsigned::from_u128(frac_bits)) + .div_ref(&B::Unsigned::from_u64(1000)) + }; + + group.bench_with_input( + BenchmarkId::new(B::NAME, format!("frac_{:.0}pct", target_frac * 100.0)), + &target_idx, + |b, idx| { + b.iter(|| { + let one = B::Unsigned::from_u64(1); + let mut lo = one.clone(); + let mut hi = std::cmp::max(&above, &below).clone(); + while lo < hi { + let mid = lo.add_ref(&hi.sub_ref(&lo).shr_ref(1)); + let total = + std::cmp::min(&mid, &above).add_ref(std::cmp::min(&mid, &below)); + if total >= *black_box(idx) { + hi = mid; + } else { + lo = mid.add_ref(&one); + } + } + lo + }) + }, + ); + } +} + +// --------------------------------------------------------------------------- +// 13. By-ref unsigned add/sub — the index search operates on references, not +// owned values. The benches above cover owned operands; this covers the +// by-reference path. +// --------------------------------------------------------------------------- + +fn ubig_ref_add_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).add_ref(black_box(c)) + }) + }); + } +} + +fn ubig_ref_sub_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ] { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| { + let a = B::sample_unsigned(class, &mut rng); + let b = B::sample_unsigned(class, &mut rng); + // Ensure a >= b so subtraction doesn't panic. + if a >= b { + (a, b) + } else { + (b, a) + } + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).sub_ref(black_box(c)) + }) + }); + } +} + +per_backend!(bench_ibig_clone_by_class, "ibig_clone", ibig_clone_by_class); +per_backend!(bench_choice_node_clone, "choice_node_clone", choice_node_clone); +per_backend!(bench_ibig_drop_by_class, "ibig_drop", ibig_drop_by_class); +per_backend!(bench_ibig_sub_magnitude, "ibig_sub_magnitude", ibig_sub_magnitude); +per_backend!(bench_ibig_clamp, "ibig_clamp", ibig_clamp); +per_backend!(bench_ibig_double_cmp, "ibig_double_cmp", ibig_double_cmp); +per_backend!(bench_ibig_from_small_consts, "ibig_from_const", ibig_from_small_consts); +per_backend!(bench_ubig_cmp_by_class, "ubig_cmp_shrinker", ubig_cmp_by_class); +per_backend!(bench_ibig_shift_right_descent, "ibig_shr_descent", ibig_shift_right_descent); +per_backend!( + bench_shrinker_consider_workload, + "shrinker_consider", + shrinker_consider_workload +); +per_backend!( + bench_ubig_binary_search_step, + "ubig_binary_search_step", + ubig_binary_search_step +); +per_backend!(bench_ibig_hashmap_keys, "ibig_hashmap_keys", ibig_hashmap_keys); +per_backend!(bench_from_index_full_search, "from_index_full_search", from_index_full_search); +per_backend!(bench_ubig_ref_add_by_class, "ubig_ref_add", ubig_ref_add_by_class); +per_backend!(bench_ubig_ref_sub_by_class, "ubig_ref_sub", ubig_ref_sub_by_class); + +// --------------------------------------------------------------------------- +// 14. Boundary-value sort — build a Vec of ~258 boundary values +// (0, ±1, ±2^k for k in 0..=128, min, max), then dedup + sort. Exercises +// the comparison path on a mix of inline and just-over-inline values. +// --------------------------------------------------------------------------- + +fn ibig_boundary_sort(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let min = B::Signed::from_i128(i128::MIN + 1); + let max = B::Signed::from_i128(i128::MAX); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut values = vec![min.clone(), max.clone(), B::Signed::from_i64(0)]; + for sign in [1i128, -1] { + for exp in 0..=128u32 { + let v = B::Signed::from_i128(sign) + .mul_ref(&B::Signed::from_u128(1u128 << exp.min(127))); + values.push(v); + } + } + values.push(B::Signed::from_i64(rand_v08::Rng::gen_range(&mut rng, -10i64..10))); + values.sort(); + values.dedup(); + black_box(values.len()) + }) + }); +} + +per_backend!(bench_ibig_boundary_sort, "ibig_boundary_sort", ibig_boundary_sort); + +// --------------------------------------------------------------------------- +// 15. Unsigned min — used heavily in the index search: std::cmp::min(&mid, +// &above). Each binary-search iteration does 2× min (Ord::cmp + branch). +// This micro-bench isolates unsigned comparison cost on inline values. +// --------------------------------------------------------------------------- + +fn ubig_min_inline(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + std::cmp::min(black_box(a), black_box(c)) + }) + }); + } +} + +per_backend!(bench_ubig_min_inline, "ubig_min", ubig_min_inline); + +// --------------------------------------------------------------------------- +// 16. value→index lookup — the forward direction, inverse of +// `from_index_full_search`, with the same sub/magnitude/min/add shape. +// For a choice over (min, s, max) the index of `value` is: +// +// above = (max - s).magnitude() +// below = (s - min).magnitude() +// d_abs = (value - s).magnitude() +// d_minus_one = d_abs - 1 +// count = min(d_minus_one, above) + min(d_minus_one, below) +// (+ 1 or 2 depending on sign / d_abs vs above) +// +// This bench drives the body for a fixed (min, s, max) over a Vec of +// pre-sampled values, capturing the per-value cost. +// --------------------------------------------------------------------------- + +fn integer_choice_to_index(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + + // Two ranges: a small i128-bracket (the common case, matches the + // `from_index_full_search` setup) and a heap-only range, so the bench + // reflects both the inline and just-over-inline paths. + let scenarios: [Scenario; 2] = [ + ( + "i128_range", + B::Signed::from_i128(i128::MIN + 1), + B::Signed::from_i64(0), + B::Signed::from_i128(i128::MAX), + ), + ( + "heap_range", + B::Signed::from_i64(0), + B::Signed::from_i64(0), + B::Signed::from_u128(1).shl_ref(200), + ), + ]; + + for (label, min_v, s, max_v) in scenarios.iter() { + // Pre-sample 32 in-range values. Mix nasty pool with random draws + // so we don't end up only exercising one branch (`d_abs <= above`). + let values: Vec = (0..32) + .map(|i| { + let class = match i % 4 { + 1 => ValueClass::TwoWord, + _ => ValueClass::OneWord, + }; + let mag = B::sample_signed(class, &mut rng); + // Clamp into range so the lookup doesn't have to reject. + if &mag > max_v { + max_v.clone() + } else if &mag < min_v { + min_v.clone() + } else { + mag + } + }) + .collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, label), + &(min_v.clone(), s.clone(), max_v.clone(), values), + |b, (min_v, s, max_v, values)| { + let mut i = 0usize; + let one = B::Unsigned::from_u64(1); + b.iter(|| { + let v = &values[i & 31]; + i = i.wrapping_add(1); + // The value→index body, inlined. + if v == s { + B::Unsigned::from_u64(0) + } else { + let above = B::magnitude(max_v.sub_ref(s)); + let below = B::magnitude(s.sub_ref(min_v)); + let d_abs = B::magnitude(v.sub_ref(s)); + let d_minus_one = d_abs.sub_ref(&one); + let mut count = std::cmp::min(&d_minus_one, &above) + .add_ref(std::cmp::min(&d_minus_one, &below)); + if v > s { + return count.add_ref(&one); + } + if d_abs <= above { + count.add_assign_ref(&one); + } + count.add_ref(&one) + } + }) + }, + ); + } +} + +per_backend!( + bench_integer_choice_to_index, + "integer_choice_to_index", + integer_choice_to_index +); + +// --------------------------------------------------------------------------- +// 17. Lazy lexicographic sort-key compare of two node sequences. +// +// A shrinker compares pre/post candidate sequences by walking both in +// lockstep and computing per-node sort keys on the fly. The per-node key +// is `(value - shrink_towards).magnitude(), value < shrink_towards` +// (an allocated unsigned magnitude + a bool). +// +// The lazy variant is meaningfully different from +// `shrinker_consider_workload` (which eagerly materialises all sort keys +// into a `Vec`): when the sequences differ early, the lazy form does far +// less work, and the per-iteration allocation cost is what the real +// shrinker pays. Two parameterisations: +// +// * `same_prefix` — sequences agree for the first half, differ in the +// middle. Exercises the typical "small change to a long shrunk +// sequence" path. +// * `differ_at_zero` — sequences differ at position 0. Tests the early- +// exit fast path where we only allocate two sort keys. +// --------------------------------------------------------------------------- + +// Each "node" is a (value, shrink_towards) pair; the sort key is +// ((value - shrink_towards).magnitude(), value < shrink_towards). +fn sort_key(node: &(B::Signed, B::Signed)) -> (B::Unsigned, bool) { + let (value, target) = node; + (B::magnitude(value.sub_ref(target)), value < target) +} + +fn lex_cmp( + a: &[(B::Signed, B::Signed)], + b: &[(B::Signed, B::Signed)], +) -> std::cmp::Ordering { + use std::cmp::Ordering; + match a.len().cmp(&b.len()) { + Ordering::Equal => {} + ord => return ord, + } + for (x, y) in a.iter().zip(b.iter()) { + let key_x = sort_key::(x); + let key_y = sort_key::(y); + match (&key_x.0, key_x.1).cmp(&(&key_y.0, key_y.1)) { + Ordering::Equal => continue, + ord => return ord, + } + } + Ordering::Equal +} + +fn nodes_sort_key_lex_cmp(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + type Node = (::Signed, ::Signed); + + for n_nodes in [4usize, 16, 64] { + // Scenario A: sequences share the first half, differ in the middle. + let a: Vec> = (0..n_nodes) + .map(|_| (B::sample_signed(ValueClass::OneWord, &mut rng), B::Signed::from_i64(0))) + .collect(); + let mut b = a.clone(); + let mid = n_nodes / 2; + b[mid].0 = b[mid].0.add_ref(&B::Signed::from_i64(1)); + + group.bench_with_input( + BenchmarkId::new(format!("{}/same_prefix", B::NAME), n_nodes), + &(a, b), + |bn, (a, b)| { + bn.iter(|| lex_cmp::(black_box(a), black_box(b))); + }, + ); + + // Scenario B: differ at index 0 — cmp returns after one pair of + // sort_key allocations. + let a: Vec> = (0..n_nodes) + .map(|_| (B::sample_signed(ValueClass::TwoWord, &mut rng), B::Signed::from_i64(0))) + .collect(); + let mut b = a.clone(); + b[0].0 = b[0].0.add_ref(&B::Signed::from_i64(1)); + group.bench_with_input( + BenchmarkId::new(format!("{}/differ_at_zero", B::NAME), n_nodes), + &(a, b), + |bn, (a, b)| { + bn.iter(|| lex_cmp::(black_box(a), black_box(b))); + }, + ); + } +} + +per_backend!(bench_nodes_sort_key_lex_cmp, "nodes_sort_key_lex_cmp", nodes_sort_key_lex_cmp); + +// --------------------------------------------------------------------------- +// 18. Descent step — the inner loop of an integer shrink-towards-zero search: +// try candidates `base - (2 * n)` for n = 1, 2, 4, 8, ... until a +// predicate fails, then binary-search the bracket. +// +// The integer work is candidate construction + range validation: +// `from(small_const)`, `&base - that`, `cand >= min && cand <= max` — +// a sub plus two compares per probe. The bench drives a fixed step +// sequence so the cost reflects the per-probe overhead. +// --------------------------------------------------------------------------- + +fn shrinker_descent_subtract(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in &[ValueClass::OneWord, ValueClass::TwoWord] { + // 32 distinct (base, min, max) triples so the bench loop sees varied + // inputs; magnitudes match the inline workload the shrinker actually + // touches in tests. + let triples: Vec<(B::Signed, B::Signed, B::Signed)> = (0..32) + .map(|_| { + let base = B::sample_signed(class, &mut rng); + let min = base.sub_ref(&B::Signed::from_i64(1024)); + let max = base.add_ref(&B::Signed::from_i64(1024)); + (base, min, max) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &triples, |b, t| { + let mut i = 0usize; + // Exponential probe sequence used by the descent search: + // 1, 2, 3, 4 then geometric (8, 16, 32, ...). + const STEPS: [u64; 9] = [1, 2, 3, 4, 8, 16, 32, 64, 128]; + b.iter(|| { + let (base, min, max) = &t[i & 31]; + i = i.wrapping_add(1); + let mut valid_count = 0u32; + for n in STEPS { + // `&base - (2 * n)` is the shrink-by-multiples-of-2 + // probe; the linear-1 probe is `&base - n`. + let cand = base.sub_ref(&B::Signed::from_u64(2 * n)); + if &cand >= black_box(min) && &cand <= black_box(max) { + valid_count += 1; + } + let cand = base.sub_ref(&B::Signed::from_u64(n)); + if &cand >= black_box(min) && &cand <= black_box(max) { + valid_count += 1; + } + } + valid_count + }) + }); + } +} + +per_backend!( + bench_shrinker_descent_subtract, + "shrinker_descent_subtract", + shrinker_descent_subtract +); + +criterion_group!( + benches, + bench_ibig_clone_by_class, + bench_choice_node_clone, + bench_ibig_drop_by_class, + bench_ibig_sub_magnitude, + bench_ibig_clamp, + bench_ibig_double_cmp, + bench_ibig_from_small_consts, + bench_ubig_cmp_by_class, + bench_ibig_shift_right_descent, + bench_shrinker_consider_workload, + bench_ubig_binary_search_step, + bench_ibig_hashmap_keys, + bench_from_index_full_search, + bench_ubig_ref_add_by_class, + bench_ubig_ref_sub_by_class, + bench_ibig_boundary_sort, + bench_ubig_min_inline, + bench_integer_choice_to_index, + bench_nodes_sort_key_lex_cmp, + bench_shrinker_descent_subtract, +); + +criterion_main!(benches); diff --git a/integer-bench/benches/small_int.rs b/integer-bench/benches/small_int.rs new file mode 100644 index 00000000..8c2c6b94 --- /dev/null +++ b/integer-bench/benches/small_int.rs @@ -0,0 +1,689 @@ +//! Fast-path / small-value benchmarks. +//! +//! The `primitive` bit-width sweep runs over 10..=10^4 bits and so under-covers +//! the small-integer range (≤ 128 bits, where most libraries keep the value +//! inline/on the fast path). This file fills that gap: each group runs across +//! every `ValueClass`, including `Zero`, `OneWord`, and `TwoWord` which the +//! bit-width sweep never reaches. +//! +//! Each bench body is generic over [`Backend`] and run for every backend, so +//! dashu and the comparison libraries land in one criterion report under the +//! backend name. The pure-Rust backends (dashu, ibig, num-bigint, malachite) +//! are always built; rug is added with `--features gmp` (needs the GMP +//! toolchain). +//! +//! Run: +//! cargo bench --manifest-path integer-bench/Cargo.toml --bench small_int -- --quick +//! Include the rug backend too (needs the GMP toolchain): add `--features gmp`. + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use criterion::measurement::WallTime; +use criterion::{ + black_box, criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, +}; +use integer_bench::{ + seeded_rng, Backend, BenchInt, Dashu, Ibig, Malachite, Num, SignedInt, UnsignedInt, ValueClass, +}; + +#[cfg(feature = "gmp")] +use integer_bench::Rug; + +/// Define a criterion entry point `$name` that opens group `$group` and runs +/// the generic body `$body` for every backend (dashu, ibig, num and malachite +/// always; rug when the `gmp` feature is on). +macro_rules! per_backend { + ($name:ident, $group:literal, $body:ident) => { + fn $name(c: &mut Criterion) { + let mut group = c.benchmark_group($group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + #[cfg(feature = "gmp")] + $body::(&mut group); + group.finish(); + } + }; +} + +// ---- construction from primitives ---- + +fn from_i64(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..256).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = inputs[i & 255]; + i = i.wrapping_add(1); + B::Signed::from_i64(black_box(v)) + }) + }); +} + +fn from_i128(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..256).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = inputs[i & 255]; + i = i.wrapping_add(1); + B::Signed::from_i128(black_box(v)) + }) + }); +} + +fn from_u64(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..256).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = inputs[i & 255]; + i = i.wrapping_add(1); + B::Unsigned::from_u64(black_box(v)) + }) + }); +} + +fn from_u128(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..256).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = inputs[i & 255]; + i = i.wrapping_add(1); + B::Unsigned::from_u128(black_box(v)) + }) + }); +} + +per_backend!(bench_from_i64, "ibig_from_i64", from_i64); +per_backend!(bench_from_i128, "ibig_from_i128", from_i128); +per_backend!(bench_from_u64, "ubig_from_u64", from_u64); +per_backend!(bench_from_u128, "ubig_from_u128", from_u128); + +// ---- TryInto primitives (round-trip cost) ---- + +fn try_into_i128(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..256) + .map(|_| B::sample_signed(ValueClass::TwoWord, &mut rng)) + .collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = &inputs[i & 255]; + i = i.wrapping_add(1); + black_box(v).try_to_i128() + }) + }); +} + +per_backend!(bench_try_into_i128, "ibig_try_into_i128", try_into_i128); + +// ---- binops parameterised by class ---- + +fn ubig_add_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).add_ref(black_box(c)) + }) + }); + } +} + +fn ubig_mul_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).mul_ref(black_box(c)) + }) + }); + } +} + +fn ibig_add_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let pairs: Vec<(B::Signed, B::Signed)> = (0..32) + .map(|_| (B::sample_signed(class, &mut rng), B::sample_signed(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).add_ref(black_box(c)) + }) + }); + } +} + +// Mixed-class: one operand drawn from a small class, the other from a larger +// one. Models the "running total += small constant" pattern that pure +// same-class benches miss. +fn ubig_add_mixed(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &bigger in &[ + ValueClass::JustOverInline, + ValueClass::Mid, + ValueClass::Large, + ] { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| { + ( + B::sample_unsigned(bigger, &mut rng), + B::sample_unsigned(ValueClass::OneWord, &mut rng), + ) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, bigger.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).add_ref(black_box(c)) + }) + }); + } +} + +per_backend!(bench_ubig_add_by_class, "ubig_add_by_class", ubig_add_by_class); +per_backend!(bench_ubig_mul_by_class, "ubig_mul_by_class", ubig_mul_by_class); +per_backend!(bench_ibig_add_by_class, "ibig_add_by_class", ibig_add_by_class); +per_backend!(bench_ubig_add_mixed, "ubig_add_mixed", ubig_add_mixed); + +// ---- assign-form binops ---- +// +// The by-ref benches above exercise `Add` / `+`. The benches below exercise the +// in-place `+= &T` form, which is the entry point on the running-sum hot path +// and the natural place for an in-place specialisation. + +fn ubig_add_assign_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let starts: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + let rhs: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, class.label()), + &(starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.add_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +fn ibig_add_assign_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let starts: Vec = (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + let rhs: Vec = (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, class.label()), + &(starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.add_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +fn ubig_sub_assign_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + // For the unsigned type, build acc = a + b and subtract b, so the result + // is non-negative. + for &class in ValueClass::ALL { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| { + let a = B::sample_unsigned(class, &mut rng); + let b = B::sample_unsigned(class, &mut rng); + (a.add_ref(&b), b) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (start, rhs) = &p[i & 31]; + let mut acc = start.clone(); + acc.sub_assign_ref(black_box(rhs)); + i = i.wrapping_add(1); + acc + }) + }); + } +} + +fn ibig_sub_assign_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let starts: Vec = (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + let rhs: Vec = (0..32).map(|_| B::sample_signed(class, &mut rng)).collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, class.label()), + &(starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.sub_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +per_backend!( + bench_ubig_add_assign_by_class, + "ubig_add_assign_by_class", + ubig_add_assign_by_class +); +per_backend!( + bench_ibig_add_assign_by_class, + "ibig_add_assign_by_class", + ibig_add_assign_by_class +); +per_backend!( + bench_ubig_sub_assign_by_class, + "ubig_sub_assign_by_class", + ubig_sub_assign_by_class +); +per_backend!( + bench_ibig_sub_assign_by_class, + "ibig_sub_assign_by_class", + ibig_sub_assign_by_class +); + +// Diagnostic for the "heap accumulator, small RHS" path: the accumulator is +// heap-resident every iteration and stays heap-resident (no shrink possible) +// — the case where per-step reduce/allocate finalisation is pure overhead. A +// successful in-place AddAssign specialisation should move this bench +// substantially while leaving the same-class benches above largely flat. +fn ubig_add_assign_heap_acc_small_rhs(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &acc_class in &[ + ValueClass::JustOverInline, + ValueClass::Mid, + ValueClass::Large, + ] { + let acc_starts: Vec = (0..32) + .map(|_| B::sample_unsigned(acc_class, &mut rng)) + .collect(); + let rhs: Vec = (0..32) + .map(|_| B::sample_unsigned(ValueClass::OneWord, &mut rng)) + .collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, acc_class.label()), + &(acc_starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.add_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +fn ibig_add_assign_heap_acc_small_rhs(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &acc_class in &[ + ValueClass::JustOverInline, + ValueClass::Mid, + ValueClass::Large, + ] { + let acc_starts: Vec = (0..32) + .map(|_| B::sample_signed(acc_class, &mut rng)) + .collect(); + let rhs: Vec = (0..32) + .map(|_| B::sample_signed(ValueClass::OneWord, &mut rng)) + .collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, acc_class.label()), + &(acc_starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.add_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +per_backend!( + bench_ubig_add_assign_heap_acc_small_rhs, + "ubig_add_assign_heap_acc_small_rhs", + ubig_add_assign_heap_acc_small_rhs +); +per_backend!( + bench_ibig_add_assign_heap_acc_small_rhs, + "ibig_add_assign_heap_acc_small_rhs", + ibig_add_assign_heap_acc_small_rhs +); + +// Primitive-RHS AddAssign benches — the recommendation explicitly mentions +// `` / `` / `` / `` variants of the specialised path. +// Each starts from a heap-resident accumulator so the per-step finalisation +// cost is visible; lifting it via specialisation should be measurable here. + +fn ibig_add_assign_i64_into_heap_acc(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let acc_starts: Vec = (0..32) + .map(|_| B::sample_signed(ValueClass::Mid, &mut rng)) + .collect(); + let rhs: Vec = (0..32).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let mut acc = acc_starts[i & 31].clone(); + acc.add_assign_i64(black_box(rhs[i & 31])); + i = i.wrapping_add(1); + acc + }) + }); +} + +fn ibig_add_assign_i128_into_heap_acc(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let acc_starts: Vec = (0..32) + .map(|_| B::sample_signed(ValueClass::Mid, &mut rng)) + .collect(); + let rhs: Vec = (0..32).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let mut acc = acc_starts[i & 31].clone(); + acc.add_assign_i128(black_box(rhs[i & 31])); + i = i.wrapping_add(1); + acc + }) + }); +} + +fn ubig_add_assign_u64_into_heap_acc(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let acc_starts: Vec = (0..32) + .map(|_| B::sample_unsigned(ValueClass::Mid, &mut rng)) + .collect(); + let rhs: Vec = (0..32).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let mut acc = acc_starts[i & 31].clone(); + acc.add_assign_u64(black_box(rhs[i & 31])); + i = i.wrapping_add(1); + acc + }) + }); +} + +fn ubig_add_assign_u128_into_heap_acc(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let acc_starts: Vec = (0..32) + .map(|_| B::sample_unsigned(ValueClass::Mid, &mut rng)) + .collect(); + let rhs: Vec = (0..32).map(|_| rand_v08::Rng::gen(&mut rng)).collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let mut acc = acc_starts[i & 31].clone(); + acc.add_assign_u128(black_box(rhs[i & 31])); + i = i.wrapping_add(1); + acc + }) + }); +} + +per_backend!( + bench_ibig_add_assign_i64_into_heap_acc, + "ibig_add_assign_i64_into_heap_acc", + ibig_add_assign_i64_into_heap_acc +); +per_backend!( + bench_ibig_add_assign_i128_into_heap_acc, + "ibig_add_assign_i128_into_heap_acc", + ibig_add_assign_i128_into_heap_acc +); +per_backend!( + bench_ubig_add_assign_u64_into_heap_acc, + "ubig_add_assign_u64_into_heap_acc", + ubig_add_assign_u64_into_heap_acc +); +per_backend!( + bench_ubig_add_assign_u128_into_heap_acc, + "ubig_add_assign_u128_into_heap_acc", + ubig_add_assign_u128_into_heap_acc +); + +// One bitwise-assign bench to verify the same-shape claim ("Sub, BitAnd, +// BitOr, BitXor are all the same pattern") will land — full coverage of all +// three bitwise ops can pile on once the Add specialisation lands. +fn ubig_bitxor_assign_by_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let starts: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + let rhs: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + group.bench_with_input( + BenchmarkId::new(B::NAME, class.label()), + &(starts, rhs), + |b, (s, r)| { + let mut i = 0usize; + b.iter(|| { + let mut acc = s[i & 31].clone(); + acc.bitxor_assign_ref(black_box(&r[i & 31])); + i = i.wrapping_add(1); + acc + }) + }, + ); + } +} + +per_backend!( + bench_ubig_bitxor_assign_by_class, + "ubig_bitxor_assign_by_class", + ubig_bitxor_assign_by_class +); + +// ---- comparison / hash / clone (cheap operations that dominate hot loops) ---- + +fn ubig_eq_same_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + // Half identical pairs, half non-equal pairs, so the benchmark sees + // both branches of the eq fast path. + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..64) + .map(|i| { + let a = B::sample_unsigned(class, &mut rng); + let b = if i % 2 == 0 { + a.clone() + } else { + B::sample_unsigned(class, &mut rng) + }; + (a, b) + }) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 63]; + i = i.wrapping_add(1); + black_box(a) == black_box(c) + }) + }); + } +} + +fn ubig_cmp_same_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let pairs: Vec<(B::Unsigned, B::Unsigned)> = (0..32) + .map(|_| (B::sample_unsigned(class, &mut rng), B::sample_unsigned(class, &mut rng))) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &pairs, |b, p| { + let mut i = 0usize; + b.iter(|| { + let (a, c) = &p[i & 31]; + i = i.wrapping_add(1); + black_box(a).cmp(black_box(c)) + }) + }); + } +} + +fn ubig_hash_same_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let inputs: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &inputs, |b, v| { + let mut i = 0usize; + b.iter(|| { + let x = &v[i & 31]; + i = i.wrapping_add(1); + let mut h = DefaultHasher::new(); + black_box(x).hash(&mut h); + h.finish() + }) + }); + } +} + +fn ubig_clone_same_class(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + for &class in ValueClass::ALL { + let inputs: Vec = (0..32) + .map(|_| B::sample_unsigned(class, &mut rng)) + .collect(); + group.bench_with_input(BenchmarkId::new(B::NAME, class.label()), &inputs, |b, v| { + let mut i = 0usize; + b.iter(|| { + let x = &v[i & 31]; + i = i.wrapping_add(1); + black_box(x).clone() + }) + }); + } +} + +per_backend!(bench_ubig_eq_same_class, "ubig_eq", ubig_eq_same_class); +per_backend!(bench_ubig_cmp_same_class, "ubig_cmp", ubig_cmp_same_class); +per_backend!(bench_ubig_hash_same_class, "ubig_hash", ubig_hash_same_class); +per_backend!(bench_ubig_clone_same_class, "ubig_clone", ubig_clone_same_class); + +// ---- string round-trip (decimal-string interchange) ---- + +fn ibig_display_small(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..128) + .map(|_| B::sample_signed(ValueClass::OneWord, &mut rng)) + .collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let v = &inputs[i & 127]; + i = i.wrapping_add(1); + black_box(v).to_string() + }) + }); +} + +fn ibig_from_str_small(group: &mut BenchmarkGroup) { + let mut rng = seeded_rng(); + let inputs: Vec = (0..128) + .map(|_| B::sample_signed(ValueClass::OneWord, &mut rng).to_string()) + .collect(); + group.bench_function(B::NAME, |b| { + let mut i = 0usize; + b.iter(|| { + let s = &inputs[i & 127]; + i = i.wrapping_add(1); + B::Signed::parse(black_box(s)) + }) + }); +} + +per_backend!(bench_ibig_display_small, "ibig_display_small", ibig_display_small); +per_backend!(bench_ibig_from_str_small, "ibig_from_str_small", ibig_from_str_small); + +criterion_group!( + benches, + bench_from_i64, + bench_from_i128, + bench_from_u64, + bench_from_u128, + bench_try_into_i128, + bench_ubig_add_by_class, + bench_ubig_mul_by_class, + bench_ibig_add_by_class, + bench_ubig_add_mixed, + bench_ubig_add_assign_by_class, + bench_ibig_add_assign_by_class, + bench_ubig_sub_assign_by_class, + bench_ibig_sub_assign_by_class, + bench_ubig_add_assign_heap_acc_small_rhs, + bench_ibig_add_assign_heap_acc_small_rhs, + bench_ibig_add_assign_i64_into_heap_acc, + bench_ibig_add_assign_i128_into_heap_acc, + bench_ubig_add_assign_u64_into_heap_acc, + bench_ubig_add_assign_u128_into_heap_acc, + bench_ubig_bitxor_assign_by_class, + bench_ubig_eq_same_class, + bench_ubig_cmp_same_class, + bench_ubig_hash_same_class, + bench_ubig_clone_same_class, + bench_ibig_display_small, + bench_ibig_from_str_small, +); + +criterion_main!(benches); diff --git a/integer-bench/benches/workload.rs b/integer-bench/benches/workload.rs new file mode 100644 index 00000000..fa24a24b --- /dev/null +++ b/integer-bench/benches/workload.rs @@ -0,0 +1,349 @@ +//! Scenario benchmarks for a property-based-testing-style workload. +//! +//! These benchmarks are derived from profiling specific hot paths that show +//! up when profiling the test suite of +//! [hegel-rust](https://github.com/hegeldev/hegel-rust), but should mostly +//! just be taken as interesting examples of realistic workloads. These ones +//! are particularly focused on hot paths that occur during generation. +//! +//! Each bench body is generic over [`Backend`] and run for every backend, so +//! dashu and the comparison libraries land in one criterion report under the +//! backend name. The pure-Rust backends (dashu, ibig, num-bigint, malachite) +//! are always built; rug is added with `--features gmp` (needs the GMP +//! toolchain). +//! +//! Run: +//! cargo bench --manifest-path integer-bench/Cargo.toml --bench workload -- --quick +//! Include the rug backend too (needs the GMP toolchain): add `--features gmp`. + +use criterion::measurement::WallTime; +use criterion::{black_box, criterion_group, criterion_main, BenchmarkGroup, Criterion}; +use integer_bench::{ + mixed_class, seeded_rng, Backend, BenchInt, Dashu, Ibig, Malachite, Num, SignedInt, ValueClass, +}; +use rand_v08::Rng; + +#[cfg(feature = "gmp")] +use integer_bench::Rug; + +const N: usize = 4096; + +/// Define a criterion entry point `$name` that opens group `$group` and runs +/// the generic body `$body` for every backend (dashu, ibig, num and malachite +/// always; rug when the `gmp` feature is on). +macro_rules! per_backend { + ($name:ident, $group:literal, $body:ident) => { + fn $name(c: &mut Criterion) { + let mut group = c.benchmark_group($group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + $body::(&mut group); + #[cfg(feature = "gmp")] + $body::(&mut group); + group.finish(); + } + }; +} + +fn build_mixed_inputs() -> Vec { + let mut rng = seeded_rng(); + (0..N) + .map(|_| B::sample_signed(mixed_class(&mut rng), &mut rng)) + .collect() +} + +/// Small-only input distribution (every value ≤ 128 bits, on the inline / +/// fast path for most libraries). Roughly 1/3 TwoWord, 2/3 OneWord — a +/// small-but-not-trivial mix. +fn build_small_inputs() -> Vec { + let mut rng = seeded_rng(); + (0..N) + .map(|_| { + let class = if rng.gen::() % 3 == 0 { + ValueClass::TwoWord + } else { + ValueClass::OneWord + }; + B::sample_signed(class, &mut rng) + }) + .collect() +} + +/// Sub-1-kbit mixed-class distribution: same shape as `mixed_class` (small +/// values dominate) but bounded to ≤ 256 bits, so no operand ever pushes the +/// accumulator past the 1-kbit regime. The 2 % `Mid` (1024-bit, right at the +/// boundary) and 1 % `Large` (100 kbit) slots of `mixed_class` get +/// redistributed to `JustOverInline` (192-bit) and the inline classes — that +/// keeps the mixed-scale flavour without inviting GMP's asymptotic kernels +/// into the bench. +fn under_1kbit_class(rng: &mut R) -> ValueClass { + let r: u32 = rng.gen_range(0..100); + match r { + 0..=4 => ValueClass::Zero, // 5 % + 5..=64 => ValueClass::OneWord, // 60 % + 65..=89 => ValueClass::TwoWord, // 25 % + _ => ValueClass::JustOverInline, // 10 % (was 7 % + redirected Mid/Large) + } +} + +fn build_under_1kbit_inputs() -> Vec { + let mut rng = seeded_rng(); + (0..N) + .map(|_| B::sample_signed(under_1kbit_class(&mut rng), &mut rng)) + .collect() +} + +/// Scenario 1: running-sum-and-compare. +/// +/// Mirrors a targeting/score loop: every step adds the next value into an +/// accumulator and checks whether it crossed a bound. Most inputs fit in i64 +/// (per `mixed_class`); the accumulator may grow. +fn running_sum_and_compare(group: &mut BenchmarkGroup) { + let inputs = build_mixed_inputs::(); + let bound = B::Signed::from_i64(1).shl_ref(200); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut sum = B::Signed::from_i64(0); + let mut hits = 0u32; + for v in &inputs { + sum.add_assign_ref(black_box(v)); + if black_box(&sum) >= black_box(&bound) { + hits += 1; + sum = B::Signed::from_i64(0); + } + } + (sum, hits) + }) + }); +} + +/// Scenario 2: string round-trip. +/// +/// When bigints cross a serialization boundary as decimal strings, every value +/// is formatted to and parsed from decimal. This bench measures the +/// steady-state cost of that path on mostly-small values. +fn string_round_trip(group: &mut BenchmarkGroup) { + let inputs = build_mixed_inputs::(); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut last = B::Signed::from_i64(0); + for v in &inputs { + let s = black_box(v).to_string(); + last = B::Signed::parse(&s); + } + last + }) + }); +} + +/// Sub-1-kbit string round-trip. Same shape as the mixed-input version +/// but every value ≤ 256 bits, so no input pulls the bench into GMP's +/// asymptotic-base-conversion regime. +fn string_round_trip_under_1kbit(group: &mut BenchmarkGroup) { + let inputs = build_under_1kbit_inputs::(); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut last = B::Signed::from_i64(0); + for v in &inputs { + let s = black_box(v).to_string(); + last = B::Signed::parse(&s); + } + last + }) + }); +} + +/// Scenario 3: bounded arithmetic mix. +/// +/// A scripted sequence of `+`, `-`, `*`, `<<`, `&` over a small working set. +/// Simulates one step of stateful test execution where most intermediate +/// values stay inline. The exact op sequence is fixed so successive runs +/// are comparable. +fn bounded_arithmetic_mix(group: &mut BenchmarkGroup) { + let inputs = build_mixed_inputs::(); + group.bench_function(B::NAME, |b| { + b.iter(|| { + // Four live registers, refreshed periodically from `inputs`. + let mut r0 = B::Signed::from_i64(0); + let mut r1 = B::Signed::from_i64(1); + let mut r2 = B::Signed::from_i64(-1); + let mut r3 = B::Signed::from_i64(2); + for (i, v) in inputs.iter().enumerate() { + match i & 7 { + 0 => r0 = r0.add_ref(black_box(v)), + 1 => r1 = r1.sub_ref(black_box(v)), + 2 => r2 = r2.mul_ref(black_box(v)), + 3 => r3 = r3.add_ref(&r0), + 4 => r0 = r0.bitxor_ref(&r1), + 5 => r1 = r2.bitand_ref(black_box(v)), + 6 => r2 = r3.shl_ref(1), + _ => r3 = r0.add_ref(&r2), + } + } + (r0, r1, r2, r3) + }) + }); +} + +/// Scenario 1-small: running-sum-and-compare over ≤ 128-bit inputs. +/// +/// Every RHS is small but the accumulator can grow heap-resident, so the +/// dominant cost is the per-step reduce/allocate finalisation on the +/// AddAssign path — a headline detector for in-place AddAssign work. +fn running_sum_and_compare_small(group: &mut BenchmarkGroup) { + let inputs = build_small_inputs::(); + let bound = B::Signed::from_i64(1).shl_ref(200); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut sum = B::Signed::from_i64(0); + let mut hits = 0u32; + for v in &inputs { + sum.add_assign_ref(black_box(v)); + if black_box(&sum) >= black_box(&bound) { + hits += 1; + sum = B::Signed::from_i64(0); + } + } + (sum, hits) + }) + }); +} + +/// Scenario 1-under_1kbit: running-sum-and-compare strictly bounded to +/// values ≤ 256 bits. The `_small` variant covers the all-inline case; this +/// one covers the more interesting "mostly inline, occasionally just-over- +/// inline heap" regime that the user's < 1-kbit performance target is +/// directly about. +fn running_sum_and_compare_under_1kbit(group: &mut BenchmarkGroup) { + let inputs = build_under_1kbit_inputs::(); + let bound = B::Signed::from_i64(1).shl_ref(200); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut sum = B::Signed::from_i64(0); + let mut hits = 0u32; + for v in &inputs { + sum.add_assign_ref(black_box(v)); + if black_box(&sum) >= black_box(&bound) { + hits += 1; + sum = B::Signed::from_i64(0); + } + } + (sum, hits) + }) + }); +} + +/// Scripted arithmetic mix where every register stays bounded. +/// +/// The earlier `bounded_arithmetic_mix*` benches grow `r2` unboundedly via +/// `r2 = &r2 * v` (and `r3` via `r3 << 1`), so by the end of a single +/// `b.iter` invocation `r2` is ~32 kbit — well outside the user's < 1 kbit +/// target. Here every reassignment writes a result whose magnitude is +/// bounded by `O(input_size)`: multiplication is between two fresh inputs +/// (≤ 512 bits), shifts and bitwise ops can only grow by one bit per op, +/// and the chained sums/diffs use freshly-drawn inputs as one operand. All +/// four registers therefore stay under 1 kbit for the entire loop. +fn bounded_arithmetic_mix_under_1kbit(group: &mut BenchmarkGroup) { + let inputs = build_under_1kbit_inputs::(); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut r0 = inputs[0].clone(); + let mut r1 = inputs[1].clone(); + let mut r2 = inputs[2].clone(); + let mut r3 = inputs[3].clone(); + for (i, v) in inputs.iter().enumerate() { + let w = &inputs[i.wrapping_add(7) & (N - 1)]; + match i & 7 { + 0 => r0 = r1.sub_ref(black_box(v)), + 1 => r1 = r0.bitxor_ref(&r2), + 2 => r2 = black_box(v).sub_ref(&r3), + 3 => r3 = r0.bitand_ref(black_box(v)), + 4 => r0 = r2.add_ref(black_box(v)), + 5 => r1 = r3.shl_ref(1), + 6 => r2 = black_box(v).mul_ref(w), + _ => r3 = r1.sub_ref(&r0), + } + } + (r0, r1, r2, r3) + }) + }); +} + +/// Same shape as `bounded_arithmetic_mix_under_1kbit`, but inputs strictly +/// inline (≤ 128 bits). Every register stays bounded for the same reasons. +fn bounded_arithmetic_mix_small(group: &mut BenchmarkGroup) { + let inputs = build_small_inputs::(); + group.bench_function(B::NAME, |b| { + b.iter(|| { + let mut r0 = inputs[0].clone(); + let mut r1 = inputs[1].clone(); + let mut r2 = inputs[2].clone(); + let mut r3 = inputs[3].clone(); + for (i, v) in inputs.iter().enumerate() { + let w = &inputs[i.wrapping_add(7) & (N - 1)]; + match i & 7 { + 0 => r0 = r1.sub_ref(black_box(v)), + 1 => r1 = r0.bitxor_ref(&r2), + 2 => r2 = black_box(v).sub_ref(&r3), + 3 => r3 = r0.bitand_ref(black_box(v)), + 4 => r0 = r2.add_ref(black_box(v)), + 5 => r1 = r3.shl_ref(1), + 6 => r2 = black_box(v).mul_ref(w), + _ => r3 = r1.sub_ref(&r0), + } + } + (r0, r1, r2, r3) + }) + }); +} + +// TODO: a fourth scenario derived from a real `generic-ints` trace once the +// repo is available locally. + +per_backend!( + bench_running_sum_and_compare, + "running_sum_and_compare", + running_sum_and_compare +); +per_backend!( + bench_running_sum_and_compare_small, + "running_sum_and_compare_small", + running_sum_and_compare_small +); +per_backend!( + bench_running_sum_and_compare_under_1kbit, + "running_sum_and_compare_under_1kbit", + running_sum_and_compare_under_1kbit +); +per_backend!(bench_string_round_trip, "string_round_trip", string_round_trip); +per_backend!( + bench_string_round_trip_under_1kbit, + "string_round_trip_under_1kbit", + string_round_trip_under_1kbit +); +per_backend!(bench_bounded_arithmetic_mix, "bounded_arithmetic_mix", bounded_arithmetic_mix); +per_backend!( + bench_bounded_arithmetic_mix_small, + "bounded_arithmetic_mix_small", + bounded_arithmetic_mix_small +); +per_backend!( + bench_bounded_arithmetic_mix_under_1kbit, + "bounded_arithmetic_mix_under_1kbit", + bounded_arithmetic_mix_under_1kbit +); + +criterion_group!( + benches, + bench_running_sum_and_compare, + bench_running_sum_and_compare_small, + bench_running_sum_and_compare_under_1kbit, + bench_string_round_trip, + bench_string_round_trip_under_1kbit, + bench_bounded_arithmetic_mix, + bench_bounded_arithmetic_mix_small, + bench_bounded_arithmetic_mix_under_1kbit, +); + +criterion_main!(benches); diff --git a/integer-bench/src/lib.rs b/integer-bench/src/lib.rs new file mode 100644 index 00000000..f63b50da --- /dev/null +++ b/integer-bench/src/lib.rs @@ -0,0 +1,1271 @@ +//! Shared helpers and the generic backend abstraction for the dashu-int +//! comparison benchmarks (`primitive`, `small_int`, `workload`, `shrinker`). +//! +//! Each criterion bench body is written once over [`Backend`] and run for every +//! backend, with the backend name as a `BenchmarkId` dimension, so one run +//! reports them side-by-side. The pure-Rust backends (dashu, ibig, num, +//! malachite) are always available; rug (GNU GMP) is added under the `gmp` +//! feature, since it needs the GMP toolchain. + +#![allow(dead_code)] + +use dashu_int::{IBig, UBig}; +use rand_v08::prelude::*; +use rand_v08::rngs::StdRng; + +/// Coarse value-magnitude classes used to drive the bench parameter sweeps. +/// +/// Intent is to exercise the small / inline-magnitude ranges (zero, one word, +/// two words, just-over-inline) rather than only the large-buffer paths that +/// the bit-width sweep covers. +#[derive(Clone, Copy, Debug)] +pub enum ValueClass { + /// Exactly zero. Common in real workloads (initial accumulators, defaults). + Zero, + /// Fits in a single `Word` (≤ 64 bits on 64-bit targets). Single-word inline. + OneWord, + /// Needs both inline words (65–128 bits). Still inline, but the upper word + /// is meaningful. + TwoWord, + /// Just past the inline boundary (129–256 bits). Heap-allocated but tiny. + JustOverInline, + /// Medium (~1024 bits). Multi-limb but still in fast-path territory for + /// schoolbook arithmetic. + Mid, + /// Large (~10k bits). Past the ~1-kbit crossover where the GMP-backed and + /// asymptotically-faster libraries pull ahead. + Large, +} + +impl ValueClass { + pub const ALL: &'static [ValueClass] = &[ + ValueClass::Zero, + ValueClass::OneWord, + ValueClass::TwoWord, + ValueClass::JustOverInline, + ValueClass::Mid, + ValueClass::Large, + ]; + + pub fn label(&self) -> &'static str { + match self { + ValueClass::Zero => "zero", + ValueClass::OneWord => "one_word", + ValueClass::TwoWord => "two_word", + ValueClass::JustOverInline => "just_over_inline", + ValueClass::Mid => "mid", + ValueClass::Large => "large", + } + } +} + +/// Sample a `UBig` from the given class. +pub fn sample_ubig(class: ValueClass, rng: &mut R) -> UBig { + match class { + ValueClass::Zero => UBig::from(0u32), + // Non-zero so the inline path is exercised meaningfully. + ValueClass::OneWord => UBig::from(rng.gen::() | 1), + ValueClass::TwoWord => { + let lo: u64 = rng.gen(); + let hi: u64 = rng.gen::() | (1 << 63); // force the top word non-empty + (UBig::from(hi) << 64) + UBig::from(lo) + } + ValueClass::JustOverInline => random_ubig(192, rng), + ValueClass::Mid => random_ubig(1024, rng), + ValueClass::Large => random_ubig(10_000, rng), + } +} + +/// Same shape as `sample_ubig`, but produces `IBig` and alternates sign for +/// even/odd RNG draws so negative paths get exercised. +pub fn sample_ibig(class: ValueClass, rng: &mut R) -> IBig { + let mag = IBig::from(sample_ubig(class, rng)); + if rng.gen::() { + -mag + } else { + mag + } +} + +/// Helper modelled on the one in `primitive.rs`: a uniformly distributed UBig +/// of approximately `bits` bits (at least 2^(bits-1)). +pub fn random_ubig(bits: usize, rng: &mut R) -> UBig { + rng.gen_range(UBig::ONE << (bits - 1)..UBig::ONE << bits) +} + +/// Draw a class from a distribution that approximates a shrinker-style +/// workload: small values dominate, large values are rare. Numbers sum to 100. +pub fn mixed_class(rng: &mut R) -> ValueClass { + let r: u32 = rng.gen_range(0..100); + match r { + 0..=4 => ValueClass::Zero, + 5..=64 => ValueClass::OneWord, + 65..=89 => ValueClass::TwoWord, + 90..=96 => ValueClass::JustOverInline, + 97..=98 => ValueClass::Mid, + _ => ValueClass::Large, + } +} + +pub fn seeded_rng() -> StdRng { + StdRng::seed_from_u64(0x0DA5_4BE4) +} + +// Cross-library sampling helpers. Every backend samples by drawing a dashu +// value (so the RNG is consumed identically and magnitudes line up across +// libraries) and then converting it. The conversion goes through a hex string, +// which every candidate library parses in O(n) — fine for setup-only work. + +/// Hex digits of an unsigned dashu value. +#[allow(dead_code)] +fn ubig_hex(u: &UBig) -> String { + u.in_radix(16).to_string() +} + +/// `(is_negative, hex-of-magnitude)` for a signed dashu value. +#[allow(dead_code)] +fn ibig_sign_hex(i: IBig) -> (bool, String) { + let neg = i < IBig::from(0i32); + (neg, i.unsigned_abs().in_radix(16).to_string()) +} + +// --------------------------------------------------------------------------- +// Rug helper — only compiled under the `gmp` feature, used by the `Rug` backend +// below. The rug samplers (in the `Rug` impl) draw a dashu value and convert it +// here, exactly like the other backends, so magnitudes track `sample_ubig` / +// `sample_ibig` automatically. +// --------------------------------------------------------------------------- + +#[cfg(feature = "gmp")] +#[allow(unused_imports)] +pub use rug_side::*; + +#[cfg(feature = "gmp")] +mod rug_side { + use dashu_int::UBig; + use rug::Integer as RugInt; + + /// Convert a `UBig` of any size to a `rug::Integer`. Goes via the byte + /// representation rather than the limb words because rug exposes + /// `Integer::from_digits` for that, and the conversion is one-off (used + /// only in bench setup, never in the timed loop). + pub fn ubig_to_rug(u: &UBig) -> RugInt { + let bytes = u.to_be_bytes(); + RugInt::from_digits(&bytes, rug::integer::Order::Msf) + } +} + +// =========================================================================== +// Generic backend abstraction. +// +// Each criterion bench body is written once over `Backend` and run against +// every enabled backend, with the backend name as a `BenchmarkId` dimension, +// so one `cargo bench` run reports them side-by-side. The pure-Rust backends +// (dashu, ibig, num-bigint, malachite) are always built; rug (GNU GMP) is +// added under the `gmp` feature. +// +// This revives the trait-based, multi-library approach of the top-level +// `benchmark/` harness while emitting criterion measurements. +// +// `Backend` selects the concrete unsigned/signed integer types and samplers; +// `BenchInt` (+ `UnsignedInt` / `SignedInt`) supply the operations the bench +// bodies call. The unsigned/signed split mirrors dashu's real `UBig` / `IBig` +// divide; rug uses its single signed `Integer` for both associated types, +// while num / malachite / ibig have a `BigUint`/`BigInt`-style split like dashu. +// +// Some libraries' by-reference operators return lazy "incomplete-computation" +// values rather than an owned integer, so the operations can't be expressed +// through the std `Add`/`Sub`/... bounds directly — each is a method here, +// exactly as the prior-art `Natural` trait did with `mul_ref`. Every backend +// samples by drawing a dashu value and converting it, so magnitudes line up +// point-for-point across libraries. +// =========================================================================== + +use core::fmt::Display; +use core::hash::Hash; +use dashu_int::fast_div::ConstDivisor; +use dashu_int::ops::{ExtendedGcd, Gcd, UnsignedAbs}; + +/// Operations shared by the unsigned and signed bench integer types. +/// +/// Every method returns an owned value or mutates in place, so it covers both +/// dashu (operators already return owned) and rug (operators return a lazy +/// incomplete value that the impl finalises with `Integer::from`). +pub trait BenchInt: Clone + Ord + Hash + Display { + fn parse(s: &str) -> Self; + + fn add_ref(&self, rhs: &Self) -> Self; + fn sub_ref(&self, rhs: &Self) -> Self; + fn mul_ref(&self, rhs: &Self) -> Self; + fn div_ref(&self, rhs: &Self) -> Self; + fn bitand_ref(&self, rhs: &Self) -> Self; + fn bitxor_ref(&self, rhs: &Self) -> Self; + fn shl_ref(&self, bits: usize) -> Self; + fn shr_ref(&self, bits: usize) -> Self; + + // The `*_assign_ref` ops default to `*self = self.op_ref(rhs)`. Backends + // with a native in-place operator (dashu, rug, ...) override them so the + // benches measure the real `+=` path; libraries without one fall back to + // the allocating form, which is what they'd do in practice anyway. + fn add_assign_ref(&mut self, rhs: &Self) { + *self = self.add_ref(rhs); + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self = self.sub_ref(rhs); + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self = self.bitxor_ref(rhs); + } +} + +/// Construction and primitive `+=` for the unsigned type. `UBig` never builds +/// from a signed primitive, so only the unsigned constructors live here. +pub trait UnsignedInt: BenchInt { + fn from_u64(v: u64) -> Self; + fn from_u128(v: u128) -> Self; + // Default to constructing the RHS and adding; backends with a native + // `+= u64` / `+= u128` override. + fn add_assign_u64(&mut self, rhs: u64) { + *self = self.add_ref(&Self::from_u64(rhs)); + } + fn add_assign_u128(&mut self, rhs: u128) { + *self = self.add_ref(&Self::from_u128(rhs)); + } +} + +/// Construction, primitive `+=`, and `TryInto` for the signed type. +/// Includes unsigned constructors because the signed benches build `IBig` +/// values from `u64` / `u128` magnitudes (e.g. `1u128 << exp`). +pub trait SignedInt: BenchInt { + fn from_i64(v: i64) -> Self; + fn from_i128(v: i128) -> Self; + fn from_u64(v: u64) -> Self; + fn from_u128(v: u128) -> Self; + fn try_to_i128(&self) -> Option; + fn add_assign_i64(&mut self, rhs: i64) { + *self = self.add_ref(&Self::from_i64(rhs)); + } + fn add_assign_i128(&mut self, rhs: i128) { + *self = self.add_ref(&Self::from_i128(rhs)); + } +} + +/// A bignum implementation under test. `Dashu`, `Ibig`, `Num` and `Malachite` +/// are always available; `Rug` is added under the `gmp` feature. +pub trait Backend { + /// Tag used as the `BenchmarkId` group/function name so dashu and rug + /// measurements sit side-by-side in one criterion report. + const NAME: &'static str; + type Unsigned: UnsignedInt; + type Signed: SignedInt; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> Self::Unsigned; + fn sample_signed(class: ValueClass, rng: &mut R) -> Self::Signed; + + /// `|value|` as the unsigned type — dashu's `IBig::unsigned_abs() -> UBig`, + /// rug's `Integer::abs()`. Consumes `value` so no extra clone enters the + /// timed path (the sort-key pattern is `(a - b).magnitude()`). + fn magnitude(value: Self::Signed) -> Self::Unsigned; + + /// Reinterpret an unsigned magnitude as the signed type. dashu: + /// `IBig::from(UBig)`; rug: identity. + fn unsigned_to_signed(value: Self::Unsigned) -> Self::Signed; +} + +// ---- dashu impls ---------------------------------------------------------- + +impl BenchInt for UBig { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } +} + +impl UnsignedInt for UBig { + fn from_u64(v: u64) -> Self { + UBig::from(v) + } + fn from_u128(v: u128) -> Self { + UBig::from(v) + } + fn add_assign_u64(&mut self, rhs: u64) { + *self += rhs; + } + fn add_assign_u128(&mut self, rhs: u128) { + *self += rhs; + } +} + +impl BenchInt for IBig { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } +} + +impl SignedInt for IBig { + fn from_i64(v: i64) -> Self { + IBig::from(v) + } + fn from_i128(v: i128) -> Self { + IBig::from(v) + } + fn from_u64(v: u64) -> Self { + IBig::from(v) + } + fn from_u128(v: u128) -> Self { + IBig::from(v) + } + fn try_to_i128(&self) -> Option { + i128::try_from(self).ok() + } + fn add_assign_i64(&mut self, rhs: i64) { + *self += rhs; + } + fn add_assign_i128(&mut self, rhs: i128) { + *self += rhs; + } +} + +/// dashu backend: distinct `UBig` / `IBig` types. +pub struct Dashu; + +impl Backend for Dashu { + const NAME: &'static str = "dashu"; + type Unsigned = UBig; + type Signed = IBig; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> UBig { + sample_ubig(class, rng) + } + fn sample_signed(class: ValueClass, rng: &mut R) -> IBig { + sample_ibig(class, rng) + } + fn magnitude(value: IBig) -> UBig { + value.unsigned_abs() + } + fn unsigned_to_signed(value: UBig) -> IBig { + IBig::from(value) + } +} + +// --------------------------------------------------------------------------- +// Extra surface used only by the `primitive` bit-width-sweep bench: gcd, pow, +// radix conversions, and modular arithmetic. Kept in dedicated traits so the +// core `BenchInt` / `Backend` used by the other suites stays small. +// +// The modular ops (`mod_mul` / `mod_pow`) are written so every backend does +// the same thing it would naturally do, with nothing amortised away: a plain +// multiply-then-reduce, and the library's native one-shot modpow. +// --------------------------------------------------------------------------- + +/// Operations the `primitive` bench needs beyond the common `BenchInt` set. +/// All on the unsigned type (the sweep is `UBig`-only). +/// +/// `pow_exp` / `gcd` / `gcd_ext_blackbox` have portable default +/// implementations (square-and-multiply, Euclid) so any library can take part +/// in the bench; backends with a faster native routine (GMP's gcd, etc.) +/// override them so that bench reflects the real thing. `to_radix_string` / +/// `from_radix` / `write_hex` are required — every candidate library has +/// radix conversion. +pub trait PrimitiveInt: UnsignedInt { + fn to_radix_string(&self, radix: u32) -> String; + fn from_radix(s: &str, radix: u32) -> Self; + fn write_hex(&self, out: &mut String); + + fn pow_exp(&self, exp: usize) -> Self { + // Square-and-multiply. + let mut result = Self::from_u64(1); + let mut base = self.clone(); + let mut e = exp; + while e > 0 { + if e & 1 == 1 { + result = result.mul_ref(&base); + } + e >>= 1; + if e > 0 { + base = base.mul_ref(&base); + } + } + result + } + + fn gcd(&self, rhs: &Self) -> Self { + // Euclid via div/mul/sub (no `rem` in `BenchInt`); on the unsigned type. + let zero = Self::from_u64(0); + let mut a = self.clone(); + let mut b = rhs.clone(); + while b != zero { + let q = a.div_ref(&b); + let r = a.sub_ref(&q.mul_ref(&b)); + a = b; + b = r; + } + a + } + + /// Compute the extended gcd and discard the result through `black_box`. + /// The cofactor types differ between backends, so only timing is compared. + /// Defaults to the plain `gcd` (libraries with a native extended gcd + /// override to measure the cofactor work too). + fn gcd_ext_blackbox(&self, rhs: &Self) { + core::hint::black_box(self.gcd(rhs)); + } +} + +/// Backend extension for the `primitive` bench: a bit-width sampler and modular +/// arithmetic. Separate from [`Backend`] so the other suites don't carry it. +/// +/// The modular ops take the modulus directly and are written the way each +/// library actually exposes them, with no setup amortised away: `mod_mul` is +/// the plain "multiply then reduce", and `mod_pow` calls the library's native +/// modular exponentiation (building any per-modulus context inside the call, +/// since that is part of a one-shot modpow's real cost). +pub trait PrimitiveBackend: Backend { + /// A non-negative value of approximately `bits` bits, magnitude-identical + /// across backends for a given RNG state. + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> Self::Unsigned; + + /// `(a * b) mod m`, multiply-then-reduce — the same shape for every backend. + fn mod_mul(a: &Self::Unsigned, b: &Self::Unsigned, m: &Self::Unsigned) -> Self::Unsigned; + + /// `a^exp mod m` via the library's native modular exponentiation. + fn mod_pow(a: &Self::Unsigned, exp: &Self::Unsigned, m: &Self::Unsigned) -> Self::Unsigned; +} + +impl PrimitiveInt for UBig { + fn pow_exp(&self, exp: usize) -> Self { + self.pow(exp) + } + fn gcd(&self, rhs: &Self) -> Self { + Gcd::gcd(self, rhs) + } + fn gcd_ext_blackbox(&self, rhs: &Self) { + core::hint::black_box(ExtendedGcd::gcd_ext(self, rhs)); + } + fn to_radix_string(&self, radix: u32) -> String { + self.in_radix(radix).to_string() + } + fn from_radix(s: &str, radix: u32) -> Self { + UBig::from_str_radix(s, radix).unwrap() + } + fn write_hex(&self, out: &mut String) { + use core::fmt::Write; + write!(out, "{:x}", self).unwrap(); + } +} + +impl PrimitiveBackend for Dashu { + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> UBig { + random_ubig(bits, rng) + } + fn mod_mul(a: &UBig, b: &UBig, m: &UBig) -> UBig { + a * b % m + } + fn mod_pow(a: &UBig, exp: &UBig, m: &UBig) -> UBig { + // dashu's modpow goes through a ConstDivisor; build it here so the + // one-shot cost (divisor setup included) is what gets measured, like + // the others' native modpow. + ConstDivisor::new(m.clone()) + .reduce(a.clone()) + .pow(exp) + .residue() + } +} + +// ---- rug impls ------------------------------------------------------------ + +#[cfg(feature = "gmp")] +pub use rug_backend::Rug; + +#[cfg(feature = "gmp")] +mod rug_backend { + use super::{ + random_ubig, sample_ibig, sample_ubig, ubig_to_rug, Backend, BenchInt, PrimitiveBackend, + PrimitiveInt, SignedInt, UnsignedInt, ValueClass, + }; + use dashu_int::ops::UnsignedAbs; + use dashu_int::IBig; + use rand_v08::Rng; + use rug::Integer; + + impl BenchInt for Integer { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + Integer::from(self + rhs) + } + fn sub_ref(&self, rhs: &Self) -> Self { + Integer::from(self - rhs) + } + fn mul_ref(&self, rhs: &Self) -> Self { + Integer::from(self * rhs) + } + fn div_ref(&self, rhs: &Self) -> Self { + Integer::from(self / rhs) + } + fn bitand_ref(&self, rhs: &Self) -> Self { + Integer::from(self & rhs) + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + Integer::from(self ^ rhs) + } + fn shl_ref(&self, bits: usize) -> Self { + Integer::from(self << bits as u32) + } + fn shr_ref(&self, bits: usize) -> Self { + Integer::from(self >> bits as u32) + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } + } + + impl UnsignedInt for Integer { + fn from_u64(v: u64) -> Self { + Integer::from(v) + } + fn from_u128(v: u128) -> Self { + Integer::from(v) + } + fn add_assign_u64(&mut self, rhs: u64) { + *self += rhs; + } + fn add_assign_u128(&mut self, rhs: u128) { + *self += rhs; + } + } + + impl SignedInt for Integer { + fn from_i64(v: i64) -> Self { + Integer::from(v) + } + fn from_i128(v: i128) -> Self { + Integer::from(v) + } + fn from_u64(v: u64) -> Self { + Integer::from(v) + } + fn from_u128(v: u128) -> Self { + Integer::from(v) + } + fn try_to_i128(&self) -> Option { + i128::try_from(self).ok() + } + fn add_assign_i64(&mut self, rhs: i64) { + *self += rhs; + } + fn add_assign_i128(&mut self, rhs: i128) { + *self += rhs; + } + } + + impl PrimitiveInt for Integer { + fn pow_exp(&self, exp: usize) -> Self { + Integer::from(rug::ops::Pow::pow(self, exp as u32)) + } + fn gcd(&self, rhs: &Self) -> Self { + Integer::from(self.gcd_ref(rhs)) + } + fn gcd_ext_blackbox(&self, rhs: &Self) { + core::hint::black_box(<(Integer, Integer)>::from(self.extended_gcd_ref(rhs))); + } + fn to_radix_string(&self, radix: u32) -> String { + self.to_string_radix(radix as i32) + } + fn from_radix(s: &str, radix: u32) -> Self { + Integer::from(Integer::parse_radix(s, radix as i32).unwrap()) + } + fn write_hex(&self, out: &mut String) { + use core::fmt::Write; + write!(out, "{:x}", self).unwrap(); + } + } + + /// rug backend: a single `Integer` serves as both unsigned and signed. + pub struct Rug; + + impl Backend for Rug { + const NAME: &'static str = "rug"; + type Unsigned = Integer; + type Signed = Integer; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> Integer { + ubig_to_rug(&sample_ubig(class, rng)) + } + fn sample_signed(class: ValueClass, rng: &mut R) -> Integer { + let i = sample_ibig(class, rng); + let neg = i < IBig::from(0i32); + let mag = ubig_to_rug(&i.unsigned_abs()); + if neg { + -mag + } else { + mag + } + } + fn magnitude(value: Integer) -> Integer { + value.abs() + } + fn unsigned_to_signed(value: Integer) -> Integer { + value + } + } + + impl PrimitiveBackend for Rug { + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> Integer { + // Go through the dashu sampler so magnitudes (and RNG draws) match + // the dashu side point-for-point. + ubig_to_rug(&random_ubig(bits, rng)) + } + fn mod_mul(a: &Integer, b: &Integer, m: &Integer) -> Integer { + Integer::from(a * b) % m + } + fn mod_pow(a: &Integer, exp: &Integer, m: &Integer) -> Integer { + Integer::from(a.pow_mod_ref(exp, m).unwrap()) + } + } +} + +// ---- ibig impls ----------------------------------------------------------- + +pub use ibig_backend::Ibig; + +mod ibig_backend { + use super::{ + ibig_sign_hex, sample_ibig, sample_ubig, ubig_hex, Backend, BenchInt, PrimitiveBackend, + PrimitiveInt, SignedInt, UnsignedInt, ValueClass, + }; + use core::fmt::Write as _; + use ibig::modular::ModuloRing; + use ibig::ops::UnsignedAbs; + use ibig::{IBig as I, UBig as U}; + use rand_v08::Rng; + + fn to_u(u: &super::UBig) -> U { + U::from_str_radix(&ubig_hex(u), 16).unwrap() + } + fn to_s(i: super::IBig) -> I { + let (neg, hex) = ibig_sign_hex(i); + let mag = I::from(U::from_str_radix(&hex, 16).unwrap()); + if neg { + -mag + } else { + mag + } + } + + impl BenchInt for U { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } + } + + impl UnsignedInt for U { + fn from_u64(v: u64) -> Self { + U::from(v) + } + fn from_u128(v: u128) -> Self { + U::from(v) + } + fn add_assign_u64(&mut self, rhs: u64) { + *self += rhs; + } + fn add_assign_u128(&mut self, rhs: u128) { + *self += rhs; + } + } + + impl BenchInt for I { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } + } + + impl SignedInt for I { + fn from_i64(v: i64) -> Self { + I::from(v) + } + fn from_i128(v: i128) -> Self { + I::from(v) + } + fn from_u64(v: u64) -> Self { + I::from(v) + } + fn from_u128(v: u128) -> Self { + I::from(v) + } + fn try_to_i128(&self) -> Option { + i128::try_from(self).ok() + } + fn add_assign_i64(&mut self, rhs: i64) { + *self += rhs; + } + fn add_assign_i128(&mut self, rhs: i128) { + *self += rhs; + } + } + + impl PrimitiveInt for U { + fn to_radix_string(&self, radix: u32) -> String { + self.in_radix(radix).to_string() + } + fn from_radix(s: &str, radix: u32) -> Self { + U::from_str_radix(s, radix).unwrap() + } + fn write_hex(&self, out: &mut String) { + write!(out, "{:x}", self).unwrap(); + } + fn pow_exp(&self, exp: usize) -> Self { + // inherent `UBig::pow` (shadows the trait method of the same area) + U::pow(self, exp) + } + fn gcd(&self, rhs: &Self) -> Self { + U::gcd(self, rhs) + } + fn gcd_ext_blackbox(&self, rhs: &Self) { + core::hint::black_box(self.extended_gcd(rhs)); + } + } + + /// ibig backend: pure-Rust `UBig` / `IBig`, dashu's ancestor. + pub struct Ibig; + + impl Backend for Ibig { + const NAME: &'static str = "ibig"; + type Unsigned = U; + type Signed = I; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> U { + to_u(&sample_ubig(class, rng)) + } + fn sample_signed(class: ValueClass, rng: &mut R) -> I { + to_s(sample_ibig(class, rng)) + } + fn magnitude(value: I) -> U { + value.unsigned_abs() + } + fn unsigned_to_signed(value: U) -> I { + I::from(value) + } + } + + // Plain `% m` modular arithmetic (ibig has a `modular` module, but the + // benches use the uniform reduce-each-step shape so every non-dashu backend + // is measured the same way). + impl PrimitiveBackend for Ibig { + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> U { + to_u(&super::random_ubig(bits, rng)) + } + fn mod_mul(a: &U, b: &U, m: &U) -> U { + a * b % m + } + fn mod_pow(a: &U, exp: &U, m: &U) -> U { + // ibig exposes modular arithmetic through a precomputed ModuloRing; + // build it here so the full one-shot cost is measured. + let ring = ModuloRing::new(m); + ring.from(a.clone()).pow(exp).residue() + } + } +} + +// ---- num-bigint impls ----------------------------------------------------- + +pub use num_backend::Num; + +mod num_backend { + use super::{ + ibig_sign_hex, sample_ibig, sample_ubig, ubig_hex, Backend, BenchInt, PrimitiveBackend, + PrimitiveInt, SignedInt, UnsignedInt, ValueClass, + }; + use core::fmt::Write as _; + use num_bigint::{BigInt, BigUint}; + use num_traits::{Num as _, ToPrimitive as _}; + use rand_v08::Rng; + + fn to_u(u: &super::UBig) -> BigUint { + BigUint::from_str_radix(&ubig_hex(u), 16).unwrap() + } + fn to_s(i: super::IBig) -> BigInt { + let (neg, hex) = ibig_sign_hex(i); + let mag = BigInt::from(BigUint::from_str_radix(&hex, 16).unwrap()); + if neg { + -mag + } else { + mag + } + } + + impl BenchInt for BigUint { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } + } + + impl UnsignedInt for BigUint { + fn from_u64(v: u64) -> Self { + BigUint::from(v) + } + fn from_u128(v: u128) -> Self { + BigUint::from(v) + } + } + + impl BenchInt for BigInt { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs; + } + fn sub_assign_ref(&mut self, rhs: &Self) { + *self -= rhs; + } + fn bitxor_assign_ref(&mut self, rhs: &Self) { + *self ^= rhs; + } + } + + impl SignedInt for BigInt { + fn from_i64(v: i64) -> Self { + BigInt::from(v) + } + fn from_i128(v: i128) -> Self { + BigInt::from(v) + } + fn from_u64(v: u64) -> Self { + BigInt::from(v) + } + fn from_u128(v: u128) -> Self { + BigInt::from(v) + } + fn try_to_i128(&self) -> Option { + self.to_i128() + } + } + + impl PrimitiveInt for BigUint { + fn to_radix_string(&self, radix: u32) -> String { + self.to_str_radix(radix) + } + fn from_radix(s: &str, radix: u32) -> Self { + BigUint::from_str_radix(s, radix).unwrap() + } + fn write_hex(&self, out: &mut String) { + write!(out, "{:x}", self).unwrap(); + } + fn pow_exp(&self, exp: usize) -> Self { + self.pow(exp as u32) + } + fn gcd(&self, rhs: &Self) -> Self { + num_integer::Integer::gcd(self, rhs) + } + fn gcd_ext_blackbox(&self, rhs: &Self) { + // BigUint has no signed cofactors; do the extended gcd on BigInt. + let a = BigInt::from(self.clone()); + let b = BigInt::from(rhs.clone()); + core::hint::black_box(num_integer::Integer::extended_gcd(&a, &b)); + } + } + + /// num-bigint backend: pure-Rust `BigUint` / `BigInt`. + pub struct Num; + + impl Backend for Num { + const NAME: &'static str = "num"; + type Unsigned = BigUint; + type Signed = BigInt; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> BigUint { + to_u(&sample_ubig(class, rng)) + } + fn sample_signed(class: ValueClass, rng: &mut R) -> BigInt { + to_s(sample_ibig(class, rng)) + } + fn magnitude(value: BigInt) -> BigUint { + value.into_parts().1 + } + fn unsigned_to_signed(value: BigUint) -> BigInt { + BigInt::from(value) + } + } + + impl PrimitiveBackend for Num { + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> BigUint { + to_u(&super::random_ubig(bits, rng)) + } + fn mod_mul(a: &BigUint, b: &BigUint, m: &BigUint) -> BigUint { + a * b % m + } + fn mod_pow(a: &BigUint, exp: &BigUint, m: &BigUint) -> BigUint { + a.modpow(exp, m) + } + } +} + +// ---- malachite impls ------------------------------------------------------ + +pub use malachite_backend::Malachite; + +mod malachite_backend { + use super::{ + ibig_sign_hex, sample_ibig, sample_ubig, ubig_hex, Backend, BenchInt, PrimitiveBackend, + PrimitiveInt, SignedInt, UnsignedInt, ValueClass, + }; + use malachite_base::num::arithmetic::traits::{ExtendedGcd, Gcd, ModPow, Pow, UnsignedAbs}; + use malachite_base::num::conversion::traits::{FromStringBase, ToStringBase}; + use malachite_nz::integer::Integer; + use malachite_nz::natural::Natural; + use rand_v08::Rng; + + fn to_u(u: &super::UBig) -> Natural { + Natural::from_string_base(16, &ubig_hex(u)).unwrap() + } + fn to_s(i: super::IBig) -> Integer { + let (neg, hex) = ibig_sign_hex(i); + let mag = Integer::from(Natural::from_string_base(16, &hex).unwrap()); + if neg { + -mag + } else { + mag + } + } + + impl BenchInt for Natural { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + } + + impl UnsignedInt for Natural { + fn from_u64(v: u64) -> Self { + Natural::from(v) + } + fn from_u128(v: u128) -> Self { + Natural::from(v) + } + } + + impl BenchInt for Integer { + fn parse(s: &str) -> Self { + s.parse().unwrap() + } + fn add_ref(&self, rhs: &Self) -> Self { + self + rhs + } + fn sub_ref(&self, rhs: &Self) -> Self { + self - rhs + } + fn mul_ref(&self, rhs: &Self) -> Self { + self * rhs + } + fn div_ref(&self, rhs: &Self) -> Self { + self / rhs + } + fn bitand_ref(&self, rhs: &Self) -> Self { + self & rhs + } + fn bitxor_ref(&self, rhs: &Self) -> Self { + self ^ rhs + } + fn shl_ref(&self, bits: usize) -> Self { + self << bits + } + fn shr_ref(&self, bits: usize) -> Self { + self >> bits + } + } + + impl SignedInt for Integer { + fn from_i64(v: i64) -> Self { + Integer::from(v) + } + fn from_i128(v: i128) -> Self { + Integer::from(v) + } + fn from_u64(v: u64) -> Self { + Integer::from(v) + } + fn from_u128(v: u128) -> Self { + Integer::from(v) + } + fn try_to_i128(&self) -> Option { + i128::try_from(self).ok() + } + } + + impl PrimitiveInt for Natural { + fn to_radix_string(&self, radix: u32) -> String { + self.to_string_base(radix as u8) + } + fn from_radix(s: &str, radix: u32) -> Self { + Natural::from_string_base(radix as u8, s).unwrap() + } + fn write_hex(&self, out: &mut String) { + out.push_str(&self.to_string_base(16)); + } + fn pow_exp(&self, exp: usize) -> Self { + Pow::pow(self, exp as u64) + } + fn gcd(&self, rhs: &Self) -> Self { + Gcd::gcd(self, rhs) + } + fn gcd_ext_blackbox(&self, rhs: &Self) { + core::hint::black_box(ExtendedGcd::extended_gcd(self, rhs)); + } + } + + /// malachite backend: pure-Rust `Natural` / `Integer`. + pub struct Malachite; + + impl Backend for Malachite { + const NAME: &'static str = "malachite"; + type Unsigned = Natural; + type Signed = Integer; + + fn sample_unsigned(class: ValueClass, rng: &mut R) -> Natural { + to_u(&sample_ubig(class, rng)) + } + fn sample_signed(class: ValueClass, rng: &mut R) -> Integer { + to_s(sample_ibig(class, rng)) + } + fn magnitude(value: Integer) -> Natural { + value.unsigned_abs() + } + fn unsigned_to_signed(value: Natural) -> Integer { + Integer::from(value) + } + } + + impl PrimitiveBackend for Malachite { + fn sample_unsigned_bits(bits: usize, rng: &mut R) -> Natural { + to_u(&super::random_ubig(bits, rng)) + } + fn mod_mul(a: &Natural, b: &Natural, m: &Natural) -> Natural { + a * b % m + } + fn mod_pow(a: &Natural, exp: &Natural, m: &Natural) -> Natural { + // malachite's `mod_pow` requires the base already reduced; the + // other backends reduce internally, so do the same here. + let base = a % m; + ModPow::mod_pow(&base, exp, m) + } + } +}