Your benchmark ran in 0 nanoseconds? Congratulations — the compiler optimised away the code you were trying to measure. std::hint::black_box prevents that by hiding values from the optimiser.
The problem: the optimiser is too smart
The Rust compiler aggressively eliminates dead code. If it can prove a result is never used, it simply removes the computation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| fn sum_range(n: u64) -> u64 {
(0..n).sum()
}
fn main() {
let start = std::time::Instant::now();
let result = sum_range(10_000_000);
let elapsed = start.elapsed();
// Without using `result`, the compiler may skip the entire computation
println!("took: {elapsed:?}");
// Force the result to be "used" so the above isn't optimised out
assert!(result > 0);
}
|
In release mode, the compiler can see through this and may still optimise the loop away — or even compute the answer at compile time. Your benchmark reports near-zero time, and you learn nothing.
Enter black_box
std::hint::black_box takes a value and returns it unchanged, but the compiler treats it as an opaque barrier — it can’t see through it, so it can’t optimise away whatever produced that value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| use std::hint::black_box;
fn sum_range(n: u64) -> u64 {
(0..n).sum()
}
fn main() {
let start = std::time::Instant::now();
let result = sum_range(black_box(10_000_000));
let _ = black_box(result);
let elapsed = start.elapsed();
println!("sum = {result}, took: {elapsed:?}");
}
|
Two black_box calls do the trick:
- Wrap the input — prevents the compiler from constant-folding the argument
- Wrap the output — prevents dead-code elimination of the computation
Before and after
Without black_box (release mode):
1
| sum = 49999995000000, took: 83ns ← suspiciously fast
|
With black_box (release mode):
1
| sum = 49999995000000, took: 5.612ms ← actual work
|
It works on any type
black_box is generic — it works on integers, strings, structs, references, whatever:
1
2
3
4
5
6
7
8
9
10
| use std::hint::black_box;
fn main() {
// Hide a vector from the optimiser
let data: Vec<i32> = black_box(vec![1, 2, 3, 4, 5]);
let total: i32 = data.iter().sum();
let total = black_box(total);
assert_eq!(total, 15);
}
|
Micro-benchmark recipe
Here’s a minimal pattern for quick-and-dirty micro-benchmarks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| use std::hint::black_box;
use std::time::Instant;
fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn main() {
let iterations = 100;
let start = Instant::now();
for _ in 0..iterations {
black_box(fibonacci(black_box(30)));
}
let elapsed = start.elapsed();
println!("{iterations} runs in {elapsed:?} ({:?}/iter)", elapsed / iterations);
}
|
Without black_box, the compiler could hoist the pure function call out of the loop or eliminate it entirely. With it, each iteration does real work.
When to use it
Reach for black_box whenever you’re timing code and the results look suspiciously fast. It’s also the foundation that benchmarking frameworks like criterion and the built-in #[bench] use under the hood.
It’s not a full benchmarking harness — for serious measurement you still want warmup, statistics, and outlier detection. But when you need a quick sanity check, black_box + Instant gets the job done.
Available since Rust 1.66 on stable.