Introdução: por que perfilar código Rust?
Rust é famoso por sua velocidade e segurança, mas mesmo o código mais rápido pode ficar lento devido a gargalos não óbvios. A criação de perfil é o processo de medir o tempo de execução, uso de memória e outros recursos, que ajuda a encontrar "gargalos". Sem a criação de perfil, a otimização se torna um palpite: você pode "melhorar" partes do código que não afetam o desempenho.
Neste artigo, abordaremos as principais ferramentas de criação de perfil para Rust: perf, flamegraph, cargo-flamegraph e o benchmarking integrado. Você aprenderá como encontrar funções lentas, alocações e cache misses, além de como interpretar os dados obtidos.
Ferramentas de criação de perfil Rust: escolhendo a abordagem correta
1. Benchmarking integrado com #[bench]
O primeiro passo para a criação de perfil é escrever benchmarks. O Rust possui suporte integrado para benchmarks (instável, requer nightly). Exemplo:
// No arquivo benches/my_bench.rs
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_my_function(b: &mut Bencher) {
let data = vec![1, 2, 3, 4, 5];
b.iter(|| {
// Função sendo testada
let _ = data.iter().sum::<i32>();
});
}
Execução: cargo bench. Você obterá medições precisas de tempo, mas isso não mostrará exatamente onde o tempo é gasto dentro da função.
2. perf — criador de perfil em nível de kernel
A ferramenta perf (Linux) coleta amostras de contadores da CPU. Ela mostra quais linhas de código são executadas com mais frequência.
Instalação: sudo apt install linux-tools-common linux-tools-generic.
Exemplo de uso:
# Compilar com símbolos de depuração
cargo build --release
# Executar a criação de perfil
sudo perf record ./target/release/my_program
# Visualizar o relatório
sudo perf report
No relatório, você verá as funções ordenadas pela porcentagem de tempo de execução. Este é o primeiro candidato para otimização.
3. flamegraph — visualização da pilha de chamadas
Flamegraph é um diagrama SVG onde a largura de cada retângulo corresponde ao tempo de execução da função. Quanto mais largo o retângulo, mais tempo é gasto nessa função ou em suas chamadas filhas.
Instalação do cargo-flamegraph:
cargo install flamegraph
Execução:
cargo flamegraph --bin my_program
O resultado é o arquivo flamegraph.svg. Abra-o no navegador. Procure por "platôs" largos — estes são os gargalos.
Exemplo prático: encontrando um gargalo no código Rust
Considere um programa simples que processa um grande array de dados:
fn process(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
for &byte in data {
result.push(byte.wrapping_mul(2));
}
result
}
fn main() {
let data = vec![0u8; 10_000_000];
let _ = process(&data);
}
Vamos compilar e executar a criação de perfil:
cargo build --release
cargo flamegraph --bin my_program
Ao abrir o flamegraph, vemos que a maior parte do tempo é gasta no loop for e na chamada wrapping_mul. Observe que Vec::push pode causar realocações se a capacidade não for calculada com precisão. No nosso caso, with_capacity está definido corretamente, mas o próprio loop é o gargalo.
Otimização: use iteradores e map:
fn process_optimized(data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| b.wrapping_mul(2)).collect()
}
Uma nova medição mostra uma aceleração de 10-15% devido à vetorização (autovetorização do LLVM).