Rust

Performance e Otimização — Profiling, SIMD e Técnicas Avançadas Já leu

18 min de leitura

Performance e Otimização — Profiling, SIMD e Técnicas Avançadas
Rust é frequentemente descrito como uma linguagem que permite escrever código seguro e rápido — mas velocidade não é automática. O compilador faz muito pelo programador, mas entender como o hardware e o compilador funcio

 

Rust é frequentemente descrito como uma linguagem que permite escrever código seguro e rápido — mas velocidade não é automática. O compilador faz muito pelo programador, mas entender como o hardware e o compilador funcionam é o que separa código "razoavelmente rápido" de código que extrai performance máxima da máquina.

Neste artigo cobrimos a metodologia completa de otimização: como medir antes de otimizar, como interpretar resultados de profiling, técnicas algorítmicas, layout de memória, e SIMD para paralelismo no nível da instrução.

A regra fundamental: meça primeiro, otimize depois. Intuição sobre onde o código é lento está errada com surpreendente frequência.


A metodologia de otimização

┌─────────────────────────────────────────────────────┐
│  1. DEFINA A META                                   │
│     "Precisa processar 100k req/s" não               │
│     "deve ser mais rápido"                           │
├─────────────────────────────────────────────────────┤
│  2. MEÇA O BASELINE                                  │
│     Benchmarks reproduzíveis antes de mudar nada     │
├─────────────────────────────────────────────────────┤
│  3. PROFILE                                          │
│     Encontre onde o tempo realmente é gasto          │
├─────────────────────────────────────────────────────┤
│  4. IDENTIFIQUE O GARGALO                            │
│     CPU? Memória? I/O? Alocações?                    │
├─────────────────────────────────────────────────────┤
│  5. OTIMIZE O GARGALO                                │
│     Algoritmo primeiro, micro-otimização depois      │
├─────────────────────────────────────────────────────┤
│  6. MEÇA NOVAMENTE                                   │
│     Confirme melhora, verifique regressões           │
└─────────────────────────────────────────────────────┘

Ferramentas de profiling

perf no Linux

# Compilar com símbolos de debug mas otimizações de release
cargo build --release
# Em Cargo.toml:
# [profile.release]
# debug = true

# Profile da execução
perf record --call-graph=dwarf ./target/release/meu_programa
perf report

# Estatísticas resumidas
perf stat ./target/release/meu_programa

# Cache misses especificamente
perf stat -e cache-references,cache-misses,instructions \
    ./target/release/meu_programa

flamegraph com criterion

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
pprof = { version = "0.13", features = ["flamegraph", "criterion"] }
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
use pprof::criterion::{Output, PProfProfiler};

fn bench_com_flamegraph(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("processamento");

    for tamanho in [1000, 10_000, 100_000] {
        let dados: Vec<u64> = (0..tamanho).collect();

        grupo.bench_with_input(
            BenchmarkId::new("soma", tamanho),
            &dados,
            |b, dados| b.iter(|| {
                dados.iter().sum::<u64>()
            }),
        );
    }

    grupo.finish();
}

criterion_group! {
    name = benches;
    config = Criterion::default()
        .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
    targets = bench_com_flamegraph
}
criterion_main!(benches);
cargo bench --bench benchmarks
# Flamegraph em: target/criterion/processamento/soma/*/profile/flamegraph.svg

cargo-flamegraph

cargo install flamegraph

# Gera flamegraph do programa completo
cargo flamegraph --bin meu_programa

# Com argumentos
cargo flamegraph --bin meu_programa -- arg1 arg2

Benchmarking correto com criterion

use criterion::{
    black_box, criterion_group, criterion_main,
    BenchmarkId, Criterion, Throughput,
};

// Três implementações de busca de substring
fn busca_naive(texto: &[u8], padrao: &[u8]) -> Option<usize> {
    if padrao.is_empty() { return Some(0); }
    texto.windows(padrao.len())
        .position(|w| w == padrao)
}

