Criterion: Benchmarking Estatístico em Rust

Guia completo da crate criterion para benchmarking em Rust. Aprenda a configurar benchmarks, grupos de comparação, benchmarks parametrizados, análise estatística, baselines e integração com CI/CD.

A crate criterion é a ferramenta padrão da comunidade Rust para benchmarking estatístico. Diferente do benchmarking nativo (#[bench]), que está disponível apenas no nightly e fornece resultados rudimentares, o Criterion coleta amostras estatísticas robustas, detecta regressões de performance automaticamente, gera gráficos HTML detalhados e funciona no Rust stable.

Medir performance corretamente é surpreendentemente difícil. Ruído do sistema operacional, cache warming, branch prediction e otimizações do compilador podem distorcer resultados. O Criterion lida com tudo isso usando técnicas estatísticas — coleta centenas de amostras, calcula intervalos de confiança e compara contra baselines anteriores para detectar regressões reais.

Instalação

Adicione ao seu Cargo.toml:

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "meu_benchmark"
harness = false

O harness = false é essencial — ele desabilita o framework de testes padrão e permite que o Criterion controle a execução.

Crie o diretório e arquivo:

mkdir -p benches

Crie benches/meu_benchmark.rs:

use criterion::{criterion_group, criterion_main, Criterion};

fn benchmark_exemplo(c: &mut Criterion) {
    c.bench_function("soma_simples", |b| {
        b.iter(|| {
            let mut soma = 0u64;
            for i in 0..1000 {
                soma += i;
            }
            soma
        })
    });
}

criterion_group!(benches, benchmark_exemplo);
criterion_main!(benches);

Execute com:

cargo bench

Uso Básico

Benchmark de Uma Função

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci_recursivo(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        n => fibonacci_recursivo(n - 1) + fibonacci_recursivo(n - 2),
    }
}

fn fibonacci_iterativo(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    let mut a = 0u64;
    let mut b = 1u64;
    for _ in 2..=n {
        let temp = a + b;
        a = b;
        b = temp;
    }
    b
}

fn bench_fibonacci(c: &mut Criterion) {
    // black_box() impede que o compilador otimize a chamada
    c.bench_function("fibonacci_recursivo_20", |b| {
        b.iter(|| fibonacci_recursivo(black_box(20)))
    });

    c.bench_function("fibonacci_iterativo_20", |b| {
        b.iter(|| fibonacci_iterativo(black_box(20)))
    });
}

criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);

A Importância de black_box

black_box impede otimizações do compilador que distorceriam os resultados:

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_blackbox(c: &mut Criterion) {
    // RUIM: o compilador pode pré-calcular o resultado em compile-time
    c.bench_function("sem_blackbox", |b| {
        b.iter(|| {
            let x = 42;
            let y = x * x + x;
            y
        })
    });

    // BOM: black_box impede otimizações, medindo o código real
    c.bench_function("com_blackbox", |b| {
        b.iter(|| {
            let x = black_box(42);
            let y = x * x + x;
            black_box(y)
        })
    });
}

criterion_group!(benches, bench_blackbox);
criterion_main!(benches);

Benchmark com Setup

use criterion::{criterion_group, criterion_main, Criterion, BatchSize};

fn bench_com_setup(c: &mut Criterion) {
    // Setup que NÃO é medido
    c.bench_function("ordenar_vec_1000", |b| {
        b.iter_batched(
            || {
                // Setup: gera dados aleatórios (NÃO cronometrado)
                let mut vec: Vec<i32> = (0..1000).rev().collect();
                vec
            },
            |mut vec| {
                // Código medido
                vec.sort();
                vec
            },
            BatchSize::SmallInput,
        )
    });
}

criterion_group!(benches, bench_com_setup);
criterion_main!(benches);

Recursos Avançados

Grupos de Benchmark (Comparação)

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};

fn bubble_sort(arr: &mut [i32]) {
    let n = arr.len();
    for i in 0..n {
        for j in 0..n - 1 - i {
            if arr[j] > arr[j + 1] {
                arr.swap(j, j + 1);
            }
        }
    }
}

fn insertion_sort(arr: &mut [i32]) {
    for i in 1..arr.len() {
        let chave = arr[i];
        let mut j = i;
        while j > 0 && arr[j - 1] > chave {
            arr[j] = arr[j - 1];
            j -= 1;
        }
        arr[j] = chave;
    }
}

