🎓 A1Learner
🔍

Rust Benchmarks

A benchmark is a stopwatch measurement of how long code takes. Real benchmarks run the code thousands of times and report an average.

Time code with a stopwatch

Instant works on stable Rust, with no crates at all. Save Instant::now(), run the work, then read .elapsed():

use std::time::Instant;

let start = Instant::now();
let total = sum_up_to(10_000_000);
let elapsed = start.elapsed();

Use the result, or lose the work

A benchmark must use its result. Print it. Return it. Or pass it to std::hint::black_box. The compiler deletes work whose result nobody uses. And deleted work measures as instant:

let total = std::hint::black_box(sum_up_to(10_000_000));
println!("sum = {total}");

The built-in bench is nightly only

Mark a benchmark with #[bench]: the function receives a &mut Bencher and runs the measured code inside b.iter(|| ...). But built-in benchmarks are unfinished, so they live behind a feature gate — and stable Rust refuses the gate itself. On nightly, cargo bench runs every benchmark and prints an average for each, like 1,234 ns/iter.

#![feature(test)]

#[bench]
fn bench_sum(b: &mut Bencher) {
    b.iter(|| sum_up_to(10_000));
}

Benchmark with criterion instead

On stable Rust, the criterion crate does this job. It runs the code many times. It averages the results. It reports when a change made things slower. Divan is a newer library. It does the same job with a different design. Either one beats guessing.

fn bench_sum(c: &mut Criterion) {
    c.bench_function("sum 10k", |b| {
        b.iter(|| sum_up_to(10_000))
    });
}

Time a whole program

hyperfine runs a finished program many times from the shell. It reports the average and the spread. It is the right tool when startup-to-finish is what matters:

hyperfine ./my_app

Example

use std::time::Instant;
fn sum_up_to(n: u64) -> u64 {
    let mut total = 0;
    for i in 1..=n { total += i; }
    total
}
fn main() {
    let start = Instant::now();
    let total = sum_up_to(10_000_000);
    let elapsed = start.elapsed();
    println!("sum = {total}");
    println!("finished in under a second = {}", elapsed.as_secs() < 1);
}

Key points

  • Benchmark: a measurement of how long code takes, used to compare two ways of doing the same job
  • That is why real benchmarks run the code thousands of times and report an average.

In this example, sum_up_to adds the numbers from 1 to 10,000,000. main starts a stopwatch with Instant::now() before the work and reads .elapsed() after it. The printed sum, 50000005000000, is the real answer, so the loop cannot be optimised away. The last line turns the timing into a yes-or-no question — did it finish in under a second? On any ordinary computer the answer is true. A #[bench] benchmark or criterion would print the measured time itself instead. That number differs on every run. This is exactly why those tools average thousands of runs.

Note

One timing is never the truth: a busy moment or a cold cache changes it.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why does the built-in #[bench] attribute fail on stable Rust?