Introdução
Rust é conhecido por sua velocidade e segurança, porém, mesmo nesta linguagem, é possível escrever código não otimizado. A otimização de desempenho de aplicações Rust é uma arte que combina a compreensão dos mecanismos de baixo nível da linguagem, o gerenciamento de memória e o uso eficiente de concorrência. Neste artigo, analisaremos as principais técnicas que o ajudarão a extrair o máximo do seu código. Você aprenderá como criar perfis de aplicações, usar zero-cost abstractions, gerenciar memória sem garbage collector e aplicar corretamente o multithreading.
1. Criação de Perfis e Benchmarking
Antes de otimizar, é preciso entender o que está causando lentidão. O Rust oferece excelentes ferramentas para medir o desempenho.
1.1. Benchmarks Integrados (cargo bench)
Use cargo bench para escrever micro-benchmarks. Para isso, adicione ao Cargo.toml:
[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }Exemplo de benchmark:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 { match n { 0 => 1, 1 => 1, n => fibonacci(n-1) + fibonacci(n-2), }}
fn bench_fib(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));}
criterion_group!(benches, bench_fib);criterion_main!(benches);1.2. Criação de Perfis com perf e flamegraph
Para trechos de código críticos, use perf (Linux) ou flamegraph. Instale:
cargo install flamegraphExecute a criação de perfil:
cargo flamegraph --bin my_appIsso criará um arquivo SVG com um flamegraph, onde as funções que mais consomem recursos ficam imediatamente visíveis.
2. Otimização do Gerenciamento de Memória
Rust não possui um garbage collector, mas isso não significa que a memória possa ser gerenciada descuidadamente. Pontos-chave:
2.1. Evite Alocações Desnecessárias
Cada chamada a Box::new() ou Vec::push() pode ser custosa. Use Vec::with_capacity() para alocação prévia de memória:
// Ruimlet mut v = Vec::new();for i in 0..1000 { v.push(i);}
// Bomlet mut v = Vec::with_capacity(1000);for i in 0..1000 { v.push(i);}2.2. Use a Pilha em Vez do Heap
Se o tamanho dos dados for conhecido em tempo de compilação, armazene-os na pilha. Por exemplo, use arrays em vez de Vec:
// Na pilhalet arr: [i32; 5] = [1, 2, 3, 4, 5];
// No heap (alocação desnecessária)let vec = vec![1, 2, 3, 4, 5];2.3. Clonagem vs Empréstimo
Evite clone() custosos. Use referências e empréstimo:
fn process(data: &[u8]) { /* ... */ }
// Ruim: cópia de dadoslet data = vec![1, 2, 3];process(&data.clone());
// Bom: passagem de referênciaprocess(&data);3. Zero-cost Abstractions e Iteradores
Rust é famoso por compilar construções de alto nível em código de máquina eficiente. No entanto, é importante saber como usá-las corretamente.
3.1. Iteradores São Rápidos
Cadeias de iteradores geralmente funcionam mais rápido que loops manuais, pois o compilador pode otimizá-las (loop unrolling, inlining):
// Loop manuallet mut sum = 0;for i in 0..1000 { sum += i * 2;}
// Iterador (frequentemente mais rápido)let sum: i32 = (0..1000).map(|x| x * 2).sum();3.2. Evite Dispatch Dinâmico
O uso de dyn Trait adiciona custos indiretos de chamadas virtuais. Sempre que possível, use dispatch estático através de genéricos (impl Trait):
// Dispatch dinâmico (mais lento)fn process(d: &dyn SomeTrait) { d.do_something(