How does criterion::BenchmarkGroup::bench_function isolate benchmarks from external noise?
criterion::BenchmarkGroup::bench_function isolates benchmarks from external noise through statistical analysis, warm-up iterations, and measurement techniques that account for system variability. The benchmark framework runs multiple iterations, collects timing samples, applies statistical methods to detect outliers and estimate true performance, and reports confidence intervals rather than single-point measurements. This approach distinguishes genuine performance differences from random noise caused by CPU frequency scaling, cache state variations, OS scheduling, memory allocation patterns, and other system-level perturbations.
Basic Benchmark Setup
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| {
b.iter(|| fibonacci(black_box(20)));
});
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);bench_function accepts a closure that receives a Bencher for running iterations.
Warm-Up Phase
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn expensive_computation(n: usize) -> usize {
(0..n).sum()
}
fn bench_with_warmup(c: &mut Criterion) {
// Criterion automatically performs warm-up before measurement
// This ensures caches are hot and JIT compilation is complete
c.bench_function("sum 1000", |b| {
b.iter(|| expensive_computation(black_box(1000)));
});
}
criterion_group!(benches, bench_with_warmup);
criterion_main!(benches);Warm-up runs many iterations before measuring, filling caches and triggering JIT compilation.
Black Box for Preventing Optimization
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn compute(x: u64) -> u64 {
x * x + x
}
fn bench_without_black_box(c: &mut Criterion) {
// DANGEROUS: Compiler might optimize away the computation
c.bench_function("compute_naive", |b| {
b.iter(|| compute(42));
});
}
fn bench_with_black_box(c: &mut Criterion) {
// CORRECT: black_box prevents optimization
c.bench_function("compute_correct", |b| {
b.iter(|| compute(black_box(42)));
});
}
criterion_group!(benches, bench_without_black_box, bench_with_black_box);
criterion_main!(benches);black_box prevents the compiler from optimizing away computations by hiding values from the optimizer.
Statistical Measurement
use criterion::{criterion_group, criterion_main, Criterion};
fn vector_push(n: usize) -> Vec<u64> {
let mut v = Vec::with_capacity(n);
for i in 0..n {
v.push(i as u64);
}
v
}
fn bench_statistics(c: &mut Criterion) {
c.bench_function("vector_push", |b| {
b.iter(|| vector_push(100));
});
// Criterion automatically:
// 1. Runs multiple iterations
// 2. Collects timing samples
// 3. Calculates statistics
// 4. Reports confidence intervals
// 5. Detects outliers
}
criterion_group!(benches, bench_statistics);
criterion_main!(benches);Multiple samples are collected and analyzed statistically, not just timed once.
Iteration Count Selection
use criterion::{criterion_group, criterion_main, Criterion, BatchSize};
fn allocation_heavy(n: usize) -> Vec<u8> {
vec
![0u8; n]
}
fn bench_batch_size(c: &mut Criterion) {
// Criterion automatically determines iteration count
// based on how long each iteration takes
// For fast functions, many iterations per sample
// For slow functions, fewer iterations per sample
c.bench_function("allocate", |b| {
b.iter(|| allocation_heavy(1024));
});
// You can also specify batch size manually:
c.bench_function("allocate_batch", |b| {
b.iter_batched(
|| 1024, // Setup
|n| allocation_heavy(n), // Routine
BatchSize::SmallInput, // Batch size
);
});
}
criterion_group!(benches, bench_batch_size);
criterion_main!(benches);Iteration counts are automatically calibrated based on function duration.
Outlier Detection
use criterion::{criterion_group, criterion_main, Criterion};
fn noisy_operation() -> u64 {
// Simulate an operation with variable runtime
std::hint::black_box(42)
}
fn bench_outliers(c: &mut Criterion) {
c.bench_function("noisy_op", |b| {
b.iter(|| noisy_operation());
});
// Criterion output includes:
// - Outlier detection
// - Confidence intervals
// - Standard deviation
// - Median vs mean comparison
}
criterion_group!(benches, bench_outliers);
criterion_main!(benches);Outliers are identified and reported separately from main statistics.
Confidence Intervals
use criterion::{criterion_group, criterion_main, Criterion};
fn process_data(data: &[u8]) -> u64 {
data.iter().map(|&b| b as u64).sum()
}
fn bench_confidence(c: &mut Criterion) {
let data = vec
![0u8; 10000];
c.bench_function("process_data", |b| {
b.iter(|| process_data(&data));
});
// Criterion reports:
// - Mean time with confidence interval
// - Lower/upper bounds (95% confidence)
// - Helps distinguish real differences from noise
}
criterion_group!(benches, bench_confidence);
criterion_main!(benches);Confidence intervals show the range where true performance likely falls.
Linear Regression Analysis
use criterion::{criterion_group, criterion_main, Criterion};
fn linear_search(data: &[i32], target: i32) -> Option<usize> {
data.iter().position(|&x| x == target)
}
fn bench_regression(c: &mut Criterion) {
let data: Vec<i32> = (0..1000).collect();
c.bench_function("linear_search", |b| {
b.iter(|| {
let target = black_box(500);
linear_search(&data, target)
});
});
// Criterion uses linear regression to estimate
// per-iteration time from multiple samples
}
criterion_group!(benches, bench_regression);
criterion_main!(benches);Linear regression separates iteration time from measurement overhead.
Throughput Measurement
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
fn copy_bytes(src: &[u8], dst: &mut [u8]) {
dst.copy_from_slice(src);
}
fn bench_throughput(c: &mut Criterion) {
let size = 1024 * 1024; // 1MB
let src = vec
![0u8; size];
let mut dst = vec
![0u8; size];
c.bench_function("copy_1mb", |b| {
b.throughput(Throughput::Bytes(size as u64));
b.iter(|| copy_bytes(&src, &mut dst));
});
// Reports throughput (MB/s) instead of just time
// Useful for I/O benchmarks
}
criterion_group!(benches, bench_throughput);
criterion_main!(benches);Throughput converts timing results into bytes/elements per second.
Comparing Implementations
use criterion::{criterion_group, criterion_main, Criterion, BatchSize};
fn sum_iter(data: &[u64]) -> u64 {
data.iter().sum()
}
fn sum_loop(data: &[u64]) -> u64 {
let mut total = 0;
for &x in data {
total += x;
}
total
}
fn bench_comparison(c: &mut Criterion) {
let data: Vec<u64> = (0..10000).collect();
let mut group = c.benchmark_group("sum_methods");
group.bench_function("iter", |b| {
b.iter(|| sum_iter(black_box(&data)));
});
group.bench_function("loop", |b| {
b.iter(|| sum_loop(black_box(&data)));
});
group.finish();
// Comparison shows relative performance
// Criterion highlights significant differences
}
criterion_group!(benches, bench_comparison);
criterion_main!(benches);Benchmark groups enable direct comparison between implementations.
Setup and Teardown with iter_batched
use criterion::{criterion_group, criterion_main, Criterion, BatchSize};
fn sort_vector(v: &mut Vec<i32>) {
v.sort();
}
fn bench_batched(c: &mut Criterion) {
c.bench_function("sort", |b| {
b.iter_batched(
|| (0..1000).rev().collect::<Vec<i32>>(), // Setup: create new vector each time
|mut v| {
sort_vector(&mut v);
v
},
BatchSize::SmallInput,
);
});
// iter_batched separates setup from measured code
// Setup is not included in timing
}
criterion_group!(benches, bench_batched);
criterion_main!(benches);iter_batched runs setup before each measured iteration, excluding it from timing.
Comparing to Previous Runs
use criterion::{criterion_group, criterion_main, Criterion};
fn optimized_function(n: usize) -> usize {
(0..n).filter(|x| x % 2 == 0).sum()
}
fn bench_regression_detection(c: &mut Criterion) {
c.bench_function("optimized", |b| {
b.iter(|| optimized_function(black_box(1000)));
});
// Criterion saves baseline measurements
// Subsequent runs compare against baseline
// Detects performance regressions automatically
}
criterion_group!(benches, bench_regression_detection);
criterion_main!(benches);
// Run with: cargo bench -- --save-baseline main
// Compare with: cargo bench -- --baseline mainBaselines enable detecting performance regressions across code changes.
Sampling Strategy
use criterion::{criterion_group, criterion_main, Criterion};
fn sample_count_demo(c: &mut Criterion) {
// Criterion uses multiple samples per benchmark
// Each sample contains multiple iterations
// Samples are spread across the run
// Default: 100 samples
// Can be configured:
let mut group = c.benchmark_group("configured");
// Set custom sample size and warm-up time
group.sample_size(50) // Number of samples
.measurement_time(std::time::Duration::from_secs(5))
.warm_up_time(std::time::Duration::from_millis(100));
group.bench_function("custom_sampled", |b| {
b.iter(|| {
// Some computation
(0..1000).sum::<u64>()
});
});
group.finish();
}
criterion_group!(benches, sample_count_demo);
criterion_main!(benches);Sample count and timing can be configured for specific measurement needs.
Noise Sources and Mitigation
use criterion::{criterion_group, criterion_main, Criterion};
// Sources of noise that Criterion mitigates:
// 1. CPU frequency scaling (warm-up stabilizes)
// 2. Cache state (repeated iterations)
// 3. Memory allocator variability (batch size)
// 4. OS scheduling (statistical analysis)
// 5. Thermal throttling (confidence intervals)
fn memory_intensive(n: usize) -> Vec<u64> {
(0..n).collect()
}
fn bench_noise_handling(c: &mut Criterion) {
// Criterion handles noise through:
// - Warm-up iterations
// - Multiple samples
// - Statistical analysis
// - Outlier detection
c.bench_function("memory_ops", |b| {
b.iter(|| memory_intensive(black_box(10000)));
});
}
criterion_group!(benches, bench_noise_handling);
criterion_main!(benches);Multiple noise sources are handled through warm-up, sampling, and statistics.
Benchmarking Async Code
use criterion::{criterion_group, criterion_main, Criterion};
use tokio::runtime::Runtime;
async fn async_compute(n: u64) -> u64 {
let mut sum = 0;
for i in 0..n {
sum += i;
}
sum
}
fn bench_async(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("async_compute", |b| {
b.to_async(&rt).iter(|| async_compute(black_box(1000)));
});
// to_async handles async benchmarks
// Uses Tokio runtime for execution
}
criterion_group!(benches, bench_async);
criterion_main!(benches);to_async enables benchmarking async functions with a runtime.
Profiling Integration
use criterion::{criterion_group, criterion_main, Criterion, Profiler};
// Custom profiler can be integrated for detailed analysis
struct MyProfiler;
impl Profiler for MyProfiler {
fn start_profiling(&mut self, benchmark_id: &str, benchmark_path: &std::path::Path) {
// Start profiling tool
}
fn stop_profiling(&mut self, benchmark_id: &str, benchmark_path: &std::path::Path) {
// Stop profiling tool
}
}
fn bench_with_profiler(c: &mut Criterion) {
// Profiler integration available for advanced analysis
c.bench_function("profiled", |b| {
b.iter(|| (0..1000).sum::<u64>());
});
}
criterion_group!(benches, bench_with_profiler);
criterion_main!(benches);Profiler integration enables detailed analysis with external tools.
Input Generation
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn process_input(input: &[u8]) -> u64 {
input.iter().map(|&b| b as u64).sum()
}
fn bench_input_generation(c: &mut Criterion) {
// Input generation excluded from timing
c.bench_function("process_input", |b| {
b.iter_batched(
|| (0..1000).map(|i| i as u8).collect::<Vec<u8>>(),
|input| process_input(&input),
BatchSize::SmallInput,
);
});
// Alternative: pre-generate input once
let data: Vec<u8> = (0..1000).map(|i| i as u8).collect();
c.bench_function("process_input_cached", |b| {
b.iter(|| process_input(black_box(&data)));
});
}
criterion_group!(benches, bench_input_generation);
criterion_main!(benches);Pre-generate inputs or use iter_batched to exclude input generation from timing.
Parameterized Benchmarks
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn binary_search(data: &[u32], target: u32) -> Option<usize> {
data.binary_search(&target).ok()
}
fn bench_parameterized(c: &mut Criterion) {
let mut group = c.benchmark_group("binary_search");
let sizes = [100, 1000, 10000, 100000];
for size in sizes {
let data: Vec<u32> = (0..size).collect();
let target = size / 2;
group.bench_with_input(
BenchmarkId::new("size", size),
&(&data, target),
|b, &(data, target)| {
b.iter(|| binary_search(black_box(data), black_box(target)));
},
);
}
group.finish();
// Produces separate results for each parameter value
}
criterion_group!(benches, bench_parameterized);
criterion_main!(benches);BenchmarkId creates parameterized benchmarks for comparing across input sizes.
Synthesis
Noise isolation techniques:
| Technique | Purpose |
|---|---|
| Warm-up iterations | Fill caches, trigger JIT, stabilize CPU frequency |
| Multiple samples | Collect statistical distribution |
| Confidence intervals | Show true performance range |
| Outlier detection | Identify and exclude anomalies |
| Linear regression | Separate iteration time from overhead |
Benchmark phases:
| Phase | Activity |
|---|---|
| Warm-up | Many iterations, not timed |
| Measurement | Timed samples collected |
| Analysis | Statistics calculated |
| Reporting | Results formatted |
Key methods:
| Method | Use case |
|---|---|
bench_function |
Simple benchmarks |
bench_with_input |
Parameterized benchmarks |
iter |
Basic iteration timing |
iter_batched |
Setup/teardown separation |
to_async |
Async function benchmarking |
Output interpretation:
| Metric | Meaning |
|---|---|
| Mean | Average iteration time |
| Median | Middle value, robust to outliers |
| Std dev | Variability measure |
| Confidence interval | True value range (95%) |
| Outliers | Extreme values detected |
| Slope | Per-iteration time from regression |
Best practices:
// Prevent optimization
b.iter(|| compute(black_box(input)));
// Separate setup
b.iter_batched(setup, routine, BatchSize::SmallInput);
// Group related benchmarks
let mut group = c.benchmark_group("name");
group.bench_function("a", ...);
group.bench_function("b", ...);
group.finish();
// Report throughput for I/O
b.throughput(Throughput::Bytes(n));
b.iter(|| read_bytes());Key insight: bench_function provides statistically rigorous benchmarking through a multi-phase approach: warm-up iterations stabilize the execution environment (filling caches, triggering JIT compilation, allowing CPU frequency to settle), measurement collects multiple samples with multiple iterations each, and statistical analysis computes confidence intervals that distinguish genuine performance differences from noise. The framework uses linear regression on sample data to estimate per-iteration time, automatically calibrates iteration counts based on function duration (fast functions get more iterations, slow functions get fewer), and reports both point estimates and uncertainty ranges. black_box prevents compiler optimizations from eliminating benchmarked code, while iter_batched separates setup from measured code. The resulting confidence intervals and outlier detection help developers distinguish real performance changes from measurement noise, making it possible to identify even small (5-10%) performance differences reliably across runs.
