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/
criteriondocumentation — benchmarks estatisticamente rigorosos — https://docs.rs/criterionflamegraphcrate — 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
rayondocumentation — 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