jemalloc vs mimalloc em Rust: Alocador Global | Rust Brasil

Compare jemalloc e mimalloc em Rust, configure o alocador global e meça throughput, latência, memória, CPU e RSS antes de levar a mudança para produção.

Para testar jemalloc ou mimalloc em Rust, declare o candidato com #[global_allocator] e compare três builds: alocador padrão, jemalloc e mimalloc. A escolha correta não vem da reputação da biblioteca, mas de medições com a carga do seu serviço. Registre throughput, latência p50/p95/p99, CPU, pico de RSS, memória depois de uma rajada e tempo de build. Se o ganho não for consistente e maior que o ruído, mantenha o padrão.

A troca costuma ser mais promissora em serviços multithread com muitas alocações pequenas, filas, mapas, parsing, serialização e ciclos intensos de criação e descarte de objetos. Em uma API limitada por banco de dados ou rede, o alocador pode representar uma fração irrelevante do tempo total. Por isso, comece com profiling de CPU e memória em produção e use um flame graph em Rust antes de adicionar outra variável ao sistema.

Resposta rápida: qual alocador testar?

CenárioPrimeiro experimentoMotivo
Serviço concorrente com alta taxa de alocaçãopadrão vs jemalloc vs mimalloco padrão de threads pode mudar contenção e fragmentação
CLI curta, executada por poucos segundosmantenha o padrão primeiroinicialização e simplicidade pesam mais
API limitada por Postgres ou chamadas externasprofile antes de trocaro alocador provavelmente não é o gargalo principal
Processo longo com RSS alto após picoscompare memória ao longo do tempouma foto isolada não mostra retenção e devolução ao SO
Binário para vários targetsvalide a matriz completadependências nativas podem mudar portabilidade e empacotamento
Embedded ou no_stdnão aplique este guia diretamenteo modelo de memória e a disponibilidade de heap são diferentes

A recomendação prática é simples: trate o alocador como uma implementação substituível, não como uma otimização obrigatória de Rust.

O que o alocador global faz

Coleções e tipos como Vec, String, Box, HashMap e Arc frequentemente precisam reservar memória no heap. O alocador global atende pedidos de alocação, realocação e desalocação feitos por essas estruturas. O Rust garante regras de ownership e segurança na interface segura, mas a estratégia usada para organizar blocos, arenas, caches por thread e retorno de páginas ao sistema pertence ao alocador.

Na ausência de #[global_allocator], o programa usa o alocador de sistema escolhido para o target. Isso significa que a baseline pode variar entre Linux, Windows e macOS. Compare candidatos no mesmo sistema operacional, imagem, arquitetura e configuração de produção; um resultado obtido no notebook não autoriza uma conclusão sobre o container Linux do serviço.

Também vale separar dois conceitos:

  • vazamento lógico: o programa ainda mantém referências para dados que deveriam ter sido descartados;
  • RSS alto após desalocação: o programa liberou objetos, mas o alocador ou o sistema operacional ainda mantém páginas mapeadas para reutilização.

Trocar de alocador pode mudar o segundo comportamento. Não corrige o primeiro. Se um cache cresce sem limite, nenhum allocator substitui uma política de expiração.

jemalloc e mimalloc: diferenças úteis

O jemalloc é um alocador maduro, conhecido pelo uso de arenas e estruturas voltadas a concorrência, fragmentação e observabilidade. No ecossistema Rust, uma integração comum é a crate tikv-jemallocator. Ele aparece com frequência em serviços long-lived e workloads com muitas threads, mas isso não significa que sempre consumirá menos memória ou entregará menor latência.

O mimalloc, desenvolvido originalmente pela Microsoft, também usa técnicas como páginas, listas locais e caminhos rápidos por thread. A crate mimalloc oferece uma integração direta com o atributo de alocador global. Em alguns workloads ele se destaca por throughput e ergonomia de adoção; em outros, a diferença para o sistema fica dentro do ruído.

CritériojemallocmimallocAlocador padrão
Integração Rust comumtikv-jemallocatormimallocnenhuma dependência extra
Perfil típico de testeserviços concorrentes e long-livedserviços, ferramentas e cargas multithreadqualquer projeto como baseline
Dependência nativasimsimfornecida pelo target/sistema
Observabilidade específicapossui opções e ferramentas própriaspossui opções de diagnóstico própriasdepende da plataforma
Melhor escolha automáticanãonãoé o ponto de partida

Não use essa tabela como ranking. Ela serve para escolher candidatos de benchmark.

Configurando jemalloc em Rust

Adicione a dependência ao Cargo.toml:

[dependencies]
tikv-jemallocator = "0.6"

Na raiz do binário, normalmente em src/main.rs, declare o alocador:

use tikv_jemallocator::Jemalloc;

#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

fn main() {
    let dados: Vec<String> = (0..100_000)
        .map(|n| format!("registro-{n}"))
        .collect();

    println!("{} registros", dados.len());
}