fn merge_sort(arr: &mut [i32]) {
    let len = arr.len();
    if len <= 1 {
        return;
    }
    let meio = len / 2;
    merge_sort(&mut arr[..meio]);
    merge_sort(&mut arr[meio..]);

    let mut temp = arr.to_vec();
    let (mut i, mut j, mut k) = (0, meio, 0);
    while i < meio && j < len {
        if arr[i] <= arr[j] {
            temp[k] = arr[i];
            i += 1;
        } else {
            temp[k] = arr[j];
            j += 1;
        }
        k += 1;
    }
    while i < meio {
        temp[k] = arr[i];
        i += 1;
        k += 1;
    }
    while j < len {
        temp[k] = arr[j];
        j += 1;
        k += 1;
    }
    arr.copy_from_slice(&temp);
}

fn bench_sorting(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("sorting");

    for tamanho in [100, 500, 1000, 5000].iter() {
        let dados: Vec<i32> = (0..*tamanho as i32).rev().collect();

        grupo.bench_with_input(
            BenchmarkId::new("bubble_sort", tamanho),
            tamanho,
            |b, &tamanho| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| {
                        bubble_sort(&mut v);
                        v
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("insertion_sort", tamanho),
            tamanho,
            |b, &tamanho| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| {
                        insertion_sort(&mut v);
                        v
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("merge_sort", tamanho),
            tamanho,
            |b, &tamanho| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| {
                        merge_sort(&mut v);
                        v
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("std_sort", tamanho),
            tamanho,
            |b, &tamanho| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| {
                        v.sort();
                        v
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );
    }

    grupo.finish();
}

criterion_group!(benches, bench_sorting);
criterion_main!(benches);

Benchmarks Parametrizados

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use std::collections::{BTreeMap, HashMap};

fn bench_maps(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("map_insert");

    for &tamanho in &[100, 1_000, 10_000] {
        grupo.bench_with_input(
            BenchmarkId::new("HashMap", tamanho),
            &tamanho,
            |b, &tamanho| {
                b.iter(|| {
                    let mut map = HashMap::new();
                    for i in 0..tamanho {
                        map.insert(i, i * 2);
                    }
                    map
                })
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("BTreeMap", tamanho),
            &tamanho,
            |b, &tamanho| {
                b.iter(|| {
                    let mut map = BTreeMap::new();
                    for i in 0..tamanho {
                        map.insert(i, i * 2);
                    }
                    map
                })
            },
        );
    }

    grupo.finish();
}

fn bench_busca(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("busca_string");

    let texto = "a".repeat(10_000) + "padrão" + &"b".repeat(10_000);

    grupo.bench_function("contains", |b| {
        b.iter(|| texto.contains("padrão"))
    });

    grupo.bench_function("find", |b| {
        b.iter(|| texto.find("padrão"))
    });

    // Com regex pré-compilado
    let re = regex::Regex::new("padrão").unwrap();
    grupo.bench_function("regex", |b| {
        b.iter(|| re.is_match(&texto))
    });

    grupo.finish();
}

criterion_group!(benches, bench_maps, bench_busca);
criterion_main!(benches);

Entendendo a Saída Estatística

Quando você executa cargo bench, o Criterion produz uma saída como:

fibonacci_iterativo_20  time:   [12.345 ns 12.456 ns 12.567 ns]
                        change: [-2.1234% -0.5678% +1.0123%] (p = 0.45 > 0.05)
                        No change in performance detected.

Interpretação:

CampoSignificado
[12.345 ns 12.456 ns 12.567 ns]Intervalo de confiança (95%): [limite inferior, estimativa central, limite superior]
change: [-2.12% -0.57% +1.01%]Mudança em relação ao baseline anterior
p = 0.45 > 0.05p-valor: alta = sem mudança significativa
No changeConclusão: performance estável

Possíveis conclusões:

  • No change in performance detected — estável
  • Performance has improved — ficou mais rápido
  • Performance has regressed — ficou mais lento

Configuração Avançada

use criterion::{criterion_group, criterion_main, Criterion, SamplingMode};
use std::time::Duration;

fn bench_configurado(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("configurado");

    // Tempo máximo de medição
    grupo.measurement_time(Duration::from_secs(10));

    // Número de amostras
    grupo.sample_size(200);

    // Tempo de warm-up
    grupo.warm_up_time(Duration::from_secs(3));

    // Nível de confiança (padrão: 0.95)
    grupo.confidence_level(0.99);

    // Nível de significância para detecção de regressão
    grupo.significance_level(0.01);

    // Modo de amostragem
    grupo.sampling_mode(SamplingMode::Auto);

    grupo.bench_function("operacao_pesada", |b| {
        b.iter(|| {
            let mut v: Vec<i32> = (0..10_000).rev().collect();
            v.sort();
            v
        })
    });

    grupo.finish();
}

// Para benchmarks muito rápidos (< 1ns), aumente as amostras
fn bench_nano(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("nano");
    grupo.sample_size(1000);

    grupo.bench_function("soma_inline", |b| {
        b.iter(|| criterion::black_box(1) + criterion::black_box(2))
    });

    grupo.finish();
}

criterion_group!(benches, bench_configurado, bench_nano);
criterion_main!(benches);

Comparando com Baselines

# Salvar baseline atual
cargo bench -- --save-baseline antes_otimizacao

# Fazer alterações no código...

# Comparar com baseline
cargo bench -- --baseline antes_otimizacao

# Resetar baselines
cargo bench -- --save-baseline main

Benchmarking com Inputs de Arquivo

use criterion::{criterion_group, criterion_main, Criterion};

fn bench_processamento_texto(c: &mut Criterion) {
    // Carregar dados fora do benchmark
    let texto = std::fs::read_to_string("benches/dados/texto_grande.txt")
        .unwrap_or_else(|_| "palavra ".repeat(100_000));

    c.bench_function("contar_palavras", |b| {
        b.iter(|| {
            texto.split_whitespace().count()
        })
    });

    c.bench_function("contar_linhas", |b| {
        b.iter(|| {
            texto.lines().count()
        })
    });

    c.bench_function("buscar_padrao", |b| {
        b.iter(|| {
            texto.matches("palavra").count()
        })
    });
}

criterion_group!(benches, bench_processamento_texto);
criterion_main!(benches);

Integração com CI/CD

# .github/workflows/bench.yml
name: Benchmarks
on:
  pull_request:
    branches: [main]

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Restaurar cache de benchmarks
        uses: actions/cache@v3
        with:
          path: target/criterion
          key: criterion-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}

      - name: Executar benchmarks
        run: cargo bench -- --output-format bencher 2>&1 | tee benchmark_results.txt

      - name: Verificar regressões
        run: |
          if grep -q "regressed" benchmark_results.txt; then
            echo "Regressão de performance detectada!"
            grep "regressed" benchmark_results.txt
            exit 1
          fi

Script para detecção de regressão local:

#!/bin/bash
# scripts/check_perf.sh

echo "Salvando baseline..."
git stash
cargo bench -- --save-baseline baseline_main 2>/dev/null
git stash pop

echo "Executando benchmarks atuais..."
cargo bench -- --baseline baseline_main 2>&1 | tee /tmp/bench_result.txt

if grep -q "regressed" /tmp/bench_result.txt; then
    echo "AVISO: Regressões de performance detectadas!"
    grep -A2 "regressed" /tmp/bench_result.txt
    exit 1
else
    echo "Sem regressões detectadas."
fi

Boas Práticas

1. Sempre Use black_box

use criterion::black_box;

// RUIM: compilador pode otimizar tudo
fn bench_ruim(b: &mut criterion::Bencher) {
    b.iter(|| {
        let v: Vec<i32> = (0..1000).collect();
        v.len() // Resultado ignorado, compilador pode eliminar tudo
    });
}

// BOM: black_box previne eliminação
fn bench_bom(b: &mut criterion::Bencher) {
    b.iter(|| {
        let v: Vec<i32> = (0..1000).collect();
        black_box(v.len())
    });
}

2. Separe Setup de Medição

use criterion::{criterion_group, criterion_main, Criterion, BatchSize};

fn bench_separado(c: &mut Criterion) {
    // RUIM: setup incluído na medição
    c.bench_function("com_setup_misturado", |b| {
        b.iter(|| {
            let mut dados: Vec<i32> = (0..10_000).rev().collect(); // setup
            dados.sort(); // código medido
            dados
        })
    });

    // BOM: setup separado
    c.bench_function("setup_separado", |b| {
        b.iter_batched(
            || (0..10_000i32).rev().collect::<Vec<_>>(), // setup
            |mut dados| { dados.sort(); dados },          // medido
            BatchSize::LargeInput,
        )
    });
}

criterion_group!(benches, bench_separado);
criterion_main!(benches);

3. Use Grupos para Comparar Implementações

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};

fn bench_comparacao(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("string_concat");

    let palavras: Vec<String> = (0..1000).map(|i| format!("palavra{}", i)).collect();

    grupo.bench_function("String_push", |b| {
        b.iter(|| {
            let mut resultado = String::new();
            for p in &palavras {
                resultado.push_str(p);
                resultado.push(' ');
            }
            resultado
        })
    });

    grupo.bench_function("join", |b| {
        b.iter(|| palavras.join(" "))
    });

    grupo.bench_function("format_loop", |b| {
        b.iter(|| {
            let mut resultado = String::new();
            for p in &palavras {
                resultado = format!("{} {}", resultado, p);
            }
            resultado
        })
    });

    grupo.bench_function("with_capacity", |b| {
        let cap: usize = palavras.iter().map(|p| p.len() + 1).sum();
        b.iter(|| {
            let mut resultado = String::with_capacity(cap);
            for p in &palavras {
                resultado.push_str(p);
                resultado.push(' ');
            }
            resultado
        })
    });

    grupo.finish();
}

criterion_group!(benches, bench_comparacao);
criterion_main!(benches);

4. Execute Benchmarks em Ambiente Controlado

# Desabilitar Turbo Boost (Linux)
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo

# Fixar frequência do CPU
sudo cpupower frequency-set -g performance

# Executar com prioridade alta
sudo nice -n -20 cargo bench

# Fechar outros programas, usar terminal mínimo

5. Documente Seus Benchmarks

use criterion::{criterion_group, criterion_main, Criterion};

/// Benchmarks de busca em diferentes estruturas de dados.
///
/// Compara performance de busca linear vs binária vs HashMap
/// para diferentes tamanhos de entrada.
fn bench_busca_documentado(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("busca");
    // Limitar tempo para CI
    grupo.measurement_time(std::time::Duration::from_secs(5));

    // ... benchmarks ...

    grupo.finish();
}

criterion_group!(benches, bench_busca_documentado);
criterion_main!(benches);

Exemplos Práticos

Exemplo Completo: Benchmarking de Algoritmos de Ordenação

use criterion::{
    black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion,
};
use std::time::Duration;

// === Implementações ===

fn quick_sort(arr: &mut [i32]) {
    if arr.len() <= 1 {
        return;
    }
    let pivot = partition(arr);
    let (esquerda, direita) = arr.split_at_mut(pivot);
    quick_sort(esquerda);
    quick_sort(&mut direita[1..]);
}

fn partition(arr: &mut [i32]) -> usize {
    let len = arr.len();
    let pivot = arr[len - 1];
    let mut i = 0;

    for j in 0..len - 1 {
        if arr[j] <= pivot {
            arr.swap(i, j);
            i += 1;
        }
    }

    arr.swap(i, len - 1);
    i
}

fn heap_sort(arr: &mut [i32]) {
    let n = arr.len();

    for i in (0..n / 2).rev() {
        heapify(arr, n, i);
    }

    for i in (1..n).rev() {
        arr.swap(0, i);
        heapify(arr, i, 0);
    }
}

fn heapify(arr: &mut [i32], n: usize, i: usize) {
    let mut maior = i;
    let esquerda = 2 * i + 1;
    let direita = 2 * i + 2;

    if esquerda < n && arr[esquerda] > arr[maior] {
        maior = esquerda;
    }
    if direita < n && arr[direita] > arr[maior] {
        maior = direita;
    }

    if maior != i {
        arr.swap(i, maior);
        heapify(arr, n, maior);
    }
}

fn counting_sort(arr: &mut [i32]) {
    if arr.is_empty() {
        return;
    }

    let min = *arr.iter().min().unwrap();
    let max = *arr.iter().max().unwrap();
    let range = (max - min + 1) as usize;

    let mut contagem = vec![0usize; range];
    for &val in arr.iter() {
        contagem[(val - min) as usize] += 1;
    }

    let mut idx = 0;
    for i in 0..range {
        while contagem[i] > 0 {
            arr[idx] = i as i32 + min;
            idx += 1;
            contagem[i] -= 1;
        }
    }
}

// === Geradores de Dados ===

fn gerar_aleatorio(n: usize) -> Vec<i32> {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    (0..n as i32)
        .map(|i| {
            let mut hasher = DefaultHasher::new();
            i.hash(&mut hasher);
            (hasher.finish() % 1_000_000) as i32
        })
        .collect()
}

fn gerar_quase_ordenado(n: usize) -> Vec<i32> {
    let mut v: Vec<i32> = (0..n as i32).collect();
    // Trocar ~5% dos elementos
    for i in (0..n).step_by(20) {
        if i + 1 < n {
            v.swap(i, i + 1);
        }
    }
    v
}

fn gerar_reverso(n: usize) -> Vec<i32> {
    (0..n as i32).rev().collect()
}

// === Benchmarks ===

fn bench_algoritmos(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("sorting_algorithms");
    grupo.measurement_time(Duration::from_secs(5));
    grupo.sample_size(50);

    for &tamanho in &[1_000, 10_000] {
        let dados = gerar_aleatorio(tamanho);

        grupo.bench_with_input(
            BenchmarkId::new("quick_sort", tamanho),
            &dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { quick_sort(&mut v); v },
                    BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("heap_sort", tamanho),
            &dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { heap_sort(&mut v); v },
                    BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("counting_sort", tamanho),
            &dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { counting_sort(&mut v); v },
                    BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("std_sort", tamanho),
            &dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { v.sort(); v },
                    BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("std_sort_unstable", tamanho),
            &dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { v.sort_unstable(); v },
                    BatchSize::SmallInput,
                )
            },
        );
    }

    grupo.finish();
}

fn bench_distribuicoes(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("sort_by_distribution");
    grupo.measurement_time(Duration::from_secs(5));

    let tamanho = 10_000;

    let distribuicoes: Vec<(&str, Vec<i32>)> = vec![
        ("aleatório", gerar_aleatorio(tamanho)),
        ("quase_ordenado", gerar_quase_ordenado(tamanho)),
        ("reverso", gerar_reverso(tamanho)),
    ];

    for (nome, dados) in &distribuicoes {
        grupo.bench_with_input(
            BenchmarkId::new("std_sort", nome),
            dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { v.sort(); v },
                    BatchSize::SmallInput,
                )
            },
        );

        grupo.bench_with_input(
            BenchmarkId::new("quick_sort", nome),
            dados,
            |b, dados| {
                b.iter_batched(
                    || dados.clone(),
                    |mut v| { quick_sort(&mut v); v },
                    BatchSize::SmallInput,
                )
            },
        );
    }

    grupo.finish();
}

