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