fn busca_memchr(texto: &[u8], padrao: &[u8]) -> Option<usize> {
    if padrao.is_empty() { return Some(0); }
    // memchr usa instruções SIMD quando disponíveis
    memchr::memmem::find(texto, padrao)
}

fn busca_std(texto: &[u8], padrao: &[u8]) -> Option<usize> {
    // std usa Two-Way algorithm
    let texto_str = std::str::from_utf8(texto).ok()?;
    let padrao_str = std::str::from_utf8(padrao).ok()?;
    texto_str.find(padrao_str)
}

fn bench_busca_substring(c: &mut Criterion) {
    let mut grupo = c.benchmark_group("busca_substring");

    // Cenário: padrão no final de texto grande (pior caso para naive)
    let texto = {
        let mut v = vec![b'a'; 100_000];
        v.extend_from_slice(b"rust_pattern");
        v
    };
    let padrao = b"rust_pattern";

    grupo.throughput(Throughput::Bytes(texto.len() as u64));

    grupo.bench_function("naive", |b| {
        b.iter(|| busca_naive(black_box(&texto), black_box(padrao)))
    });

    grupo.bench_function("memchr", |b| {
        b.iter(|| busca_memchr(black_box(&texto), black_box(padrao)))
    });

    grupo.bench_function("std", |b| {
        b.iter(|| busca_std(black_box(&texto), black_box(padrao)))
    });

    grupo.finish();
}

criterion_group!(benches, bench_busca_substring);
criterion_main!(benches);

Adicione:

[dev-dependencies]
criterion = "0.5"
memchr = "2"

Layout de memória e cache

A maior fonte de lentidão em código moderno não é CPU — é o acesso à memória. Um cache miss L3 custa ~200 ciclos, durante os quais a CPU fica ociosa.

// Comparação: Array of Structs vs Struct of Arrays

#[derive(Clone)]
struct ParticulaAoS {  // Array of Structs (AoS)
    x: f32, y: f32, z: f32,   // posição
    vx: f32, vy: f32, vz: f32, // velocidade
    massa: f32,
    carga: f32,
}

struct ParticulasSoA {  // Struct of Arrays (SoA)
    x: Vec<f32>, y: Vec<f32>, z: Vec<f32>,
    vx: Vec<f32>, vy: Vec<f32>, vz: Vec<f32>,
    massa: Vec<f32>,
    carga: Vec<f32>,
}

// AoS: para atualizar apenas posições, carregamos
// também velocidade, massa e carga — dados desnecessários
fn atualizar_posicoes_aos(particulas: &mut Vec<ParticulaAoS>, dt: f32) {
    for p in particulas.iter_mut() {
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.z += p.vz * dt;
    }
}

// SoA: para atualizar posições, lemos apenas x, y, z, vx, vy, vz
// Cache muito mais eficiente — dados relevantes estão contíguos
fn atualizar_posicoes_soa(p: &mut ParticulasSoA, dt: f32) {
    for i in 0..p.x.len() {
        p.x[i] += p.vx[i] * dt;
        p.y[i] += p.vy[i] * dt;
        p.z[i] += p.vz[i] * dt;
    }
}

// SoA com iteradores paralelos (rayon)
fn atualizar_posicoes_soa_paralelo(p: &mut ParticulasSoA, dt: f32) {
    use rayon::prelude::*;

    p.x.par_iter_mut()
        .zip(p.vx.par_iter())
        .for_each(|(x, vx)| *x += vx * dt);

    p.y.par_iter_mut()
        .zip(p.vy.par_iter())
        .for_each(|(y, vy)| *y += vy * dt);

    p.z.par_iter_mut()
        .zip(p.vz.par_iter())
        .for_each(|(z, vz)| *z += vz * dt);
}

Alinhamento e padding

// Rust ordena campos para minimizar padding por padrão
// MAS a ordem que você escreve pode ser subótima

// Ruim: muitos buracos de padding
#[repr(C)]
struct MalAlinhado {
    a: u8,   // 1 byte + 7 padding
    b: u64,  // 8 bytes
    c: u8,   // 1 byte + 7 padding
    d: u64,  // 8 bytes
    // Total: 32 bytes
}