Consulte a release atual da crate antes de fixar a versão real do projeto. Use o lockfile e valide todos os targets suportados. Se você mantém biblioteca, não imponha um alocador global ao consumidor: essa decisão normalmente pertence ao binário final, que conhece o ambiente e a carga.

Configurando mimalloc em Rust

A integração é parecida:

[dependencies]
mimalloc = "0.1"
use mimalloc::MiMalloc;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

fn main() {
    let mut buffers = Vec::with_capacity(50_000);

    for n in 0..50_000 {
        buffers.push(vec![n as u8; 256]);
    }

    println!("{} buffers", buffers.len());
}

As versões acima ilustram a configuração. Confirme versões, features e requisitos de build na documentação oficial e no cargo tree do projeto. A adoção operacional inclui atualizar imagens, SBOM, política de licenças e auditoria da cadeia de dependências — temas aprofundados no guia de cargo-audit e RustSec.

Como manter três builds comparáveis

Editar main.rs antes de cada teste funciona, mas dificulta reproduzir o experimento. Uma alternativa é usar features mutuamente exclusivas:

[features]
default = []
alloc-jemalloc = ["dep:tikv-jemallocator"]
alloc-mimalloc = ["dep:mimalloc"]

[dependencies]
tikv-jemallocator = { version = "0.6", optional = true }
mimalloc = { version = "0.1", optional = true }
#[cfg(all(feature = "alloc-jemalloc", feature = "alloc-mimalloc"))]
compile_error!("ative apenas um alocador global");

#[cfg(feature = "alloc-jemalloc")]
#[global_allocator]
static GLOBAL_JEMALLOC: tikv_jemallocator::Jemalloc =
    tikv_jemallocator::Jemalloc;

#[cfg(feature = "alloc-mimalloc")]
#[global_allocator]
static GLOBAL_MIMALLOC: mimalloc::MiMalloc =
    mimalloc::MiMalloc;

Agora gere os candidatos:

cargo build --release
cargo build --release --features alloc-jemalloc
cargo build --release --features alloc-mimalloc

Não compare arquivos sobrescritos no mesmo target/release. Copie cada binário para um nome identificável ou use diretórios de target separados:

CARGO_TARGET_DIR=target/system cargo build --release
CARGO_TARGET_DIR=target/jemalloc cargo build --release --features alloc-jemalloc
CARGO_TARGET_DIR=target/mimalloc cargo build --release --features alloc-mimalloc

Isso evita executar o candidato errado e deixa o experimento auditável na CI/CD do projeto Rust.

Benchmark: o que medir de verdade

Microbenchmarks com Criterion ajudam quando você isolou uma função que aloca muito. Para escolher o alocador de um serviço, porém, prefira também um teste de carga do processo completo.

Meça pelo menos:

  1. throughput: requests, jobs ou mensagens por segundo;
  2. latência: p50, p95 e p99, não apenas média;
  3. CPU: total e por core durante a fase estável;
  4. RSS: baseline, pico e valor depois da carga;
  5. tempo de recuperação: quanto o processo demora para estabilizar após uma rajada;
  6. falhas e timeouts: performance sem correção não é ganho;
  7. tamanho do binário e imagem: importante para deploy e cold start;
  8. tempo de build: dependências nativas também têm custo operacional.

Um roteiro A/B confiável:

1. fixe commit, Cargo.lock, toolchain e profile de release;
2. use a mesma máquina ou runner dedicado;
3. aqueça o serviço e o gerador de carga;
4. execute cada candidato várias vezes em ordem alternada;
5. descarte warm-up e registre dispersão;
6. repita com carga normal e com pico realista;
7. só escolha quando o resultado for consistente.

Se “mimalloc venceu” por 1,5%, mas a variação entre rodadas é 4%, você mediu ruído. Se jemalloc melhora throughput, mas aumenta p99 ou mantém RSS alto por mais tempo, a decisão depende do SLO e do limite de memória do serviço.

Exemplo de workload de alocação

O programa abaixo não substitui a carga real, mas ajuda a verificar se os três builds estão funcionando:

use std::collections::HashMap;
use std::hint::black_box;
use std::time::Instant;

fn rodada() -> usize {
    let mut mapa = HashMap::with_capacity(200_000);

    for n in 0..200_000 {
        mapa.insert(
            format!("cliente-{n}"),
            vec![(n % 251) as u8; 128 + (n % 512)],
        );
    }

    for n in (0..200_000).step_by(3) {
        mapa.remove(&format!("cliente-{n}"));
    }

    mapa.values().map(Vec::len).sum()
}

fn main() {
    for _ in 0..3 {
        let inicio = Instant::now();
        let total = black_box(rodada());
        println!("bytes={total} tempo={:?}", inicio.elapsed());
    }
}

O exemplo mistura tamanhos e desalocações, mas continua artificial. Um benchmark útil deve reproduzir o comportamento do produto: payloads, concorrência, filas, cache, serialização e duração semelhantes aos de produção.

Armadilhas comuns

Trocar o alocador antes de remover alocações

