Introdução: por que a performance do Rust não é mágica, mas disciplina
Rust é merecidamente considerado uma das linguagens de programação de sistema mais rápidas. No entanto, como em qualquer outra linguagem, o código "cru" nem sempre opera no limite das possibilidades. Muitos desenvolvedores iniciantes, ao migrarem para Rust vindos de Python ou JavaScript, esperam um ganho instantâneo de velocidade, mas esquecem que a otimização é um processo, não um presente do compilador.
Este artigo é destinado àqueles que já escreveram seus primeiros programas em Rust e desejam entender como extrair o máximo deles. Abordaremos três direções principais: profiling (para saber o que corrigir), zero-cost abstractions (para não pagar pelo que não usa) e gerenciamento de memória (para evitar gargalos). Todos os exemplos serão acompanhados de código que pode ser executado imediatamente em seu ambiente.
1. Profiling: antes de otimizar, meça
A regra principal da otimização: não otimize às cegas. Sem profiling, você corre o risco de gastar horas "acelerando" um código que já é rápido, enquanto o verdadeiro gargalo permanece despercebido.
Ferramentas para profiling
No ecossistema Rust, existem várias ferramentas excelentes. Para começar, recomendo cargo-flamegraph — ele visualiza onde o programa gasta mais tempo. Instalação:
cargo install flamegraphExecutando o profiling para seu arquivo binário:
cargo flamegraph --bin my_appApós a execução, você obterá um arquivo SVG com o "flame graph". "Barras" largas indicam as funções que consomem mais CPU.
Exemplo: encontrando o gargalo
Suponha que temos uma função que soma os quadrados dos números:
fn sum_of_squares(n: u64) -> u64 { let mut sum = 0; for i in 1..=n { sum += i * i; } sum}Se o profiler mostrar que 90% do tempo é gasto nesta função, sabemos onde olhar. Se o gargalo for entrada/saída (por exemplo, leitura de arquivo), otimizar os cálculos não trará resultado.
2. Zero-cost abstractions: como o compilador ajuda (e atrapalha)
Uma das principais características do Rust é o conceito de zero-cost abstractions. Isso significa que construções de alto nível (iterators, closures, traits) não devem adicionar custos extras em comparação com a escrita manual de loops. No entanto, isso funciona apenas com o uso correto.
Iterators vs. loops
Muitos iniciantes escrevem assim:
let v: Vec<i32> = (0..1000).collect();let mut result = Vec::new();for i in &v { result.push(i * 2);}Este código pode ser reescrito usando iterators, e o compilador Rust o otimizará para o mesmo código de máquina:
let v: Vec<i32> = (0..1000).collect();let result: Vec<i32> = v.iter().map(|x| x * 2).collect();Além disso, se você usar filter, map e fold sequencialmente, o compilador pode "fundi-los" em uma única passagem — isso é chamado de loop fusion. Loops manuais não oferecem essa otimização.
Box<dyn Trait> vs. generics
Outro erro comum é o abuso de trait objects (Box<dyn SomeTrait>). Eles causam dispatch dinâmico, o que adiciona custos extras. Se a quantidade de variantes é conhecida em tempo de compilação, use generics (dispatch estático):
// Ruim: dispatch dinâmicofn process(items: Vec<Box<dyn Processor>>) { for item in items { item.run(); }}
// Bom: dispatch estáticofn process<T: Processor>(items: Vec<T>) { for item in items { item.run()