// Bom: campos maiores primeiro
#[repr(C)]
struct BemAlinhado {
    b: u64,  // 8 bytes
    d: u64,  // 8 bytes
    a: u8,   // 1 byte
    c: u8,   // 1 byte + 6 padding
    // Total: 24 bytes
}

// Rust sem #[repr(C)] reorganiza automaticamente:
struct RustPadrao {
    a: u8,   // Rust pode reordenar
    b: u64,
    c: u8,
    d: u64,
    // Rust garante tamanho mínimo — provavelmente 24 bytes
}

fn verificar_tamanhos() {
    println!("MalAlinhado : {} bytes", std::mem::size_of::<MalAlinhado>());
    println!("BemAlinhado : {} bytes", std::mem::size_of::<BemAlinhado>());
    println!("RustPadrao  : {} bytes", std::mem::size_of::<RustPadrao>());
}

fn main() {
    verificar_tamanhos();
}

Evitando alocações desnecessárias

Alocações heap são custosas: invocam o alocador, podem causar fragmentação, e criam pressão no GC (se houvesse um). Rust torna alocações visíveis — aproveite isso:

use std::borrow::Cow;

// Ruim: sempre aloca, mesmo quando desnecessário
fn processar_nome_ruim(nome: &str) -> String {
    if nome.is_empty() {
        "Anônimo".to_string()  // alocação
    } else {
        nome.to_string()       // alocação
    }
}

// Bom: Cow (Clone on Write) — aloca apenas quando necessário
fn processar_nome(nome: &str) -> Cow<str> {
    if nome.is_empty() {
        Cow::Borrowed("Anônimo")  // sem alocação
    } else {
        Cow::Borrowed(nome)        // sem alocação
    }
}

// Quando modificação é necessária às vezes
fn normalizar_email(email: &str) -> Cow<str> {
    if email.chars().any(|c| c.is_uppercase()) {
        // Só aloca se houver maiúsculas
        Cow::Owned(email.to_lowercase())
    } else {
        Cow::Borrowed(email)  // sem alocação se já minúsculas
    }
}

// Reutilizar buffers em vez de realocar
fn processar_linhas(texto: &str) -> Vec<String> {
    let mut buffer = String::new();
    let mut resultado = Vec::with_capacity(
        texto.lines().count()  // pré-aloca
    );

    for linha in texto.lines() {
        buffer.clear();
        buffer.push_str(linha.trim());
        buffer = buffer.replace("  ", " ");  // normaliza espaços

        if !buffer.is_empty() {
            resultado.push(buffer.clone());
        }
    }

    resultado
}

// SmallVec: evita heap para vetores pequenos
// [dev-dependency] smallvec = "1"
use smallvec::SmallVec;

fn tags_do_documento(html: &str) -> SmallVec<[&str; 8]> {
    // A maioria dos documentos tem menos de 8 tags únicas
    // SmallVec<[T; 8]> usa stack para até 8 elementos
    let mut tags = SmallVec::new();
    // ... parsing ...
    tags
}

Adicione:

[dependencies]
smallvec = "1"

SIMD — Single Instruction, Multiple Data

SIMD permite que uma instrução opere em múltiplos dados simultaneamente. Um registrador AVX2 tem 256 bits — pode processar 8 floats de 32 bits em paralelo:

// SIMD portável com std::simd (nightly) ou com a crate wide

// Abordagem 1: Deixar o compilador auto-vetorizar
// O compilador detecta loops simples e usa SIMD automaticamente

fn soma_escalar(a: &[f32], b: &[f32], resultado: &mut [f32]) {
    for i in 0..a.len() {
        resultado[i] = a[i] + b[i];
    }
}
// Com --release e target-cpu=native, isto vira automaticamente SIMD

// Abordagem 2: SIMD explícito com a crate `wide`
// (mais portável que std::simd que ainda é nightly)

