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::Instantfor simple timingcargo benchfor benchmark targets- the
criterioncrate 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
Instantwhen 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
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 aDurationas_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:
-
let start = Instant::now();- Rust stores the current instant in time.
-
let mut value = 0;- A mutable variable is created.
-
for i in 1..=3- The loop runs with
i = 1,i = 2, andi = 3.
- The loop runs with
-
value += i;- After
i = 1,valuebecomes1 - After
i = 2,valuebecomes3 - After
i = 3,valuebecomes6
- After
-
let elapsed = start.elapsed();- Rust calculates how much time has passed since
start. - The result is stored as a
Duration.
- Rust calculates how much time has passed since
-
elapsed.as_secs_f64()- The duration is converted into fractional seconds, such as
0.000012.
- The duration is converted into fractional seconds, such as
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?
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_boxto 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
| Approach | Best for | Pros | Cons |
|---|---|---|---|
std::time::Instant | Quick manual timing | Built into Rust, simple to use | No statistics, easy to misuse |
cargo bench | Organized benchmark targets | Standard Rust workflow | Built-in benchmarking support has changed over time and is less convenient alone |
criterion crate | Reliable performance comparisons | Repeated runs, statistical analysis, reports | Requires extra setup |
Instant vs Criterion
| Feature | Instant | Criterion |
|---|---|---|
| Part of standard library | Yes | No |
| Good for timing whole program | Yes | Not the main purpose |
| Good for comparing two functions | Limited | Yes |
| Runs code repeatedly | No, unless you do it manually | Yes |
| Helps reduce noisy results | No | Yes |
Prevents optimization issues with helpers like black_box | No | Yes |
Seconds vs milliseconds vs microseconds
| Unit | When useful |
|---|---|
| Seconds | Long-running tasks, full program timing |
| Milliseconds | API calls, file processing, user-visible latency |
| Microseconds / nanoseconds | Very 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
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 --releasefor realistic speed - Use
cargo benchorcriterionfor 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.
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
- Write one function that sums numbers using a loop.
- Write another function that sums numbers using an iterator.
- Measure each function separately with
std::time::Instant. - Print both the computed sum and the execution time.
- Run the program in release mode to compare performance realistically.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.