From b12ab8104bc56b82cd2a021f29720fcb122335f0 Mon Sep 17 00:00:00 2001 From: CodSpeed Bot Date: Sun, 30 Aug 2026 05:34:18 +0000 Subject: [PATCH] Add CodSpeed benchmarks and CI workflow --- .github/workflows/codspeed.yml | 44 +++++ Cargo.toml | 15 ++ README.md | 2 + benches/cmath.rs | 112 +++++++++++ benches/integer.rs | 71 +++++++ benches/math.rs | 327 +++++++++++++++++++++++++++++++++ 6 files changed, 571 insertions(+) create mode 100644 .github/workflows/codspeed.yml create mode 100644 benches/cmath.rs create mode 100644 benches/integer.rs create mode 100644 benches/math.rs diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..cf487f5 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,44 @@ +name: CodSpeed + +on: + push: + branches: [ "main" ] + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +env: + CARGO_TERM_COLOR: always + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Setup Rust toolchain, cache and cargo-codspeed binary + uses: moonrepo/setup-rust@v0 + with: + channel: stable + cache-target: release + bins: cargo-codspeed + + - name: Build the benchmark targets + run: cargo codspeed build --features num-bigint + + - name: Run the benchmarks + uses: CodSpeedHQ/action@v5 + with: + mode: simulation + run: cargo codspeed run diff --git a/Cargo.toml b/Cargo.toml index 8acc2ee..af7fa13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,20 @@ num-integer = { version = "0.1", optional = true } malachite-bigint = { version = "0", optional = true } # malachite upgrades minor version a lot [dev-dependencies] +divan = { version = "5.0.1", package = "codspeed-divan-compat" } proptest = "1.6.0" pyo3 = { version = "0.27", features = ["abi3", "auto-initialize"] } + +[[bench]] +name = "math" +harness = false + +[[bench]] +name = "cmath" +harness = false +required-features = ["complex"] + +[[bench]] +name = "integer" +harness = false +required-features = ["num-bigint"] diff --git a/README.md b/README.md index 675785e..3d73794 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ pymath ====== +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/RustPython/pymath?utm_source=badge) + **0 ULP (bit-exact) compatibility with CPython's math and cmath modules.** Every function produces identical results to Python at the binary representation level - not just "close enough", but exactly the same bits. diff --git a/benches/cmath.rs b/benches/cmath.rs new file mode 100644 index 0000000..eecac46 --- /dev/null +++ b/benches/cmath.rs @@ -0,0 +1,112 @@ +//! Benchmarks for the complex-valued `cmath` module. + +use divan::{Bencher, black_box}; +use num_complex::Complex64; + +fn main() { + divan::main(); +} + +/// Number of inputs evaluated per iteration. +const N: usize = 128; + +/// A spiral of complex values covering all four quadrants. +fn spiral(scale: f64) -> Vec { + (0..N) + .map(|i| { + let t = i as f64 / N as f64 * std::f64::consts::TAU; + let r = scale * (0.25 + i as f64 / N as f64); + Complex64::new(r * t.cos(), r * t.sin()) + }) + .collect() +} + +/// Generate one benchmark per `Complex64 -> Result` function. +macro_rules! bench_1 { + ($($name:ident($scale:expr)),* $(,)?) => { + $( + #[divan::bench] + fn $name(bencher: Bencher) { + let zs = spiral($scale); + bencher.bench(|| { + let mut acc = Complex64::new(0.0, 0.0); + for &z in &zs { + acc += pymath::cmath::$name(black_box(z)) + .unwrap_or(Complex64::new(0.0, 0.0)); + } + acc + }); + } + )* + }; +} + +bench_1! { + exp(2.0), + log10(10.0), + sqrt(10.0), + sin(2.0), + cos(2.0), + tan(2.0), + sinh(2.0), + cosh(2.0), + tanh(2.0), + asin(0.8), + acos(0.8), + atan(2.0), + asinh(2.0), + acosh(2.0), + atanh(0.8), +} + +#[divan::bench] +fn log_natural(bencher: Bencher) { + let zs = spiral(10.0); + bencher.bench(|| { + let mut acc = Complex64::new(0.0, 0.0); + for &z in &zs { + acc += pymath::cmath::log(black_box(z), None).unwrap_or(Complex64::new(0.0, 0.0)); + } + acc + }); +} + +#[divan::bench] +fn log_base(bencher: Bencher) { + let zs = spiral(10.0); + let base = Complex64::new(3.0, 0.0); + bencher.bench(|| { + let mut acc = Complex64::new(0.0, 0.0); + for &z in &zs { + acc += pymath::cmath::log(black_box(z), Some(black_box(base))) + .unwrap_or(Complex64::new(0.0, 0.0)); + } + acc + }); +} + +#[divan::bench] +fn abs_phase(bencher: Bencher) { + let zs = spiral(10.0); + bencher.bench(|| { + let mut acc = 0.0; + for &z in &zs { + let z = black_box(z); + acc += pymath::cmath::abs(z) + pymath::cmath::phase(z).unwrap_or_default(); + } + acc + }); +} + +#[divan::bench] +fn polar_rect(bencher: Bencher) { + let zs = spiral(10.0); + bencher.bench(|| { + let mut acc = Complex64::new(0.0, 0.0); + for &z in &zs { + let (r, phi) = pymath::cmath::polar(black_box(z)).unwrap_or((0.0, 0.0)); + acc += pymath::cmath::rect(r, phi).unwrap_or(Complex64::new(0.0, 0.0)); + } + acc + }); +} diff --git a/benches/integer.rs b/benches/integer.rs new file mode 100644 index 0000000..d3a5979 --- /dev/null +++ b/benches/integer.rs @@ -0,0 +1,71 @@ +//! Benchmarks for `math.integer`, the big-integer functions. +//! +//! Requires the `num-bigint` feature. + +use divan::{Bencher, black_box}; +use num_bigint::BigInt; +use pymath::math::integer; + +fn main() { + divan::main(); +} + +/// A deterministic big integer with roughly `bits` significant bits. +fn big(bits: u32) -> BigInt { + let mut n = BigInt::from(1u32); + let mut i = 0; + while i < bits { + n = (n << 13) + BigInt::from(6_700_417u32); + i += 13; + } + n +} + +const FACTORIAL_ARGS: [i64; 4] = [20, 200, 2_000, 20_000]; + +#[divan::bench(args = FACTORIAL_ARGS)] +fn factorial(bencher: Bencher, n: i64) { + bencher.bench(|| integer::factorial(black_box(n))); +} + +const COMB_ARGS: [(i64, i64); 3] = [(30, 15), (500, 250), (5_000, 2_500)]; + +#[divan::bench(args = COMB_ARGS)] +fn comb(bencher: Bencher, (n, k): (i64, i64)) { + bencher.bench(|| integer::comb(black_box(n), black_box(k))); +} + +#[divan::bench(args = COMB_ARGS)] +fn perm(bencher: Bencher, (n, k): (i64, i64)) { + bencher.bench(|| integer::perm(black_box(n), Some(black_box(k)))); +} + +const BITS: [u32; 3] = [64, 1_024, 8_192]; + +#[divan::bench(consts = BITS)] +fn isqrt(bencher: Bencher) { + let n = big(B); + bencher.bench(|| integer::isqrt(black_box(&n))); +} + +#[divan::bench(consts = BITS)] +fn gcd(bencher: Bencher) { + let a = big(B); + let b = big(B) * BigInt::from(7u32) + BigInt::from(1u32); + let args = [&a, &b]; + bencher.bench(|| integer::gcd(black_box(&args))); +} + +#[divan::bench(consts = BITS)] +fn lcm(bencher: Bencher) { + let a = big(B); + let b = big(B) * BigInt::from(7u32) + BigInt::from(1u32); + let args = [&a, &b]; + bencher.bench(|| integer::lcm(black_box(&args))); +} + +#[divan::bench(consts = BITS)] +fn log_bigint(bencher: Bencher) { + let n = big(B); + bencher.bench(|| pymath::math::log_bigint(black_box(&n), None)); +} diff --git a/benches/math.rs b/benches/math.rs new file mode 100644 index 0000000..f66dcff --- /dev/null +++ b/benches/math.rs @@ -0,0 +1,327 @@ +//! Benchmarks for the real-valued `math` module. +//! +//! Every benchmark evaluates the function over a fixed sweep of representative +//! finite inputs so that the measurement is dominated by the function itself +//! rather than by the harness. + +use divan::{Bencher, black_box}; + +fn main() { + divan::main(); +} + +/// Number of inputs evaluated per iteration. +const N: usize = 256; + +/// Evenly spaced values over `[start, end)`. +fn sweep(start: f64, end: f64) -> Vec { + let step = (end - start) / N as f64; + (0..N).map(|i| start + step * i as f64).collect() +} + +/// Generate one benchmark per `f64 -> Result` function. +macro_rules! bench_1 { + ($module:path; $($name:ident($start:expr, $end:expr)),* $(,)?) => { + $( + #[divan::bench] + fn $name(bencher: Bencher) { + use $module as m; + let xs = sweep($start, $end); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += m::$name(black_box(x)).unwrap_or_default(); + } + acc + }); + } + )* + }; +} + +mod trigonometric { + use super::*; + + bench_1! { + pymath::math; + sin(-10.0, 10.0), + cos(-10.0, 10.0), + tan(-1.5, 1.5), + asin(-1.0, 1.0), + acos(-1.0, 1.0), + atan(-10.0, 10.0), + sinh(-5.0, 5.0), + cosh(-5.0, 5.0), + tanh(-5.0, 5.0), + asinh(-10.0, 10.0), + acosh(1.0, 20.0), + atanh(-0.999, 0.999), + } + + #[divan::bench] + fn atan2(bencher: Bencher) { + let ys = sweep(-10.0, 10.0); + let xs = sweep(-7.0, 13.0); + bencher.bench(|| { + let mut acc = 0.0; + for (&y, &x) in ys.iter().zip(&xs) { + acc += pymath::math::atan2(black_box(y), black_box(x)).unwrap_or_default(); + } + acc + }); + } +} + +mod exponential { + use super::*; + + bench_1! { + pymath::math; + exp(-20.0, 20.0), + exp2(-20.0, 20.0), + expm1(-1.0, 1.0), + log10(1e-8, 1e8), + log2(1e-8, 1e8), + log1p(-0.5, 10.0), + sqrt(0.0, 1e6), + cbrt(-1e6, 1e6), + } + + #[divan::bench] + fn log_natural(bencher: Bencher) { + let xs = sweep(1e-8, 1e8); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += pymath::math::log(black_box(x), None).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn log_base(bencher: Bencher) { + let xs = sweep(1e-8, 1e8); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += pymath::math::log(black_box(x), Some(black_box(3.0))).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn pow(bencher: Bencher) { + let xs = sweep(0.1, 20.0); + let ys = sweep(-5.0, 5.0); + bencher.bench(|| { + let mut acc = 0.0; + for (&x, &y) in xs.iter().zip(&ys) { + acc += pymath::math::pow(black_box(x), black_box(y)).unwrap_or_default(); + } + acc + }); + } +} + +mod gamma { + use super::*; + + bench_1! { + pymath::math; + erf(-4.0, 4.0), + erfc(-4.0, 4.0), + gamma(0.5, 20.0), + lgamma(0.5, 200.0), + } + + /// `erf`/`erfc` switch algorithms around |x| = 1.5, so cover both branches. + #[divan::bench] + fn erf_tail(bencher: Bencher) { + let xs = sweep(1.5, 6.0); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += pymath::math::erf(black_box(x)).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn gamma_negative(bencher: Bencher) { + let xs = sweep(-19.5, -0.5); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += pymath::math::gamma(black_box(x)).unwrap_or_default(); + } + acc + }); + } +} + +mod misc { + use super::*; + + bench_1! { + pymath::math; + fabs(-1e6, 1e6), + } + + #[divan::bench] + fn floor_ceil_trunc(bencher: Bencher) { + let xs = sweep(-1e6, 1e6); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + let x = black_box(x); + acc += pymath::math::floor(x) + pymath::math::ceil(x) + pymath::math::trunc(x); + } + acc + }); + } + + #[divan::bench] + fn frexp_modf(bencher: Bencher) { + let xs = sweep(-1e6, 1e6); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + let x = black_box(x); + let (m, e) = pymath::math::frexp(x); + let (fract, int) = pymath::math::modf(x); + acc += m + e as f64 + fract + int; + } + acc + }); + } + + #[divan::bench] + fn fmod(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + let ys = sweep(0.5, 17.0); + bencher.bench(|| { + let mut acc = 0.0; + for (&x, &y) in xs.iter().zip(&ys) { + acc += pymath::math::fmod(black_box(x), black_box(y)).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn remainder(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + let ys = sweep(0.5, 17.0); + bencher.bench(|| { + let mut acc = 0.0; + for (&x, &y) in xs.iter().zip(&ys) { + acc += pymath::math::remainder(black_box(x), black_box(y)).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn fma(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + let ys = sweep(0.5, 17.0); + let zs = sweep(-5.0, 5.0); + bencher.bench(|| { + let mut acc = 0.0; + for ((&x, &y), &z) in xs.iter().zip(&ys).zip(&zs) { + acc += + pymath::math::fma(black_box(x), black_box(y), black_box(z)).unwrap_or_default(); + } + acc + }); + } + + #[divan::bench] + fn nextafter_steps(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + acc += pymath::math::nextafter(black_box(x), black_box(f64::INFINITY), Some(64)); + } + acc + }); + } + + #[divan::bench] + fn ldexp_ulp(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + bencher.bench(|| { + let mut acc = 0.0; + for &x in &xs { + let x = black_box(x); + acc += pymath::math::ldexp(x, 12).unwrap_or_default() + pymath::math::ulp(x); + } + acc + }); + } + + #[divan::bench] + fn isclose(bencher: Bencher) { + let xs = sweep(-1e3, 1e3); + bencher.bench(|| { + let mut count = 0usize; + for &x in &xs { + let x = black_box(x); + if pymath::math::isclose(x, x + 1e-9, None, None).unwrap_or(false) { + count += 1; + } + } + count + }); + } +} + +mod aggregate { + use super::*; + + /// Sizes exercising both the small and the large paths of the reductions. + const SIZES: [usize; 3] = [8, 128, 4096]; + + fn values(len: usize) -> Vec { + (0..len) + .map(|i| (i as f64 + 1.0) * 1.000_000_1_f64.powi(i as i32 % 64)) + .collect() + } + + #[divan::bench(consts = SIZES)] + fn fsum(bencher: Bencher) { + let xs = values(LEN); + bencher.bench(|| pymath::math::fsum(black_box(&xs).iter().copied())); + } + + #[divan::bench(consts = SIZES)] + fn dist(bencher: Bencher) { + let p = values(LEN); + let q: Vec = values(LEN).iter().map(|v| v * 0.5).collect(); + bencher.bench(|| pymath::math::dist(black_box(&p), black_box(&q))); + } + + #[divan::bench(consts = SIZES)] + fn sumprod(bencher: Bencher) { + let p = values(LEN); + let q: Vec = values(LEN).iter().map(|v| v * 0.5).collect(); + bencher.bench(|| pymath::math::sumprod(black_box(&p), black_box(&q))); + } + + #[divan::bench(consts = SIZES)] + fn sumprod_int(bencher: Bencher) { + let p: Vec = (0..LEN as i64).collect(); + let q: Vec = (0..LEN as i64).map(|i| i * 3 + 1).collect(); + bencher.bench(|| pymath::math::sumprod_int(black_box(&p), black_box(&q))); + } + + #[divan::bench(consts = SIZES)] + fn prod(bencher: Bencher) { + let xs: Vec = (0..LEN).map(|i| 1.0 + (i % 7) as f64 * 1e-3).collect(); + bencher.bench(|| pymath::math::prod(black_box(&xs).iter().copied(), None)); + } +}