Se o flame graph aponta format!, clones, collect() intermediários e buffers que crescem repetidamente, primeiro reduza o trabalho. Vec::with_capacity, reutilização de buffers, empréstimos e estruturas adequadas podem eliminar alocações inteiras. O artigo de otimização de performance em Rust mostra esse caminho.

Medir apenas tempo total

Dois candidatos podem terminar o teste no mesmo tempo, mas apresentar p99, CPU e RSS diferentes. Serviços têm SLOs e limites de container; uma única métrica esconde trade-offs.

Confundir memória reservada com vazamento

RSS alto não prova que objetos continuam alcançáveis. Use profiler de heap, métricas do processo e observação depois de rajadas. Confirme se a aplicação retém dados ou se o allocator mantém páginas disponíveis para reutilização.

Ignorar targets e imagens

Uma dependência nativa pode funcionar no Linux glibc do notebook e falhar na imagem final, em outro target ou no cross-compilation. Valide o mesmo Dockerfile usado em produção; o guia de builds Rust otimizados com Docker ajuda a manter essa comparação honesta.

Ativar dois alocadores

Só pode existir um alocador global no binário. Proteja features incompatíveis com compile_error! e teste a matriz. Em workspaces, mantenha a escolha no crate binário, não espalhada entre bibliotecas.

Quando a troca vale a pena

Considere adotar jemalloc ou mimalloc quando:

  • profiling mostra custo relevante em alocação, desalocação ou contenção;
  • o serviço é long-lived e multithread;
  • o benchmark representa a carga real;
  • o ganho se repete em várias rodadas;
  • p99, CPU e memória caminham na direção dos SLOs;
  • a matriz de build e deploy continua suportada;
  • existe rollback simples para o alocador padrão.

Mantenha o padrão quando o benefício é marginal, instável ou restrito a um microbenchmark que não representa o produto. Menos dependências e menos configuração também são ganhos de engenharia.

Alocadores como tema de carreira Rust

Saber configurar #[global_allocator] é fácil. O diferencial em uma entrevista de backend Rust é explicar como você provaria que a troca vale a pena. Uma boa resposta menciona baseline, flame graph, carga representativa, percentis, RSS, concorrência, targets, rollback e decisão orientada por SLO.

Para portfólio, publique um repositório pequeno com:

  • três builds reproduzíveis;
  • script de carga;
  • tabela com várias rodadas;
  • gráficos de latência e memória;
  • conclusão que aceite “nenhuma troca” como resultado válido;
  • README explicando limitações do experimento.

Esse projeto demonstra sistemas, método científico e operação — competências úteis para vagas Rust, mesmo quando a empresa não usa jemalloc nem mimalloc.

Checklist antes do deploy

  • O profiler mostrou que alocação merece atenção?
  • O alocador padrão entrou na comparação?
  • Commit, toolchain, lockfile e profile são idênticos?
  • A carga representa concorrência e payloads reais?
  • Foram medidos p95/p99, CPU, RSS e recuperação após pico?
  • O resultado supera o ruído entre rodadas?
  • Todos os targets e a imagem final compilam?
  • Licença, SBOM e auditoria de dependências foram atualizados?
  • Existe feature ou commit simples para rollback?
  • A decisão e os dados ficaram registrados?

Conclusão

jemalloc e mimalloc são candidatos legítimos para workloads Rust intensivos em alocação, mas nenhum deles é um upgrade automático. Configure cada opção no binário, preserve o alocador de sistema como baseline e faça um experimento controlado. Meça o que o usuário e a operação sentem: throughput, cauda de latência, CPU, RSS e estabilidade após picos.

Se o ganho for claro e repetível, leve a mudança para um canário, observe métricas e mantenha rollback. Se não for, a melhor otimização é não adicionar complexidade. Em Rust, performance profissional não é escolher a biblioteca com o benchmark mais bonito; é demonstrar, no seu workload, por que uma mudança melhora o sistema inteiro.

Perguntas frequentes

Como usar jemalloc como alocador global em Rust?

Adicione tikv-jemallocator, declare uma static do tipo Jemalloc com #[global_allocator] no crate binário e compile todos os targets suportados. Depois compare o resultado ao alocador padrão com carga representativa.

Como usar mimalloc em Rust?

Adicione a crate mimalloc e declare mimalloc::MiMalloc como alocador global. A alteração de código é pequena; o trabalho importante é medir latência, throughput, CPU e memória com rigor.

Qual é mais rápido: jemalloc ou mimalloc?

Depende do workload, da concorrência, dos tamanhos de alocação e da plataforma. Não existe vencedor universal. Inclua também o alocador padrão na disputa.

Trocar o alocador reduz o RSS?

Pode reduzir em uma carga e aumentar em outra. Observe o processo durante estado estável, picos e recuperação, e diferencie retenção lógica de memória mantida pelo allocator para reutilização.

Uma biblioteca Rust deve definir o alocador global?

Normalmente não. A biblioteca não conhece todas as restrições do programa final. Deixe a escolha para o binário ou aplicação que controla deploy, métricas e compatibilidade de targets.