Question

How to Benchmark Programs in Rust

rusttimebenchmarking

Question

Is it possible to benchmark programs in Rust? If so, how can you do it?

For example, how can you measure the execution time of a Rust program or a section of code in seconds?

A simple example might look like this:

use std::time::Instant;

fn main() {
    let start = Instant::now();

    // Code to measure
    let mut sum = 0;
    for i in 0..1_000_000 {
        sum += i;
    }

    let elapsed = start.elapsed();
    println!("sum = {sum}, elapsed = {:.6} seconds", elapsed.as_secs_f64());
}

Short Answer

By the end of this page, you will understand how to measure execution time in Rust, when to use simple timing versus proper benchmarking, and how developers benchmark Rust code in real projects. You will also learn common mistakes that make timing results unreliable.

Concept

Rust supports benchmarking in a few different ways, depending on what you want to measure.

The most basic approach is timing code manually with std::time::Instant. This is useful when you want to know how long a program or one specific block of code takes to run.

For example, you can:

  • record a start time with Instant::now()
  • run the code
  • ask for the elapsed time with start.elapsed()

This gives you a duration, which you can print in:

  • seconds
  • milliseconds
  • microseconds
  • nanoseconds

For more serious performance work, developers usually use benchmarking tools rather than one-off timing. A benchmark tool runs code many times, reduces noise, and helps compare different implementations more fairly.

In Rust, common options are:

  • std::time::Instant for simple timing
  • cargo bench for benchmark targets
  • the criterion crate for more reliable statistical benchmarking

This matters because raw execution time is often misleading. A single run can be affected by:

  • CPU scheduling
  • memory allocation
  • caching effects
  • debug vs release build differences
  • background processes on your machine

So the real idea is:

  • use Instant when you want a quick measurement
  • use a benchmark framework when you want trustworthy performance comparisons

Mental Model

Think of benchmarking like timing a runner.

If you use a stopwatch once, you get a rough result. That is like using Instant around your code.

If you want a fair comparison between two runners, you would:

  • make them run multiple times
  • use the same track
  • reduce outside distractions
  • average the results

That is what a real benchmark framework does.

So:

  • Instant = a stopwatch for quick checks
  • benchmarking framework = a proper timed test with repeated runs and better analysis
Take Quiz

Syntax and Examples

Measuring elapsed time with Instant

The simplest way to time Rust code is with std::time::Instant.

use std::time::Instant;

fn main() {
    let start = Instant::now();

    let mut total = 0;
    for i in 0..5_000_000 {
        total += i;
    }

    let duration = start.elapsed();

    println!("total = {total}");
    println!("Elapsed: {:.6} seconds", duration.as_secs_f64());
}

What this does

  • Instant::now() stores the current moment
  • the loop runs
  • start.elapsed() returns a Duration
  • as_secs_f64() converts that duration into seconds as a decimal number

Measuring milliseconds

use std::time::Instant;

fn main() {
    let start = Instant::now();

    std::thread::sleep(std::time::Duration::from_millis(150));

    let elapsed = start.elapsed();
    println!("{} ms", elapsed.as_millis());
}

Timing a function

use std::time::Instant;

fn expensive_work() {
    let mut sum = 0u64;
    for i in 0..10_000_000 {
        sum += i;
    }
    println!("{sum}");
}

fn main() {
    let start = Instant::now();
    expensive_work();
    println!("Took {:.6} seconds", start.elapsed().as_secs_f64());
}

Proper benchmarking with Criterion

If you want to compare implementations, criterion is a better choice.

Add it to Cargo.toml:

[dev-dependencies]
criterion = "0.5"

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

Create `benches/my_benchmark.rs`:

```rust
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn sum_loop(n: u64) -> u64 {
    let mut sum = 0;
    for i in 0..n {
        sum += i;
    }
    sum
}

fn benchmark_sum(c: &mut Criterion) {
    c.bench_function("sum_loop", |b| {
        b.iter(|| sum_loop(black_box(1_000_000)))
    });
}

criterion_group!(benches, benchmark_sum);
criterion_main!(benches);

Run:

cargo bench

Why black_box is used

It helps prevent the compiler from optimizing away the work, which would make the benchmark meaningless.

Step by Step Execution

Consider this example:

use std::time::Instant;

fn main() {
    let start = Instant::now();

    let mut value = 0;
    for i in 1..=3 {
        value += i;
    }

    let elapsed = start.elapsed();

    println!("value = {value}");
    println!("elapsed = {:.6} seconds", elapsed.as_secs_f64());
}

Here is what happens step by step:

  1. let start = Instant::now();

    • Rust stores the current instant in time.
  2. let mut value = 0;

    • A mutable variable is created.
  3. for i in 1..=3

    • The loop runs with i = 1, i = 2, and i = 3.
  4. value += i;

    • After i = 1, value becomes 1
    • After i = 2, value becomes 3
    • After i = 3, value becomes 6
  5. let elapsed = start.elapsed();

    • Rust calculates how much time has passed since start.
    • The result is stored as a Duration.
  6. elapsed.as_secs_f64()

    • The duration is converted into fractional seconds, such as 0.000012.

This pattern is the standard way to get a quick execution-time measurement in Rust.

Real World Use Cases

Benchmarking and timing are used in many practical Rust tasks.

Command-line tools

A CLI program may measure how long it takes to:

  • parse files
  • search text
  • compress data
  • process logs

Web services and APIs

Developers often measure:

  • request handling time
  • JSON serialization speed
  • database query processing time
  • middleware overhead

Data processing

Rust is often used for fast data pipelines. Timing helps compare:

  • parsing strategies
  • batch sizes
  • algorithms for sorting or grouping
  • memory-heavy transformations

Systems programming

In lower-level code, timing helps evaluate:

  • file I/O performance
  • network packet processing
  • caching strategies
  • lock contention and synchronization overhead

Algorithm comparison

Suppose you wrote two functions that produce the same result. Benchmarking helps answer:

  • which one is faster?
  • how much faster?
  • does the speed difference matter for realistic inputs?
Take Quiz

Real Codebase Usage

In real Rust projects, developers usually do not sprinkle Instant::now() everywhere permanently. Instead, they apply timing and benchmarking in structured ways.

Quick local profiling

During development, a programmer may temporarily wrap a slow section:

let start = std::time::Instant::now();
let result = process_records(&records);
println!("process_records took {} ms", start.elapsed().as_millis());

This is useful for quick investigation.

Guarding important code paths

Developers often measure only expensive operations, such as:

  • parsing
  • sorting
  • API response generation
  • large allocations

Comparing implementations

A team may write two versions of the same logic and benchmark both:

  • naive implementation
  • optimized implementation

Then they keep the version that is both correct and meaningfully faster.

Benchmark suites

In mature projects, benchmark files are added under benches/ so performance can be checked regularly.

This is especially common for:

  • libraries
  • parsers
  • crypto code
  • serialization code
  • numerical code

Avoiding misleading results

Real codebases often use patterns like:

  • running benchmarks in --release
  • using criterion
  • using black_box to reduce optimization artifacts
  • benchmarking small functions many times instead of relying on one run
  • separating benchmarking from application logging and setup work

Common Mistakes

1. Measuring in debug mode

A very common mistake is timing code with:

cargo run

Debug builds are much slower and are not suitable for performance conclusions.

Use:

cargo run --release

or for benchmarks:

cargo bench

2. Timing code only once

One run is noisy. The result may change because of background system activity.

Better approach

  • run the code multiple times
  • average results
  • use a benchmark framework for serious comparison

3. Benchmarking code that gets optimized away

Broken example:

use std::time::Instant;

fn main() {
    let start = Instant::now();

    let mut sum = 0;
    for i in 0..1_000_000 {
        sum += i;
    }

    let _ = start.elapsed();
}

The compiler may optimize aggressively if the result is never meaningfully used.

Better

use std::time::Instant;

fn main() {
    let start = Instant::now();

    let mut sum = 0;
    for i in 0..1_000_000 {
        sum += i;
    }

    println!("{sum}");
    println!("{} ms", start.elapsed().as_millis());
}

For true benchmarks, prefer black_box.

4. Measuring setup instead of the real work

If you include file loading, memory allocation, and printing in the same measurement, the result may not represent the part you care about.

Try to time the exact operation you want to study.

5. Comparing times that use different inputs

A benchmark is only useful if both versions do equivalent work on comparable input.

6. Using wall-clock output as if it were exact

Timing is approximate. Treat measurements as indicators, not absolute truth.

Look for consistent patterns, not tiny differences like a few nanoseconds in a noisy environment.

Comparisons

Common ways to measure performance in Rust

ApproachBest forProsCons
std::time::InstantQuick manual timingBuilt into Rust, simple to useNo statistics, easy to misuse
cargo benchOrganized benchmark targetsStandard Rust workflowBuilt-in benchmarking support has changed over time and is less convenient alone
criterion crateReliable performance comparisonsRepeated runs, statistical analysis, reportsRequires extra setup

Instant vs Criterion

FeatureInstantCriterion
Part of standard libraryYesNo
Good for timing whole programYesNot the main purpose
Good for comparing two functionsLimitedYes
Runs code repeatedlyNo, unless you do it manuallyYes
Helps reduce noisy resultsNoYes
Prevents optimization issues with helpers like black_boxNoYes

Seconds vs milliseconds vs microseconds

UnitWhen useful
SecondsLong-running tasks, full program timing
MillisecondsAPI calls, file processing, user-visible latency
Microseconds / nanosecondsVery small operations, low-level benchmarks

As a rule:

  • use seconds for overall program duration
  • use milliseconds for most practical app timings
  • use Criterion for very small code paths
Take Quiz

Cheat Sheet

Quick timing with Instant

use std::time::Instant;

let start = Instant::now();
// code here
let elapsed = start.elapsed();

Convert Duration

elapsed.as_secs_f64(); // fractional seconds
elapsed.as_secs();     // whole seconds
elapsed.as_millis();   // milliseconds
elapsed.as_micros();   // microseconds
elapsed.as_nanos();    // nanoseconds

Time a whole function

let start = Instant::now();
do_work();
println!("{} ms", start.elapsed().as_millis());

Important rules

  • Use cargo run --release for realistic speed
  • Use cargo bench or criterion for real benchmarks
  • Run multiple times before drawing conclusions
  • Make sure the compiler does not optimize away the work
  • Measure the specific section you care about

Criterion setup

Cargo.toml:

[dev-dependencies]
criterion = "0.5"

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

Benchmark file:

```rust
use criterion::{black_box, criterion_group, criterion_main, Criterion};

