Rust Performance Engineering

Benchmarking with Criterion - External Tooling

Criterion.rs (criterion crate) is the standard statistical microbenchmarking framework for stable Rust. Unlike a naive single-run timer loop that can be skewed by OS context switches, CPU turbo-boost warmup, or background processes, Criterion warms up the CPU and instruction cache, collects hundreds of multi-iteration samples, performs bootstrap statistical analysis ($p$-values and confidence intervals), and automatically compares results against the previous baseline run to detect performance regressions.

Criterion Execution Pipeline (`cargo bench`):

  1. Warmup Phase (default 3.0s)
     ---> Fills CPU L1/L2 caches & stabilizes CPU clock frequency
  2. Measurement Phase (default 100 samples over 5.0s)
     ---> Runs increasing iteration counts (N, 2N, 3N...) & fits linear regression
  3. Statistical Analysis & Baseline Comparison
     ---> Computes [lower, estimate, upper] 95% CI, outliers, & % change vs prior run
  4. Report Generation
     ---> Writes terminal summary + interactive SVG/HTML plots in `target/criterion/`

Setting Up Criterion in Cargo.toml and benches/

To configure Criterion on a package, add criterion under [dev-dependencies] and declare a [[bench]] target with harness = false so cargo bench runs Criterion's main function instead of the standard libtest runner:

# File: Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "hash_benchmarks"
harness = false

Parameterized Benchmarks (BenchmarkId) and Throughput Reporting

Inside benches/hash_benchmarks.rs, use BenchmarkGroup, Throughput::Bytes, and BenchmarkId to compare multiple algorithms across varying input sizes (64 B, 1 KiB, 64 KiB) and measure throughput in GiB/s:

// Structure of `benches/hash_benchmarks.rs` when using the `criterion` crate:
//
// use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
//
// fn bench_hashes(c: &mut Criterion) {
//     let mut group = c.benchmark_group("checksum_comparison");
//     for size in [64usize, 1024, 16384] {
//         let payload = vec![0x5Au8; size];
//         group.throughput(Throughput::Bytes(size as u64));
//
//         group.bench_with_input(BenchmarkId::new("fnv1a", size), &payload, |b, data| {
//             b.iter(|| fnv1a_hash(black_box(data)));
//         });
//
//         group.bench_with_input(BenchmarkId::new("adler32", size), &payload, |b, data| {
//             b.iter(|| adler32_hash(black_box(data)));
//         });
//     }
//     group.finish();
// }
// criterion_group!(benches, bench_hashes);
// criterion_main!(benches);

Excluding Setup and Drop Overhead (iter_batched)

When a benchmarked function mutates or consumes its input by value (for example, vec.sort_unstable() or drop(tree)), calling input.clone() directly inside b.iter(|| ...) measures the cost of clone() + drop() alongside the algorithm itself!

Criterion provides b.iter_batched(setup_fn, routine_fn, BatchSize::SmallInput) to run setup and teardown outside the timed measurement window. Below is a self-contained demonstration of linear-slope timing and throughput calculation matching Criterion's model:

use std::hint::black_box;
use std::time::Instant;

pub fn fnv1a_64(bytes: &[u8]) -> u64 {
    bytes.iter().fold(0xcbf29ce484222325u64, |acc, &b| {
        (acc ^ u64::from(b)).wrapping_mul(0x100000001b3)
    })
}

fn main() {
    let payload = vec![0x42u8; 4096];

    // 1. Warmup phase (stabilize instruction & data caches)
    for _ in 0..500 {
        black_box(fnv1a_64(black_box(&payload)));
    }

    // 2. Sampled measurement phase
    let iters = 2_000u64;
    let start = Instant::now();
    for _ in 0..iters {
        black_box(fnv1a_64(black_box(&payload)));
    }
    let elapsed = start.elapsed();

    let ns_per_iter = elapsed.as_nanos() as f64 / (iters as f64);
    let total_bytes = (payload.len() as u64) * iters;
    let gib_per_sec = (total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)) / elapsed.as_secs_f64();

    println!(
        "fnv1a_64 (4096B): {:.1} ns/iter ({:.2} GiB/s) > 0: {}",
        ns_per_iter,
        gib_per_sec,
        ns_per_iter > 0.0
    );
}
fnv1a_64 (4096B): 1850.4 ns/iter (2.06 GiB/s) > 0: true

Saving and Comparing Named Baselines

Before starting an optimization pass, save a named Criterion baseline so every subsequent edit can be compared directly against that reference point:

# 1. Record baseline before changing code:
cargo bench --bench hash_benchmarks -- --save-baseline main_branch

# 2. After optimizing code, compare against the saved baseline without overwriting it:
cargo bench --bench hash_benchmarks -- --baseline main_branch