Profiling: Medindo o Tempo de Execução
Profiling é o processo de medir onde seu código gasta tempo e recursos. Em Rust, a forma mais direta é usar std::time::Instant para medir trechos específicos. Isso permite identificar gargalos sem ferramentas externas, sendo especialmente útil durante desenvolvimento.
use std::time::Instant;
fn algoritmo_lento(n: usize) -> u64 {
let mut sum = 0u64;
for i in 0..n {
sum = sum.wrapping_add((i * i) as u64);
}
sum
}
fn main() {
let start = Instant::now();
let resultado = algoritmo_lento(1_000_000_000);
let duration = start.elapsed();
println!("Resultado: {}", resultado);
println!("Tempo: {:.2?}", duration);
}
Para profiling mais profundo, use perf no Linux. Compile seu projeto com informações de debug (cargo build --release preserva símbolos) e execute: perf record ./target/release/seu_binario. O comando perf report mostra exatamente quais funções consomem mais CPU. Essa abordagem revela gargalos que medições manuais podem perder.
Ferramentas Integradas do Rust
O Rust oferece cargo flamegraph para visualizar onde o tempo é gasto. Instale com cargo install flamegraph, depois execute cargo flamegraph --release. Isso gera um SVG interativo mostrando a pilha de chamadas com tempo proporcional à largura. Invaluável para entender comportamento em aplicações complexas.
Benchmarking: Comparando Abordagens
Benchmarking mede a performance relativa entre implementações. O Rust inclui um framework nativo (unstable) no libtest, mas a crate criterion é o padrão da indústria por ser mais robusta e estatisticamente confiável.
// Cargo.toml
[dev-dependencies]
criterion = "0.5"
// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn busca_linear(haystack: &[i32], needle: i32) -> bool {
haystack.iter().any(|&x| x == needle)
}
fn busca_binaria(haystack: &[i32], needle: i32) -> bool {
haystack.binary_search(&needle).is_ok()
}
fn benchmark_searches(c: &mut Criterion) {
let vec: Vec<i32> = (0..10000).collect();
let needle = 5000;
c.bench_function("linear_search", |b| {
b.iter(|| busca_linear(black_box(&vec), black_box(needle)))
});
c.bench_function("binary_search", |b| {
b.iter(|| busca_binaria(black_box(&vec), black_box(needle)))
});
}
criterion_group!(benches, benchmark_searches);
criterion_main!(benches);
Execute com cargo bench. A saída mostra tempo médio, desvio padrão e detecta regressões automaticamente. O black_box() previne otimizações indevidas do compilador que invalidariam o teste. Sempre use-o com valores de entrada.
Evitando Armadilhas Comuns
Benchmarks falsos ocorrem quando o compilador otimiza código demais. Se um benchmark mostra tempo zero, provavelmente o resultado é constante em tempo de compilação. Use black_box() para entradas e assert!() para resultados. Outra armadilha: medir alocação sem considerar reuso de memória. Benchmarks devem refletir uso real—se seu código em produção reutiliza buffers, seu benchmark deve fazer o mesmo.
Otimizações Práticas Baseadas em Dados
Depois de identificar gargalos via profiling, aplique otimizações. A regra de ouro: sempre meça antes e depois. Nunca otimize por intuição em Rust.
// Versão lenta: concatenação de strings em loop
fn versao_lenta(linhas: &[&str]) -> String {
let mut resultado = String::new();
for linha in linhas {
resultado = resultado + linha + "\n";
}
resultado
}
// Versão otimizada: pré-aloca capacidade
fn versao_otimizada(linhas: &[&str]) -> String {
let tamanho_total: usize = linhas.iter().map(|l| l.len() + 1).sum();
let mut resultado = String::with_capacity(tamanho_total);
for linha in linhas {
resultado.push_str(linha);
resultado.push('\n');
}
resultado
}
// Versão mais rápida: use collect
fn versao_ideal(linhas: &[&str]) -> String {
linhas.iter().map(|l| format!("{}\n", l)).collect()
}
O exemplo acima mostra como pré-alocar memória (with_capacity) evita realocações. Em Rust, alocações dinâmicas são caras. Benchmarks revelam que a versão ideal geralmente vence porque collect() otimiza internamente. Sempre considere iteradores funcionais—compilador as otimiza melhor que loops manuais.
Profiling de Memória
Memória é tão importante quanto CPU. Use valgrind ou heaptrack para detectar vazamentos. No Linux, execute: valgrind --tool=massif ./target/release/seu_binario. Para Rust especificamente, cargo valgrind simplifica o processo. Ferramentas revelam picos de alocação e padrões ineficientes de reuso.
// Ineficiente: cria Vec desnecessariamente
fn processar_ineficiente(dados: &[u8]) -> Vec<u8> {
let mut temp = Vec::new();
for &byte in dados {
temp.push(byte * 2);
}
temp
}
// Eficiente: usa iterador sem alocar
fn processar_eficiente(dados: &[u8]) -> impl Iterator<Item = u8> + '_ {
dados.iter().map(|&b| b * 2)
}
Quando a saída precisa de Vec, aloque uma única vez. Quando pode ser iterador, prefira isso—zero alocações extras. Esse padrão é fundamental em código crítico.
Conclusão
Três pilares aprendidos: Primeiro, profiling (medir com Instant, perf, flamegraph) identifica onde otimizar. Segundo, benchmarking com criterion compara objetivamente soluções, evitando otimizações inúteis. Terceiro, otimizações baseadas em dados concretos—pré-alocação, iteradores, evitar clones—multiplicam performance sem especulação. Combine essas técnicas sistematicamente: perfil → benchmark → otimize → re-benchmark. Rust permite escrever código rápido por design; essas ferramentas garantem que o seja.