Loading page…
Rust walkthroughs
Loading page…
criterion::BatchSize affect benchmarking setup and teardown measurements?criterion::BatchSize controls how Criterion groups benchmark iterations for measurement, directly affecting whether setup and teardown costs are included in timing. The key insight is that Criterion measures the time to process a batch of iterations, then divides by the batch size to get per-iteration time. With BatchSize::Small, each batch contains few iterations, so the overhead of calling the benchmarked function (including setup/teardown) is amortized over fewer iterations, potentially inflating measurements. With BatchSize::Large, more iterations per batch dilute per-call overhead, but may introduce cache effects. The BatchSize::NumBatches variant allows specifying how many times setup/teardown occur, giving control over how these costs factor into measurements.
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_basic(c: &mut Criterion) {
c.bench_function("addition", |bencher| {
bencher.iter(|| {
// This is measured
let x = black_box(1 + 1);
x
})
});
}
fn benchmark_with_setup(c: &mut Criterion) {
c.bench_function("with_setup", |bencher| {
bencher.iter(|| {
// Setup - THIS IS MEASURED!
let data: Vec<u64> = (0..1000).collect();
// The actual operation we want to measure
let sum: u64 = data.iter().sum();
black_box(sum)
})
});
}
criterion_group!(benches, benchmark_basic, benchmark_with_setup);
criterion_main!(benches);Without explicit handling, setup inside iter() is included in measurements.
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn problem_demonstration(c: &mut Criterion) {
// Benchmark 1: Setup included in measurement
c.bench_function("setup_included", |bencher| {
bencher.iter(|| {
// Setup creates 10,000 element vector EVERY iteration
let data: Vec<u64> = (0..10_000).collect();
// What we actually want to measure
let sum: u64 = data.iter().sum();
black_box(sum)
})
});
// The measured time includes:
// 1. Vector allocation
// 2. Iterator creation and collection
// 3. The sum operation
// But we only wanted to measure #3!
}
criterion_group!(benches, problem_demonstration);
criterion_main!(benches);Including setup distorts the measurement of the actual operation.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn iter_batched_basic(c: &mut Criterion) {
c.bench_function("proper_setup", |bencher| {
// iter_batched separates setup from measurement
bencher.iter_batched(
// Setup routine - NOT measured
|| {
let data: Vec<u64> = (0..10_000).collect();
data
},
// Measurement routine - this IS measured
|data| {
let sum: u64 = data.iter().sum();
black_box(sum)
},
// BatchSize determines iteration grouping
BatchSize::SmallInput,
)
});
}
criterion_group!(benches, iter_batched_basic);
criterion_main!(benches);iter_batched separates setup from the timed portion.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn batch_size_variants(c: &mut Criterion) {
// Small batches: setup/teardown overhead more visible
c.bench_function("small_batch", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::SmallInput,
)
});
// Large batches: overhead diluted across more iterations
c.bench_function("large_batch", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::LargeInput,
)
});
// PerIteration: setup runs for each iteration
c.bench_function("per_iteration", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::PerIteration,
)
});
// NumBatches: control number of batches explicitly
c.bench_function("num_batches", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::NumBatches(10), // 10 batches total
)
});
}
criterion_group!(benches, batch_size_variants);
criterion_main!(benches);Different BatchSize variants control how iterations are grouped.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn measurement_explanation(c: &mut Criterion) {
// Criterion's measurement model:
// 1. Run setup
// 2. Time the batch of iterations
// 3. Run teardown (via Drop)
// 4. Divide total time by batch size for per-iteration time
// With BatchSize::SmallInput:
// - Few iterations per batch
// - Setup/teardown runs more frequently
// - Per-call overhead is more visible in measurements
// - Better for operations with high variance
// With BatchSize::LargeInput:
// - Many iterations per batch
// - Setup/teardown runs less frequently
// - Per-call overhead is amortized
// - Better for very fast operations
let mut group = c.benchmark_group("batch_comparison");
group.bench_function("small", |bencher| {
bencher.iter_batched(
|| 0u64,
|x| black_box(x + 1),
BatchSize::SmallInput,
)
});
group.bench_function("large", |bencher| {
bencher.iter_batched(
|| 0u64,
|x| black_box(x + 1),
BatchSize::LargeInput,
)
});
group.finish();
}
criterion_group!(benches, measurement_explanation);
criterion_main!(benches);Batch size affects how setup/teardown overhead is amortized.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn visualize_batch_impact(c: &mut Criterion) {
// Simulate expensive setup
let mut group = c.benchmark_group("expensive_setup");
// Setup takes ~100µs, operation takes ~1µs
group.bench_function("small_batch_expensive_setup", |bencher| {
bencher.iter_batched(
|| {
// Expensive setup
std::thread::sleep(std::time::Duration::from_micros(100));
vec![1u64; 1000]
},
|data| {
// Fast operation
data.iter().sum::<u64>()
},
BatchSize::SmallInput, // Setup runs many times
)
});
group.bench_function("large_batch_expensive_setup", |bencher| {
bencher.iter_batched(
|| {
std::thread::sleep(std::time::Duration::from_micros(100));
vec![1u64; 1000]
},
|data| data.iter().sum::<u64>(),
BatchSize::LargeInput, // Setup runs fewer times
)
});
group.finish();
// Small batch: setup overhead is more visible
// Large batch: setup overhead is diluted across more iterations
// Both measure the operation correctly, but numbers differ
}
criterion_group!(benches, visualize_batch_impact);
criterion_main!(benches);Expensive setup amplifies the difference between batch sizes.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn per_iteration_vs_batched(c: &mut Criterion) {
let mut group = c.benchmark_group("per_iter_comparison");
// PerIteration: setup runs once per measured iteration
group.bench_function("per_iteration", |bencher| {
bencher.iter_batched(
|| {
// This runs for EVERY iteration
let mut data = vec![0u64; 100];
for i in 0..100 {
data[i] = i as u64;
}
data
},
|data| data.iter().sum::<u64>(),
BatchSize::PerIteration,
)
});
// SmallInput: setup runs once per small batch
group.bench_function("small_input", |bencher| {
bencher.iter_batched(
|| {
// Runs once per batch (e.g., every 10-100 iterations)
let mut data = vec![0u64; 100];
for i in 0..100 {
data[i] = i as u64;
}
data
},
|data| data.iter().sum::<u64>(),
BatchSize::SmallInput,
)
});
// Key difference:
// PerIteration: setup overhead is fully included in measurement
// SmallInput/LargeInput: setup overhead is amortized
group.finish();
}
criterion_group!(benches, per_iteration_vs_batched);
criterion_main!(benches);PerIteration includes setup in every measurement; batched modes amortize it.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn teardown_example(c: &mut Criterion) {
// iter_batched moves input to the routine
// Teardown (Drop) happens AFTER measurement
c.bench_function("with_teardown", |bencher| {
bencher.iter_batched(
|| {
// Setup: create expensive resource
vec![1u64; 1000]
},
|mut data| {
// Measurement: modify the data
data.push(42);
let sum: u64 = data.iter().sum();
black_box(sum)
// Teardown: data is dropped here (NOT measured)
},
BatchSize::SmallInput,
)
});
// If teardown should be measured, include it in the routine
c.bench_function("with_measured_teardown", |bencher| {
bencher.iter_batched(
|| {
vec![1u64; 1000]
},
|mut data| {
let sum: u64 = data.iter().sum();
black_box(sum);
// Explicitly measure teardown
drop(data); // This IS measured
},
BatchSize::SmallInput,
)
});
}
criterion_group!(benches, teardown_example);
criterion_main!(benches);Teardown (Drop) happens after measurement by default.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
use std::io::{Cursor, Read, Write};
fn io_benchmark(c: &mut Criterion) {
// Setup creates fresh state for each batch
c.bench_function("read_from_cursor", |bencher| {
bencher.iter_batched(
|| {
// Setup: create cursor with data
let data = vec![0u8; 1024];
Cursor::new(data)
},
|mut cursor| {
// Measurement: read from cursor
let mut buf = [0u8; 512];
cursor.read_exact(&mut buf).unwrap();
black_box(buf)
},
BatchSize::SmallInput,
)
});
// For file I/O, ensure each iteration gets fresh state
c.bench_function("write_to_cursor", |bencher| {
bencher.iter_batched(
|| {
// Setup: fresh cursor for writing
Cursor::new(Vec::with_capacity(1024))
},
|mut cursor| {
// Measurement: write to cursor
let data = [1u8; 512];
cursor.write_all(&data).unwrap();
black_box(cursor)
},
BatchSize::SmallInput,
)
});
}
criterion_group!(benches, io_benchmark);
criterion_main!(benches);I/O benchmarks benefit from fresh setup state per batch.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn num_batches_control(c: &mut Criterion) {
// NumBatches gives explicit control over batch count
// This affects how often setup/teardown run
let setup_count = std::sync::atomic::AtomicUsize::new(0);
c.bench_function("explicit_10_batches", |bencher| {
bencher.iter_batched(
|| {
// Setup will run exactly 10 times
setup_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
vec![1u64; 100]
},
|data| {
data.iter().sum::<u64>()
},
BatchSize::NumBatches(10), // Exactly 10 batches
)
});
// Useful when:
// 1. Setup is very expensive
// 2. You want consistent number of setup calls
// 3. Debugging measurement overhead
c.bench_function("explicit_100_batches", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::NumBatches(100), // Exactly 100 batches
)
});
}
criterion_group!(benches, num_batches_control);
criterion_main!(benches);NumBatches gives precise control over batch count.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn choosing_batch_size(c: &mut Criterion) {
let mut group = c.benchmark_group("batch_size_guide");
// Very fast operation (< 1µs): use LargeInput
// Minimizes measurement overhead
group.bench_function("fast_operation_large", |bencher| {
bencher.iter_batched(
|| 1u64,
|x| black_box(x + 1),
BatchSize::LargeInput,
)
});
// Medium operation (1µs - 1ms): use SmallInput
// Good balance of overhead and statistical accuracy
group.bench_function("medium_operation_small", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|data| data.iter().sum::<u64>(),
BatchSize::SmallInput,
)
});
// Slow operation (> 1ms): use PerIteration
// Setup overhead is negligible
group.bench_function("slow_operation_per_iter", |bencher| {
bencher.iter_batched(
|| {
// Some setup work
1000u64
},
|n| {
// Slow operation
std::thread::sleep(std::time::Duration::from_micros(n));
black_box(42)
},
BatchSize::PerIteration,
)
});
// Expensive setup: consider NumBatches
// Control how often setup runs
group.bench_function("expensive_setup_num_batches", |bencher| {
bencher.iter_batched(
|| {
// Expensive setup
(0..1000).collect::<Vec<_>>()
},
|data| data.iter().sum::<u64>(),
BatchSize::NumBatches(10),
)
});
group.finish();
}
criterion_group!(benches, choosing_batch_size);
criterion_main!(benches);Choose batch size based on operation speed and setup cost.
use criterion::{black_box, criterion_group, criterion_main, Criterion, BatchSize};
fn common_pitfalls(c: &mut Criterion) {
// Pitfall 1: Forgetting that setup isn't measured
c.bench_function("pitfall_unmeasured_setup", |bencher| {
bencher.iter_batched(
|| {
// This setup isn't in the measurement
// If it's part of the real workload, benchmark is misleading
let mut data = vec![0u64; 1_000_000];
data.fill(42);
data
},
|data| data.iter().sum::<u64>(),
BatchSize::SmallInput,
)
});
// Pitfall 2: Batch size too small for fast operations
c.bench_function("pitfall_small_batch_fast_op", |bencher| {
bencher.iter_batched(
|| 1u64,
|x| black_box(x + 1),
BatchSize::PerIteration, // Too much overhead for such fast op
)
});
// Correct: use iter() for trivial operations
c.bench_function("correct_fast_op", |bencher| {
bencher.iter(|| black_box(1u64 + 1))
});
// Pitfall 3: Reusing mutable state incorrectly
c.bench_function("pitfall_state_reuse", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100],
|mut data| {
// Mutating and relying on fresh state each time
data.clear(); // Wrong if you expect full vector!
data.iter().sum::<u64>()
},
BatchSize::SmallInput,
)
});
// Correct: setup provides fresh state for each batch
c.bench_function("correct_fresh_state", |bencher| {
bencher.iter_batched(
|| vec![1u64; 100], // Fresh vector each batch
|data| {
data.iter().sum::<u64>() // Don't mutate
},
BatchSize::SmallInput,
)
});
}
criterion_group!(benches, common_pitfalls);
criterion_main!(benches);Avoid common mistakes by matching batch size to operation characteristics.
| BatchSize | Setup Frequency | Best For |
|-----------|----------------|----------|
| PerIteration | Every iteration | Slow operations (> 1ms) |
| SmallInput | Every few iterations | Medium operations (1µs - 1ms) |
| LargeInput | Many iterations per batch | Fast operations (< 1µs) |
| NumBatches(n) | Exactly n times | Expensive setup, debugging |
BatchSize controls the trade-off between measurement accuracy and setup/teardown overhead:
How it works:
Choosing BatchSize:
LargeInput to minimize per-call overheadSmallInput for balanced measurementPerIteration since setup overhead is negligibleNumBatches to control setup frequencyKey insight: The batch size determines how setup/teardown costs are amortized across measurements. With PerIteration, setup costs are fully included in each measurement. With LargeInput, setup costs are spread across many iterations, reducing their impact on per-iteration measurements. Choose based on whether setup is part of what you want to measure or just infrastructure for the benchmark. For operations where setup should be excluded, iter_batched with appropriate BatchSize ensures clean measurements. For operations where setup should be included, use iter or PerIteration.