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 perf ou valgrind)

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:

  1. Adicione Criterion como dependência dev
  2. Crie benchmarks para funções críticas
  3. Use --save-baseline antes de mudanças significativas
  4. Configure CI para falhar em regressões >5%
  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