criterion_group!(benches, bench_algoritmos, bench_distribuicoes);
criterion_main!(benches);

Os relatórios HTML são gerados em target/criterion/ e podem ser abertos no navegador para visualização interativa.

Comparação com Alternativas

FerramentaTipoEstável?EstatísticasGráficos
criterionCrate externaSimCompletasHTML
#[bench]NativoNightlyBásicasNão
divanCrate externaSimBoasNão
iaiCrate externaSimBaseado em cachegrindNão
hyperfineCLIN/ABoasTerminal

Criterion e a escolha padrão para benchmarks de biblioteca/função. divan e uma alternativa mais recente com API ergonomica. iai usa contagem de instrucoes (deterministica, ideal para CI). hyperfine mede programas inteiros (CLI).

Conclusão

O Criterion transforma benchmarking de uma arte obscura em uma prática científica. Com coleta estatística robusta, detecção automática de regressões, relatórios HTML e integração com CI/CD, ele permite que você tome decisões de otimização baseadas em dados reais, não em intuição.

Lembre-se de sempre usar black_box, separar setup de medição, comparar implementações no mesmo grupo, executar em ambiente controlado e salvar baselines antes de otimizações.

Próximos passos:

  • Explore proptest para garantir que suas otimizações não introduzem bugs
  • Veja rayon para paralelizar código e medir o ganho com Criterion
  • Use cargo flamegraph para identificar gargalos antes de otimizar