Your first Rust benchmark

Compare two implementations in one interleaved native Suite group.

This example compares two ways to sort the same reversed integer input. Setup and returned-output destruction are excluded so the question is the cost of sorting an existing vector. Choose a timing boundary that matches your own use case before adapting it.

1. Add a benchmark target

Add the zenbench 0.1.11 dependency and a benchmark target:

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

[dev-dependencies]
zenbench = "0.1.11"

Add these entries to your existing Cargo.toml; keep your other dependencies and targets. Rust 1.85 or newer is required. See installation for the optional CLI and revision pinning.

2. Check correctness and register both arms

Create benches/my_bench.rs:

use zenbench::prelude::*;

fn bench_sort(suite: &mut Suite) {
    // An untimed correctness control. Extend it to your actual input domain.
    let input: Vec<i32> = (0..1000).rev().collect();
    let mut expected = input.clone();
    expected.sort();
    let mut actual = input;
    actual.sort_unstable();
    assert_eq!(expected, actual);

    suite.group("sort", |g| {
        g.throughput(Throughput::Elements(1000));
        g.bench("std_sort", |b| {
            b.with_input(|| (0..1000).rev().collect::<Vec<i32>>())
                .run(|mut v| { v.sort(); v })
        });
        g.bench("sort_unstable", |b| {
            b.with_input(|| (0..1000).rev().collect::<Vec<i32>>())
                .run(|mut v| { v.sort_unstable(); v })
        });
    });
}

zenbench::main!(bench_sort);

Each round runs both arms in shuffled order. Creating a fresh vector for every operation prevents the next operation from silently measuring already sorted data. Returning the vector moves its destruction outside the with_input timer. Any destruction you perform inside the measured closure stays included.

3. Inspect, then run

cargo bench --bench my_bench -- --help
cargo bench --bench my_bench -- --group=sort --dry-run
cargo bench --bench my_bench -- --group=sort

A dry run resolves registration, groups, placement, plan and storage without running benchmark callbacks. If placement is refused, follow CPU placement before measuring.

Default Iterate uses one process, up to 30 rounds and 100 ms warmup per arm unless your group overrides them. Budgets are not runtime predictions. A successful exit means the benchmark completed; it does not establish acceptance.

4. Read the result

The report includes mean latency, spread and comparison intervals. Lower latency is faster; a positive percent latency change means slower. Throughput is derived from the work count you declared. An interval crossing zero leaves direction unresolved; a narrow exploratory interval still needs a separate confirmation before acceptance. See example output.

You have completed this guide when the outputs agree, both arms appear in the sort group and the report represents your intended timing boundary. Next, explore an edit or freeze and confirm.

Choose the right timing API

NeedAPIIncluded work
Time a complete repeated operation`b.iter(
Exclude per-operation setup and returned-output destruction`b.with_input(
Exclude destruction of returned outputs`b.iter_deferred_drop(
Time work already managed by Rayon or Tokiog.bench(...)Wait for all submitted work to finish inside the callback
Create independent workers yourselfg.bench_parallel(...)Coordinator wall time through worker completion
Measure shared-state contentiong.bench_contended(...)Shared worker execution and synchronization

Deferred destruction changes live memory and cache behavior. Avoid using it as an automatic replacement for iter. Configure external pools yourself and keep the thread count fixed across arms. The API reference covers released signatures; the current source and measurement protocol cover the implementation described here.

Found an error or it needs a clarification? Open an issue on GitHub.
Substantiated corrections will be incorporated with attribution.