Otimizações de performance e profiling
1. Introdução à performance em Rust
Rust oferece zero-cost abstractions: você paga apenas pelo que usa, sem overhead oculto. Isso significa que construções de alto nível como iteradores, closures e genéricos são compiladas para o mesmo código assembly que suas equivalentes manuais. O compilador elimina abstrações durante a otimização.
O mindset fundamental para performance é: meça antes de otimizar. Sem métricas, você pode gastar horas em micro-otimizações que não afetam o gargalo real.
A diferença entre compilação debug e release é crucial:
// Cargo.toml
[profile.release]
opt-level = 3 # Máximo de otimizações
cargo build --release # Ativa LTO, inlining, etc.
Flags essenciais no Cargo.toml:
[profile.release]
opt-level = 3
lto = "fat" # Link-Time Optimization
codegen-units = 1 # Máximo de inlining
panic = "abort" # Remove unwind tables
2. Ferramentas de profiling e benchmarking
Benchmark com Criterion
A crate criterion é o padrão para benchmarks precisos:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fibonacci 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
Flamegraphs com cargo flamegraph
cargo install flamegraph
cargo flamegraph --bin meu_programa
Isso gera um SVG interativo mostrando onde o tempo é gasto.
Profiling com perf (Linux)
perf record --call-graph dwarf ./target/release/meu_programa
perf report
Uso do valgrind para cache misses
valgrind --tool=cachegrind ./target/release/meu_programa
3. Otimizações no compilador LLVM
Flags de otimização
[profile.release]
opt-level = 3
lto = "fat" # Otimiza entre crates
codegen-units = 1 # Um único código objeto
panic = "abort" # Remove tratamento de pânico
target-cpu=native
Instrui o LLVM a usar todas as instruções da CPU atual:
[build]
rustflags = ["-C", "target-cpu=native"]
Isso habilita instruções SIMD (AVX2, AVX-512) automaticamente.
Inlining estratégico
#[inline(always)]
fn hot_function(x: u32) -> u32 {
x.wrapping_mul(42)
}
#[inline(never)]
fn cold_function(x: u32) -> u32 {
// Operação rara
x.checked_mul(42).unwrap_or(0)
}
Use #[inline(always)] apenas em funções pequenas chamadas em loops críticos.
4. Gerenciamento de memória e alocação
Evitando alocações
use std::borrow::Cow;
fn process_name(name: &str) -> Cow<'_, str> {
if name.contains(" ") {
// Aloca nova String
Cow::Owned(name.replace(" ", "_"))
} else {
// Reutiliza a referência
Cow::Borrowed(name)
}
}
// Reuso de buffer
fn parse_numbers(input: &str, buffer: &mut Vec<u32>) {
buffer.clear();
buffer.reserve(input.len() / 2); // Pré-aloca
for token in input.split_whitespace() {
buffer.push(token.parse().unwrap());
}
}
Alocadores customizados
[dependencies]
tikv-jemallocator = "0.5"
use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
fn main() {
// Agora usa jemalloc, melhor para concorrência
}
mimalloc é outra alternativa leve e rápida.
5. Otimizações em coleções e algoritmos
Escolha da estrutura de dados
use std::collections::{HashMap, BTreeMap};
// HashMap: O(1) médio, mas overhead de hash
// BTreeMap: O(log n), ordenado, melhor cache local
// Para poucos elementos, Vec<(K,V)> + binary_search pode ser mais rápido
Iteradores e lazy evaluation
let numbers: Vec<u32> = (0..1_000_000).collect();
// Ruim: aloca intermediariamente
let result: Vec<_> = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.collect();
// Bom: lazy, sem alocações extras
let sum: u32 = numbers
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.sum(); // Um único collect
Coleções compactas
[dependencies]
smallvec = "1.0"
arrayvec = "0.7"
use smallvec::SmallVec;
// Otimizado para até 4 elementos no stack
let mut vec: SmallVec<[u32; 4]> = SmallVec::new();
vec.push(1);
vec.push(2);
vec.push(3);
6. Concorrência e paralelismo
Rayon para paralelismo de dados
use rayon::prelude::*;
fn process_data(data: &[u32]) -> u32 {
data.par_iter() // Paralelo
.map(|x| x * 2)
.filter(|x| *x > 10)
.sum()
}
// Para streams não paralelizáveis
use rayon::iter::ParallelBridge;
fn process_stream(iter: impl Iterator<Item = u32>) -> u32 {
iter.par_bridge()
.map(|x| x * 2)
.sum()
}
False sharing e alinhamento
#[repr(align(64))] // Alinha a linha de cache (64 bytes)
struct AlignedCounter {
counter: u64,
}
// Evita false sharing entre threads
Crossbeam para canais sem bloqueio
use crossbeam::channel;
let (tx, rx) = channel::bounded(1000);
// Trabalhadores
for _ in 0..4 {
let rx = rx.clone();
std::thread::spawn(move || {
while let Ok(msg) = rx.recv() {
process(msg);
}
});
}
7. Medindo e mitigando gargalos comuns
Identificando hot loops
#[cfg_attr(feature = "profiling", inline(never))]
fn hot_loop(data: &[u32]) -> u32 {
let mut sum = 0;
for &x in data {
sum += x;
}
sum
}
// Com contadores manuais
fn profile_hot_loop(data: &[u32]) -> u32 {
let start = std::time::Instant::now();
let result = hot_loop(data);
println!("Hot loop: {:?}", start.elapsed());
result
}
Clippy com lints de performance
cargo clippy -- -W clippy::perf
Isso ativa lints como:
- clippy::large_enum_variant
- clippy::boxed_local
- clippy::vec_box
Uso seguro de unsafe
// Apenas quando o compilador não otimiza bem
unsafe {
let slice = std::slice::from_raw_parts(ptr, len);
// Acesso direto sem bounds check
}
8. Caso prático: otimizando uma crate real
Cenário: processamento de logs
use rayon::prelude::*;
// Versão inicial
fn process_logs_naive(logs: &[String]) -> Vec<String> {
logs.iter()
.filter(|l| l.contains("ERROR"))
.map(|l| l.to_uppercase())
.collect()
}
// Versão otimizada
fn process_logs_optimized(logs: &[String]) -> Vec<String> {
let mut result = Vec::with_capacity(logs.len() / 10); // Pré-aloca
logs.par_iter() // Paralelo
.filter(|l| l.contains("ERROR"))
.map(|l| l.to_uppercase())
.collect_into_vec(&mut result); // Evita alocação extra
result
}
Benchmark comparativo
fn bench_logs(c: &mut Criterion) {
let logs: Vec<String> = (0..100_000)
.map(|i| format!("Line {}: {} message", i,
if i % 10 == 0 { "ERROR" } else { "INFO" }))
.collect();
c.bench_function("naive", |b| b.iter(||
process_logs_naive(black_box(&logs))));
c.bench_function("optimized", |b| b.iter(||
process_logs_optimized(black_box(&logs))));
}
Resultados típicos:
- Naive: ~15ms
- Otimizado: ~3ms (5x mais rápido)
Trade-offs:
- Legibilidade: a versão otimizada é mais verbosa
- Manutenibilidade: collect_into_vec é menos comum
- Paralelismo: overhead para datasets pequenos
Referências
- The Rust Performance Book — Guia abrangente sobre otimizações em Rust, com exemplos práticos
- Criterion.rs Documentation — Documentação oficial da crate de benchmarking
- Flamegraph Crate — Ferramenta para gerar flamegraphs a partir de profiling
- Rayon: Data Parallelism in Rust — Documentação da crate de paralelismo de dados
- Rust Compiler Optimization Flags — Documentação oficial das flags de otimização do compilador
- SmallVec Crate — Coleção otimizada para pequenos vetores no stack
- jemalloc: A Scalable Concurrent Allocator — Documentação do alocador jemalloc usado no TikV
- Clippy Performance Lints — Lista completa de lints de performance do Clippy
- Crossbeam: Lock-Free Data Structures — Documentação da crate para concorrência sem bloqueio