// Abordagem 3: Hints para o compilador
fn soma_vetorizada(a: &[f32], b: &[f32]) -> Vec<f32> {
    // assert ajuda o compilador — elimina bounds checks
    assert_eq!(a.len(), b.len());

    let n = a.len();
    let mut resultado = Vec::with_capacity(n);

    // Chunk de 8 — tamanho de registrador AVX2 para f32
    let chunks = n / 8;
    let resto = n % 8;

    // Loop principal vetorizável
    for i in 0..chunks {
        let base = i * 8;
        for j in 0..8 {
            resultado.push(
                unsafe {
                    // SAFETY: bounds foram verificados acima
                    *a.get_unchecked(base + j) +
                    *b.get_unchecked(base + j)
                }
            );
        }
    }

    // Elementos restantes
    for i in (n - resto)..n {
        resultado.push(a[i] + b[i]);
    }

    resultado
}

// SIMD explícito com std::simd (requer nightly)
// #![feature(portable_simd)]
// use std::simd::{f32x8, SimdFloat};
//
// fn soma_simd(a: &[f32], b: &[f32], resultado: &mut [f32]) {
//     let (a_chunks, a_resto) = a.as_chunks::<8>();
//     let (b_chunks, b_resto) = b.as_chunks::<8>();
//     let (r_chunks, r_resto) = resultado.as_chunks_mut::<8>();
//
//     for ((a, b), r) in a_chunks.iter().zip(b_chunks).zip(r_chunks) {
//         let va = f32x8::from_array(*a);
//         let vb = f32x8::from_array(*b);
//         *r = (va + vb).to_array();
//     }
//
//     for ((a, b), r) in a_resto.iter().zip(b_resto).zip(r_resto) {
//         *r = a + b;
//     }
// }

Usando a crate wide para SIMD portável

[dependencies]
wide = "0.7"
use wide::f32x8;

fn produto_escalar_simd(a: &[f32], b: &[f32]) -> f32 {
    assert_eq!(a.len(), b.len());
    let n = a.len();

    let mut soma = f32x8::ZERO;
    let chunks = n / 8;

    for i in 0..chunks {
        let base = i * 8;
        let va = f32x8::new([
            a[base], a[base+1], a[base+2], a[base+3],
            a[base+4], a[base+5], a[base+6], a[base+7],
        ]);
        let vb = f32x8::new([
            b[base], b[base+1], b[base+2], b[base+3],
            b[base+4], b[base+5], b[base+6], b[base+7],
        ]);
        soma += va * vb;
    }

    // Reduz o vetor SIMD para um escalar
    let soma_arr = soma.to_array();
    let mut total: f32 = soma_arr.iter().sum();

    // Elementos restantes
    for i in (chunks * 8)..n {
        total += a[i] * b[i];
    }

    total
}

fn produto_escalar_escalar(a: &[f32], b: &[f32]) -> f32 {
    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}

fn main() {
    let n = 1_000_000;
    let a: Vec<f32> = (0..n).map(|i| i as f32 * 0.001).collect();
    let b: Vec<f32> = (0..n).map(|i| (n - i) as f32 * 0.001).collect();

    let resultado_escalar = produto_escalar_escalar(&a, &b);
    let resultado_simd    = produto_escalar_simd(&a, &b);

    println!("Escalar: {resultado_escalar:.4}");
    println!("SIMD:    {resultado_simd:.4}");
    println!("Diferença: {:.6}", (resultado_escalar - resultado_simd).abs());
}

Técnicas algorítmicas

Antes de qualquer micro-otimização, revise os algoritmos. Um algoritmo O(n log n) que substitui O(n²) supera qualquer ganho de SIMD:

use std::collections::{HashMap, HashSet};

// Problema: encontrar todos os pares que somam X em um slice

// O(n²) — ruim para n grande
fn pares_soma_quadratico(nums: &[i32], alvo: i32) -> Vec<(i32, i32)> {
    let mut pares = Vec::new();
    for i in 0..nums.len() {
        for j in (i + 1)..nums.len() {
            if nums[i] + nums[j] == alvo {
                pares.push((nums[i], nums[j]));
            }
        }
    }
    pares
}

