Skip to content
rusthacks.dev
Testing

Benchmarking in Rust With Criterion and Divan Compared

Criterion gives statistics and plots on stable Rust; Divan gives a lighter attribute-based API with allocation counting. Compare setup, output, features and pitfalls.

2 min read
Summarize in AI
Flat illustration for the post “Benchmarking in Rust With Criterion and Divan Compared”, composed around perf.

Measuring a Rust function takes more than a timer around a loop: the optimizer deletes unused work, the first run is cold, and one sample means nothing. Two crates handle the details. Everything compiles on Rust 1.90 with edition 2024, criterion 0.5 and divan 0.1. Timings are not shown, because they would only describe one machine.

The function

src/lib.rsrust
pub fn sum_even_squares(v: &[u64]) -> u64 {
    v.iter().filter(|x| **x % 2 == 0).map(|x| x * x).sum()
}

Criterion

Cargo.tomlTOML
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "sum"
harness = false
benches/sum.rsrust
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use demo::sum_even_squares;

fn bench(c: &mut Criterion) {
    let mut group = c.benchmark_group("sum_even_squares");
    for size in [100usize, 10_000, 1_000_000] {
        let data: Vec<u64> = (0..size as u64).collect();
        group.throughput(Throughput::Elements(size as u64));
        group.bench_with_input(BenchmarkId::from_parameter(size), &data, |b, data| {
            b.iter(|| sum_even_squares(black_box(data)))
        });
    }
    group.finish();
}

criterion_group!(benches, bench);
criterion_main!(benches);
Shell
cargo bench

Criterion warms up, takes many samples, reports a time interval and compares to the previous run: change: [-1.2% +0.3% +1.9%] with "No change in performance detected" or "Performance has regressed". HTML reports land in target/criterion/. Name a baseline with -- --save-baseline main and compare later with --baseline main.

Divan

Cargo.tomlTOML
[dev-dependencies]
divan = "0.1"

[[bench]]
name = "sum"
harness = false
benches/sum.rsrust
use demo::sum_even_squares;

fn main() {
    divan::main();
}

#[divan::bench(args = [100, 10_000, 1_000_000])]
fn sum(bencher: divan::Bencher, size: usize) {
    let data: Vec<u64> = (0..size as u64).collect();
    bencher.bench(|| sum_even_squares(divan::black_box(&data)));
}

The benchmark is a function with an attribute, and args runs it once per value. Divan prints a table with fastest, slowest, median and mean. Allocation counting is built in with a global allocator wrapper:

rust
#[global_allocator]
static ALLOC: divan::AllocProfiler = divan::AllocProfiler::system();

Then each benchmark also reports allocation counts and bytes, which Criterion does not.

Comparison

CriterionDivan
Setupmacros, groups, idsattribute on a function
Regression detectionbuilt in, saved baselinesnot built in
ReportsHTML plots, CSVterminal table
Allocationsnoyes, with AllocProfiler
Generics and parametersbench_with_inputargs, types
Maturitylong establishednewer

Pitfalls shared by both

  • **Dead code elimination:** use black_box on the input and let the closure return the result.
  • **Setup inside the timed loop:** build data outside b.iter, or use batched setup (iter_batched, bencher.with_inputs).
  • **Noise:** close other programs, pin CPU frequency if you can, and compare relative differences from runs on one machine.
  • **Unrealistic data:** a sorted array benchmarks differently from real data.
  • **CI:** shared runners vary too much for pass/fail thresholds; use them to record trends.

The traps are covered at length in benchmarking pitfalls and microbenchmarks that lie. For measuring a whole program, use hyperfine (benchmarking CLIs), and to see where time goes, a flamegraph.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.