Benchmarks com Criterion
1. Introdução ao Benchmarking em Rust
Benchmarks em Rust vão muito além de testes unitários. Enquanto testes verificam correção, benchmarks medem desempenho — uma diferença crucial. Testes unitários passam ou falham; benchmarks geram números: nanossegundos, throughput, alocações. E para isso, o Criterion.rs é a ferramenta padrão na comunidade Rust.
Criterion oferece análise estatística robusta, detecção de regressões e saída visual (HTML) via cargo criterion. Para instalar:
cargo add criterion --dev
A estrutura básica de um benchmark difere de testes: cargo bench executa benchmarks, enquanto cargo test executa testes. Ambos coexistem pacificamente.
2. Configurando o Ambiente de Benchmark
Adicione Criterion como dependência de desenvolvimento no Cargo.toml:
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "benchmark"
harness = false
Crie o arquivo benches/benchmark.rs com o benchmark minimalista:
use criterion::{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("fibonacci 20", |b| b.iter(|| fibonacci(20)));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
Execute com cargo bench. A saída mostrará média, desvio padrão e estimativas de desempenho.
3. Escrevendo Benchmarks com Criterion
O coração do Criterion é o Bencher. A função bench_function aceita um closure que recebe &mut Bencher:
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkGroup, measurement::WallTime};
use std::collections::BinaryHeap;
fn sort_benchmarks(c: &mut Criterion) {
let mut group: BenchmarkGroup<WallTime> = c.benchmark_group("sorting");
let mut data: Vec<i32> = (0..1000).collect();
data.reverse();
group.bench_function("vec_sort", |b| {
b.iter(|| {
let mut v = data.clone();
v.sort();
black_box(v);
})
});
group.bench_function("binary_heap", |b| {
b.iter(|| {
let mut heap = BinaryHeap::from(data.clone());
let mut sorted = Vec::with_capacity(data.len());
while let Some(val) = heap.pop() {
sorted.push(val);
}
black_box(sorted);
})
});
group.finish();
}
criterion_group!(benches, sort_benchmarks);
criterion_main!(benches);
BenchmarkGroup permite agrupar benchmarks relacionados e gerar gráficos comparativos. O black_box evita que o compilador otimize o código não utilizado.
4. Medindo Diferentes Métricas
Criterion mede tempo de execução por padrão (nanossegundos a microssegundos), mas também suporta throughput:
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
fn throughput_benchmark(c: &mut Criterion) {
let data = vec![0u8; 1_000_000];
let mut group = c.benchmark_group("throughput");
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_function("process_data", |b| {
b.iter(|| {
let _ = black_box(data.iter().map(|&x| x.wrapping_add(1)).collect::<Vec<_>>());
})
});
group.finish();
}
criterion_group!(benches, throughput_benchmark);
criterion_main!(benches);
Para métricas avançadas (alocações, instruções), use cargo-criterion com perf no Linux:
cargo install cargo-criterion
cargo criterion --features=html_reports
5. Interpretando os Resultados do Criterion
A saída padrão do Criterion inclui:
sorting/vec_sort time: [12.345 µs 12.456 µs 12.567 µs]
change: [-0.5% +0.2% +1.0%] (p = 0.23 > 0.05)
No change detected.
- time: média, desvio padrão (intervalo de confiança)
- change: variação percentual em relação ao baseline
- p: significância estatística (p > 0.05 indica que a mudança não é significativa)
Para rastrear regressões ao longo do tempo:
cargo bench -- --save-baseline v1.0
# Após alterações no código:
cargo bench -- --load-baseline v1.0 --baseline v2.0
6. Técnicas Avançadas e Boas Práticas
Evitando Otimizações do Compilador
black_box é essencial, mas iter_batched oferece mais controle:
use criterion::{BenchmarkId, Criterion, black_box};
fn parametric_bench(c: &mut Criterion) {
let mut group = c.benchmark_group("search");
for size in [10, 100, 1000] {
group.bench_with_input(BenchmarkId::new("linear", size), &size, |b, &s| {
let data: Vec<u64> = (0..s).collect();
b.iter(|| black_box(data.iter().find(|&&x| x == s - 1)));
});
}
group.finish();
}
Benchmarks Paramétricos
Use BenchmarkId para criar benchmarks com diferentes tamanhos de entrada, gerando gráficos comparativos automaticamente.
Integração com CI/CD
Configure falha em caso de regressão no Cargo.toml:
[package.metadata.criterion]
fail_if_regression = true
Ou via linha de comando:
cargo bench -- --failure-threshold 5%
7. Comparando com Alternativas e Limitações
O cargo bench nativo (sem Criterion) oferece apenas medições básicas de tempo. Criterion adiciona:
- Análise estatística (média, desvio, outliers)
- Detecção automática de regressões
- Gráficos HTML interativos
- Suporte a throughput e métricas avançadas
Quando evitar benchmarks:
- Código não crítico para desempenho
- Dependências externas lentas (rede, I/O de disco)
- Benchmarks em máquina virtual (ruído elevado)
Limitações:
- Ruído de SO (context switches, throttling de CPU)
- Benchmarks em máquinas compartilhadas
- Criterion não mede consumo de energia ou uso de cache L1/L2 (para isso, use
perfouvalgrind)
8. Exemplo Completo e Conclusão
Vamos comparar três implementações de busca em string: ingênua (força bruta), KMP e regex:
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, black_box};
use regex::Regex;
fn naive_search(text: &str, pattern: &str) -> bool {
text.contains(pattern)
}
fn kmp_search(text: &str, pattern: &str) -> bool {
let n = text.len();
let m = pattern.len();
if m == 0 { return true; }
if n < m { return false; }
let text_bytes = text.as_bytes();
let pattern_bytes = pattern.as_bytes();
// Construir tabela de falha (LPS)
let mut lps = vec![0; m];
let mut len = 0;
let mut i = 1;
while i < m {
if pattern_bytes[i] == pattern_bytes[len] {
len += 1;
lps[i] = len;
i += 1;
} else if len != 0 {
len = lps[len - 1];
} else {
lps[i] = 0;
i += 1;
}
}
// Busca KMP
let mut j = 0;
for &ch in text_bytes {
while j > 0 && ch != pattern_bytes[j] {
j = lps[j - 1];
}
if ch == pattern_bytes[j] {
j += 1;
}
if j == m {
return true;
}
}
false
}
fn regex_search(text: &str, pattern: &str) -> bool {
let re = Regex::new(pattern).unwrap();
re.is_match(text)
}
fn benchmark_string_search(c: &mut Criterion) {
let text = "a".repeat(10_000) + "b" + &"a".repeat(10_000);
let pattern = "b";
let mut group = c.benchmark_group("string_search");
group.bench_with_input(BenchmarkId::new("naive", "b"), &pattern, |b, &p| {
b.iter(|| black_box(naive_search(&text, p)));
});
group.bench_with_input(BenchmarkId::new("kmp", "b"), &pattern, |b, &p| {
b.iter(|| black_box(kmp_search(&text, p)));
});
group.bench_with_input(BenchmarkId::new("regex", "b"), &pattern, |b, &p| {
b.iter(|| black_box(regex_search(&text, p)));
});
group.finish();
}
criterion_group!(benches, benchmark_string_search);
criterion_main!(benches);
Execute com cargo criterion para gerar relatórios HTML em target/criterion/. O gráfico mostrará claramente qual implementação é mais rápida para cada padrão de busca.
Checklist final para integrar benchmarks no fluxo de desenvolvimento:
- Adicione Criterion como dependência dev
- Crie benchmarks para funções críticas
- Use
--save-baselineantes de mudanças significativas - Configure CI para falhar em regressões >5%
- Revise os gráficos HTML após cada release
Benchmarks com Criterion transformam "achismo" sobre desempenho em dados concretos. Incorpore-os ao seu fluxo e tome decisões baseadas em evidências.
Referências
- Documentação oficial do Criterion.rs — Guia completo da API, macros e configurações avançadas
- Criterion.rs Book — Tutorial detalhado com exemplos práticos e explicações sobre análise estatística
- cargo-criterion: extensão para relatórios avançados — Ferramenta para gerar relatórios HTML, gráficos e integração com perf
- Rust Performance Book: Benchmarking — Capítulo sobre benchmarking no guia oficial de performance Rust
- Benchmarking Rust Code with Criterion — Artigo prático de Luca Palmieri com dicas de uso em projetos reais
- Rust Design Patterns: Benchmarking — Padrões de design para benchmarks eficientes em Rust