// O(n) — melhor para n grande
fn pares_soma_linear(nums: &[i32], alvo: i32) -> Vec<(i32, i32)> {
    let mut vistos: HashSet<i32> = HashSet::with_capacity(nums.len());
    let mut pares = Vec::new();

    for &n in nums {
        let complemento = alvo - n;
        if vistos.contains(&complemento) {
            pares.push((complemento, n));
        }
        vistos.insert(n);
    }

    pares
}

// Problema: contar frequências — HashMap com entry API

fn contar_palavras_otimizado(texto: &str) -> HashMap<&str, usize> {
    let mut contagem = HashMap::new();

    for palavra in texto.split_whitespace() {
        // entry() evita dupla busca no HashMap
        *contagem.entry(palavra).or_insert(0) += 1;
    }

    contagem
}

// Lazy evaluation — não compute o que não será usado

struct IteradorPrimos {
    candidato: u64,
}

impl IteradorPrimos {
    fn novo() -> Self {
        IteradorPrimos { candidato: 2 }
    }
}

impl Iterator for IteradorPrimos {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        loop {
            let n = self.candidato;
            self.candidato += 1;

            let e_primo = (2..=(n as f64).sqrt() as u64)
                .all(|i| n % i != 0);

            if e_primo {
                return Some(n);
            }
        }
    }
}

fn main() {
    // Pega apenas os primeiros 10 primos — lazy, sem calcular todos
    let primeiros_10: Vec<u64> = IteradorPrimos::novo()
        .take(10)
        .collect();

    println!("{:?}", primeiros_10);

    // Primeiro primo maior que 1 milhão
    let primo_grande = IteradorPrimos::novo()
        .find(|&p| p > 1_000_000)
        .unwrap();

    println!("Primeiro primo > 1M: {primo_grande}");
}

Paralelismo com Rayon

use rayon::prelude::*;
use std::time::Instant;

fn processar_imagem(pixels: &mut Vec<u8>) {
    // Sequencial
    pixels.iter_mut().for_each(|p| {
        *p = 255 - *p; // inverte
    });
}

fn processar_imagem_paralelo(pixels: &mut Vec<u8>) {
    // Paralelo — rayon divide automaticamente entre threads
    pixels.par_iter_mut().for_each(|p| {
        *p = 255 - *p;
    });
}

fn ordenar_complexo(dados: &mut Vec<String>) {
    // par_sort_unstable é geralmente o mais rápido
    dados.par_sort_unstable();
}

fn mapreduce_paralelo(dados: &[i64]) -> i64 {
    // Map em paralelo, reduce é paralela (árvore de somas)
    dados.par_iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum()
}

fn pipeline_paralelo(textos: &[String]) -> Vec<String> {
    textos
        .par_iter()
        .filter(|t| t.len() > 5)
        .map(|t| t.to_uppercase())
        .filter(|t| t.contains('A'))
        .collect()
}

fn benchmark_paralelo() {
    let n = 10_000_000i64;
    let dados: Vec<i64> = (0..n).collect();

    // Sequencial
    let inicio = Instant::now();
    let soma_seq: i64 = dados.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum();
    let tempo_seq = inicio.elapsed();

    // Paralelo
    let inicio = Instant::now();
    let soma_par: i64 = dados.par_iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum();
    let tempo_par = inicio.elapsed();

    println!("Resultado seq : {soma_seq}");
    println!("Resultado par : {soma_par}");
    println!("Tempo seq     : {:?}", tempo_seq);
    println!("Tempo par     : {:?}", tempo_par);
    println!("Speedup       : {:.2}x",
        tempo_seq.as_secs_f64() / tempo_par.as_secs_f64());
}

fn main() {
    benchmark_paralelo();
}

Um programa completo: processador de texto de alta performance

Vamos combinar as técnicas em um processador de texto que conta palavras, encontra os N mais frequentes, e calcula estatísticas:

use std::collections::HashMap;
use std::time::Instant;
use rayon::prelude::*;
use memchr::memchr_iter;

fn tokenizar_rapido(texto: &[u8]) -> Vec<&[u8]> {
    let mut tokens = Vec::with_capacity(texto.len() / 5); // estimativa
    let mut inicio = None;

    for (i, &byte) in texto.iter().enumerate() {
        let e_palavra = byte.is_ascii_alphanumeric() || byte == b'\'';

        match (e_palavra, inicio) {
            (true, None)     => inicio = Some(i),
            (false, Some(s)) => {
                tokens.push(&texto[s..i]);
                inicio = None;
            }
            _ => {}
        }
    }

    if let Some(s) = inicio {
        tokens.push(&texto[s..]);
    }

    tokens
}

fn contar_frequencias<'a>(
    tokens: &[&'a [u8]]
) -> HashMap<&'a [u8], usize> {
    let mut mapa = HashMap::with_capacity(tokens.len() / 10);

    for &token in tokens {
        if token.len() >= 2 { // ignora palavras de 1 letra
            *mapa.entry(token).or_insert(0) += 1;
        }
    }

    mapa
}

fn top_n<'a>(
    frequencias: &HashMap<&'a [u8], usize>,
    n: usize,
) -> Vec<(&'a [u8], usize)> {
    let mut pares: Vec<_> = frequencias.iter()
        .map(|(&k, &v)| (k, v))
        .collect();

    // partial_sort seria mais eficiente, mas sort_unstable é simples
    pares.sort_unstable_by(|a, b| b.1.cmp(&a.1));
    pares.truncate(n);
    pares
}

struct EstatisticasTexto {
    total_bytes: usize,
    total_tokens: usize,
    tokens_unicos: usize,
    token_mais_longo: usize,
    comprimento_medio: f64,
}

fn calcular_estatisticas(
    texto: &[u8],
    tokens: &[&[u8]],
    frequencias: &HashMap<&[u8], usize>,
) -> EstatisticasTexto {
    let token_mais_longo = tokens.iter()
        .map(|t| t.len())
        .max()
        .unwrap_or(0);

    let comprimento_total: usize = tokens.iter().map(|t| t.len()).sum();
    let comprimento_medio = if tokens.is_empty() {
        0.0
    } else {
        comprimento_total as f64 / tokens.len() as f64
    };

    EstatisticasTexto {
        total_bytes: texto.len(),
        total_tokens: tokens.len(),
        tokens_unicos: frequencias.len(),
        token_mais_longo,
        comprimento_medio,
    }
}

