Property-Based Benchmarking with Criterion
Integration between Protest property-based testing and Criterion benchmarking framework.
Property-based benchmarking allows you to:
- 📊 Benchmark with diverse inputs - Use generators to create realistic test data
- 📈 Understand performance distribution - See how your code performs across the input space
- 🔍 Detect performance regressions - Leverage Criterion's statistical analysis
- ⚡ Find performance edge cases - Discover worst-case scenarios automatically
- 🎯 Reproducible benchmarks - Seed-based generation for consistent results
Add to your Cargo.toml:
[dev-dependencies]
protest = "0.1"
protest-criterion = "0.1"
criterion = "0.5"Create benches/my_benchmark.rs:
use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::IntGenerator;
fn bench_abs(c: &mut Criterion) {
c.bench_function_over_inputs(
"i32::abs",
|b, input: &i32| b.iter(|| input.abs()),
IntGenerator::new(-1000, 1000),
100, // number of samples
);
}
criterion_group!(benches, bench_abs);
criterion_main!(benches);Run with:
cargo benchUse bench_function_over_inputs to benchmark a function with diverse inputs:
use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::IntGenerator;
fn bench_multiplication(c: &mut Criterion) {
c.bench_function_over_inputs(
"i32 multiplication",
|b, input: &(i32, i32)| {
let (a, b_val) = *input;
b.iter(|| a * b_val)
},
protest::primitives::TupleGenerator::new((
IntGenerator::new(-1000, 1000),
IntGenerator::new(-1000, 1000),
)),
50,
);
}
criterion_group!(benches, bench_multiplication);
criterion_main!(benches);Use bench_property to benchmark property checks:
use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::VecGenerator;
use protest::Generator;
fn bench_reverse_property(c: &mut Criterion) {
c.bench_property(
"vec reverse is involutive",
VecGenerator::new(
protest::primitives::IntGenerator::new(0, 100),
0,
1000,
),
|v: &Vec<i32>| {
let mut reversed = v.clone();
reversed.reverse();
reversed.reverse();
assert_eq!(v, &reversed);
},
100,
);
}
criterion_group!(benches, bench_reverse_property);
criterion_main!(benches);Compare performance across different input sizes:
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
use protest_criterion::PropertyBenchmarkGroup;
use protest::primitives::{IntGenerator, VecGenerator};
use protest::Generator;
fn bench_sort_by_size(c: &mut Criterion) {
let mut group = c.benchmark_group("sort_by_size");
for size in [10, 100, 1000, 10000] {
let generator = VecGenerator::new(
IntGenerator::new(0, 1000),
size,
size,
);
group.bench_generated(
&size.to_string(),
generator,
|v: &Vec<i32>| {
let mut sorted = v.clone();
sorted.sort();
},
);
}
group.finish();
}
criterion_group!(benches, bench_sort_by_size);
criterion_main!(benches);Benchmark sorting with various input distributions:
use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::{IntGenerator, VecGenerator};
use protest::Generator;
fn bench_sorting(c: &mut Criterion) {
// Random data
c.bench_property(
"sort/random",
VecGenerator::new(IntGenerator::new(0, 10000), 1000, 1000),
|v: &Vec<i32>| {
let mut sorted = v.clone();
sorted.sort();
},
50,
);
// Nearly sorted data
c.bench_property(
"sort/nearly_sorted",
VecGenerator::new(IntGenerator::new(0, 100), 1000, 1000),
|v: &Vec<i32>| {
let mut sorted = v.clone();
sorted.sort();
},
50,
);
}
criterion_group!(benches, bench_sorting);
criterion_main!(benches);use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::StringGenerator;
fn bench_string_ops(c: &mut Criterion) {
c.bench_function_over_inputs(
"String::to_uppercase",
|b, input: &String| b.iter(|| input.to_uppercase()),
StringGenerator::new(10, 100),
50,
);
c.bench_function_over_inputs(
"String::contains",
|b, input: &String| b.iter(|| input.contains("test")),
StringGenerator::new(10, 1000),
50,
);
}
criterion_group!(benches, bench_string_ops);
criterion_main!(benches);use criterion::{criterion_group, criterion_main, Criterion};
use protest_criterion::PropertyBencher;
use protest::primitives::{IntGenerator, HashMapGenerator, StringGenerator};
use std::collections::HashMap;
fn bench_hashmap(c: &mut Criterion) {
c.bench_property(
"HashMap insert and lookup",
HashMapGenerator::new(
StringGenerator::new(5, 20),
IntGenerator::new(0, 1000),
10,
100,
),
|map: &HashMap<String, i32>| {
let mut m = map.clone();
m.insert("test_key".to_string(), 42);
let _ = m.get("test_key");
},
50,
);
}
criterion_group!(benches, bench_hashmap);
criterion_main!(benches);Extension trait for Criterion providing property-based benchmarking methods.
fn bench_function_over_inputs<I, G, F>(
&mut self,
name: &str,
bench_fn: F,
generator: G,
sample_count: usize,
) where
I: Clone + 'static,
G: Generator<I>,
F: FnMut(&mut criterion::Bencher, &I);Benchmarks a function with inputs generated by a property-based generator.
Parameters:
name- Name of the benchmarkbench_fn- Function to benchmark (receives bencher and input reference)generator- Generator for creating test inputssample_count- Number of different inputs to generate and benchmark
fn bench_property<I, G, P>(
&mut self,
name: &str,
generator: G,
property: P,
sample_count: usize,
) where
I: Clone + 'static,
G: Generator<I>,
P: Fn(&I) + 'static;Benchmarks a property test function.
Parameters:
name- Name of the benchmarkgenerator- Generator for creating test inputsproperty- Property function to benchmark (can include assertions)sample_count- Number of different inputs to generate and benchmark
Extension trait for BenchmarkGroup for more ergonomic grouped benchmarks.
fn bench_generated<I, G, F>(
&mut self,
id: &str,
generator: G,
f: F,
) where
I: Clone + 'static,
G: Generator<I>,
F: FnMut(&I) + 'static;Benchmarks with generated inputs within a benchmark group.
// For fast operations, use more samples
c.bench_function_over_inputs(
"fast_op",
|b, input: &i32| b.iter(|| input + 1),
IntGenerator::new(0, 100),
200, // More samples for stable statistics
);
// For slow operations, fewer samples
c.bench_function_over_inputs(
"slow_op",
|b, input: &Vec<i32>| b.iter(|| expensive_operation(input)),
VecGenerator::new(IntGenerator::new(0, 1000), 1000, 1000),
20, // Fewer samples to keep benchmark time reasonable
);// Good: Reflects real-world data
VecGenerator::new(IntGenerator::new(0, 1_000_000), 100, 10000)
// Bad: Unrealistic range
VecGenerator::new(IntGenerator::new(0, 10), 10, 10)let mut group = c.benchmark_group("scenarios");
// Best case
group.bench_generated("best_case", sorted_generator, |v| {
let mut result = v.clone();
result.sort();
});
// Worst case
group.bench_generated("worst_case", reverse_sorted_generator, |v| {
let mut result = v.clone();
result.sort();
});
// Average case
group.bench_generated("average_case", random_generator, |v| {
let mut result = v.clone();
result.sort();
});
group.finish();use rand::{SeedableRng, rngs::StdRng};
// For reproducible benchmarks
let mut rng = StdRng::seed_from_u64(42);protest-criterion works seamlessly with all Protest generators:
- Primitives:
IntGenerator,StringGenerator,BoolGenerator, etc. - Collections:
VecGenerator,HashMapGenerator,HashSetGenerator - Composite:
TupleGenerator,OptionGenerator - Custom: Any type implementing
Generator<T>
See protest documentation for available generators.
See the benches/ directory for complete examples:
example_benchmarks.rs- Basic usage
- Warm up: Criterion automatically warms up, but consider longer warm-up for complex operations
- Input caching: Pre-generate inputs if generation is expensive
- Batch sizes: Use
criterion::BatchSizeappropriately - Sample size: Balance statistical significance with benchmark duration
fn bench_traditional(c: &mut Criterion) {
c.bench_function("sort", |b| {
b.iter(|| {
let mut v = vec![5, 2, 8, 1, 9];
v.sort();
});
});
}Limitations:
- Single input case
- No coverage of edge cases
- Manual input creation
- No distribution analysis
fn bench_property_based(c: &mut Criterion) {
c.bench_property(
"sort",
VecGenerator::new(IntGenerator::new(0, 1000), 0, 1000),
|v: &Vec<i32>| {
let mut sorted = v.clone();
sorted.sort();
},
100, // Tests across 100 different inputs
);
}Benefits:
- Tests across input space
- Automatic edge case discovery
- Statistical distribution of performance
- Realistic input generation
Contributions are welcome! Please see the main Protest repository for contribution guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.
- protest - Property-based testing framework
- criterion - Statistics-driven micro-benchmarking
- protest-extras - Additional generators
- protest-stateful - Stateful property testing
Built on top of:
- Criterion.rs - Excellent benchmarking framework
- Protest - Property-based testing for Rust