Run:

cargo bench

Edge cases

  • Debug builds give misleading timings
  • Printing to the console can affect results
  • Tiny benchmarks are noisy without repeated runs
  • One measurement is rarely enough

FAQ

How do I measure execution time in Rust?

Use std::time::Instant. Record the start time with Instant::now(), run your code, then call start.elapsed().

How do I print elapsed time in seconds in Rust?

Use elapsed.as_secs_f64() to get fractional seconds:

println!("{:.6}", elapsed.as_secs_f64());

Should I use cargo run or cargo run --release for timing?

Use cargo run --release. Debug builds are much slower and do not reflect real performance.

What is the difference between timing and benchmarking?

Timing usually means measuring one run. Benchmarking means running code repeatedly and analyzing performance more carefully.

Does Rust have built-in benchmarking?

Rust supports benchmark workflows, but many developers use the criterion crate because it is easier and more reliable for practical benchmarking.

Why are my benchmark results inconsistent?

Small timing differences can be affected by CPU load, caching, memory allocation, and background processes. Run multiple times and use a benchmark tool for better results.

Can I benchmark just part of a program?

Yes. Put Instant::now() before the specific section you want to measure and call elapsed() right after it.

Why is black_box used in benchmarks?

It helps stop the compiler from removing or simplifying the work being measured, which makes benchmark results more realistic.

Take Quiz

Mini Project

Description

Build a small Rust program that measures how long two different approaches take to compute the sum of numbers from 0 to n. This demonstrates basic timing with Instant and shows how to compare implementations in a practical way.

Goal

Create a Rust program that times two summing strategies and prints their results and elapsed times in milliseconds and seconds.

Requirements

  1. Write one function that sums numbers using a loop.
  2. Write another function that sums numbers using an iterator.
  3. Measure each function separately with std::time::Instant.
  4. Print both the computed sum and the execution time.
  5. Run the program in release mode to compare performance realistically.
Take Quiz

Keep learning

Related questions

View more