fn processar_texto_completo(texto: &str, top: usize) {
    let bytes = texto.as_bytes();
    println!("── Processando {} bytes ──", bytes.len());

    let inicio = Instant::now();

    // Tokenização
    let t0 = Instant::now();
    let tokens = tokenizar_rapido(bytes);
    println!("Tokenização  : {:?} ({} tokens)", t0.elapsed(), tokens.len());

    // Contagem de frequências
    let t1 = Instant::now();
    let frequencias = contar_frequencias(&tokens);
    println!("Frequências  : {:?} ({} únicos)", t1.elapsed(), frequencias.len());

    // Top N
    let t2 = Instant::now();
    let mais_comuns = top_n(&frequencias, top);
    println!("Top {top}        : {:?}", t2.elapsed());

    // Estatísticas
    let stats = calcular_estatisticas(bytes, &tokens, &frequencias);
    let total = inicio.elapsed();

    println!("
── Estatísticas ──");
    println!("Total bytes      : {}", stats.total_bytes);
    println!("Total tokens     : {}", stats.total_tokens);
    println!("Tokens únicos    : {}", stats.tokens_unicos);
    println!("Token mais longo : {} chars", stats.token_mais_longo);
    println!("Comprimento médio: {:.2} chars", stats.comprimento_medio);
    println!("Tempo total      : {:?}", total);
    println!("Throughput       : {:.1} MB/s",
        stats.total_bytes as f64 / total.as_secs_f64() / 1e6);

    println!("
── Top {top} palavras ──");
    for (i, (token, freq)) in mais_comuns.iter().enumerate() {
        println!("  {:2}. {:20} {:6}x",
            i + 1,
            std::str::from_utf8(token).unwrap_or("?"),
            freq
        );
    }
}

fn main() {
    // Gera texto de teste
    let palavras_base = [
        "rust", "sistema", "memoria", "seguro", "rapido",
        "compilador", "tipo", "ownership", "borrowing", "trait",
        "estrutura", "funcao", "iterador", "closure", "async",
        "tokio", "cargo", "crate", "modulo", "erro",
    ];

    use std::fmt::Write;
    let mut texto = String::with_capacity(1_000_000);

    // Gera ~1MB de texto com distribuição zipf-like
    let mut rng_seed = 42u64;
    for _ in 0..200_000 {
        rng_seed = rng_seed.wrapping_mul(6364136223846793005)
            .wrapping_add(1442695040888963407);

        // Palavras mais curtas são mais frequentes (Zipf)
        let idx = (rng_seed >> 33) as usize % palavras_base.len();
        let peso = palavras_base.len() - idx;
        let repeticoes = (rng_seed >> 40) as usize % peso + 1;

        for _ in 0..repeticoes {
            let _ = write!(texto, "{} ", palavras_base[idx]);
        }
    }

    processar_texto_completo(&texto, 10);
}

Dicas de performance específicas de Rust

#[inline] e #[inline(always)] — para funções pequenas chamadas em loops críticos. O compilador decide sozinho na maioria dos casos, mas #[inline(always)] força:

#[inline(always)]
fn clamp(x: f32, min: f32, max: f32) -> f32 {
    x.max(min).min(max)
}

Eliminar bounds checks com get_unchecked — use apenas quando você provou que os índices são válidos:

unsafe fn somar_sem_bounds_check(a: &[f32], b: &[f32]) -> f32 {
    assert_eq!(a.len(), b.len());
    let n = a.len();
    let mut soma = 0.0f32;
    for i in 0..n {
        // SAFETY: i < n = a.len() = b.len()
        soma += unsafe { a.get_unchecked(i) + b.get_unchecked(i) };
    }
    soma
}

target-cpu=native — compila para o CPU específico da máquina:

RUSTFLAGS="-C target-cpu=native" cargo build --release

LTO e codegen-units=1 — otimização de link-time e compilação monolítica:

[profile.release]
lto = "fat"
codegen-units = 1

Fontes e leituras recomendadas

  • "The Rust Performance Book" — guia completo de performance em Rust — https://nnethercote.github.io/perf-book/
  • criterion documentation — benchmarks estatisticamente rigorosos — https://docs.rs/criterion
  • flamegraph crate — https://github.com/flamegraph-rs/flamegraph
  • "What Every Programmer Should Know About Memory" — Ulrich Drepper — clássico sobre cache e memória — https://akkadia.org/drepper/cpumemory.pdf
  • "Data-Oriented Design" — Richard Fabian — SoA vs AoS e pensamento orientado a dados
  • rayon documentation — paralelismo de dados — https://docs.rs/rayon
  • "Computer Architecture: A Quantitative Approach" — Hennessy & Patterson — para entender o hardware que você está otimizando
  • Godbolt Compiler Explorer — veja o assembly gerado — https://godbolt.org

 

Comentários

Mais em Rust

Projetos Integradores — Construindo Sistemas Completos do Zero
Projetos Integradores — Construindo Sistemas Completos do Zero

Rust — Artigo #51 Projetos Integradores — Construindo Sistemas Completos do Z...

Macros Procedurais — Criando Derive Macros e Atributos Customizados
Macros Procedurais — Criando Derive Macros e Atributos Customizados

No Artigo #21 exploramos macros declarativas com macro_rules! — padrões que t...

Controle de Fluxo — if, loop, while e for como você nunca viu antes
Controle de Fluxo — if, loop, while e for como você nunca viu antes

No artigo anterior, aprendemos que Rust distingue statements de expressions,...