Pular para o conteúdo

O Toolchain de Compilação Rápida em 2026: sccache, mold, nextest, e o Custo de Esperar

· 13 min read · default
developmentperformancerustbuild-toolsciproductivity

Há uma observação bem conhecida sobre esperar: sob dez segundos, você fica focado; passado um minuto, você muda de contexto e perde o fio. Tempos de compilação se sentam precisamente naquela faixa perigosa. Uma compilação de noventa segundos não apenas custa noventa segundos — custa o reload de tudo que você estava segurando em sua cabeça quando você volta da aba que você mudou. Multiplique por cinquenta compilações um dia através de um time e compilações lentas param de ser uma inconveniência e começam a formar comportamento: commits maiores e mais arriscados porque iterar é caro, menos refatoração porque o loop de feedback a pune, e testes pulados localmente porque levam muito tempo.

A coisa encorajadora sobre desempenho de compilação em 2026 é que as maiores vitórias normalmente são configuração, não engenharia. Você não precisa reestruturar sua base de código; você instala umas poucas ferramentas que endereçam as três fases distintas aonde tempo realmente vai. Este guia cobre aquelas fases — compilação, ligação, e testes — através de sccache, mold, e cargo-nextest, mais bacon para o loop interno, e, importantes, como medir se qualquer coisa disto realmente ajudou.

Medir antes de otimizar

O erro mais comum único é otimizar a fase errada. "A compilação é lenta" não é um diagnóstico — uma compilação é no mínimo compilação, ligação, e (se você está executando eles) testes, e o balanço entre eles varia enormemente por projeto. Uma base de código com muitos crates pequenos e um binário final enorme pode gastar a maioria de seu tempo ligando; um com genéricos pesados e macros pode ser dominado por compilação; um projeto maduro pode gastar mais tempo em testes que em qualquer um.

Então comece com um breakdown de timing ao invés de um palpite. Em Rust, cargo build --timings produz um relatório HTML mostrando exatamente quanto tempo cada crate levou e aonde paralelismo travou — frequentemente revelando que uma dependência serializa tudo o mais. Para uma visão mais aproximada, time cargo build em uma árvore limpa versus uma incremental diz a você quanto você está pagando por compilações frias. E hyperfine dá comparações antes/depois estatisticamente confiáveis ao invés daquela single noisy run que é fácil se enganar com.

Isto importa porque cada ferramenta abaixo endereça uma fase e faz nada pelos outros. Instalar um linker mais rápido quando seu bottleneck é compilação produz um erro de arredondamento e um falso senso de progresso.

Compilação: parar de recompilar o que você já compilou

A fonte mais comum de desperdício em compilação é redundância — compilar o mesmo crate de dependência, com as mesmas flags, que você ou um colega ou um CI runner já compilou uma hora atrás. sccache endereça isto diretamente: ele encapsula o compilador, faz hash das entradas, e retorna um artefato cached quando viu aquela compilação exata antes. Ele suporta Rust, C/C++, e CUDA.

O que eleva sccache além de um cache local é backends de storage compartilhado — S3, GCS, Redis, ou cache de GitHub Actions. Isto muda a economia de CI em particular. A experiência padrão de CI é uma máquina fria compilando cada dependência do zero em cada execução, o qual é puro desperdício uma vez que aquelas dependências não mudaram. Com um cache compartilhado, a primeira execução popula isto e cada execução subsequente baixa ao invés de compilar. Times comumente veem tempos de compilação de CI cairem pela metade ou mais, e o mesmo cache serve máquinas de desenvolvedor.

Setup é uma única variável de ambiente (RUSTC_WRAPPER=sccache) ou uma entrada ~/.cargo/config.toml. O follow-up importante é verificação: sccache --show-stats relata sua taxa de hit, e uma taxa baixa é um sinal que algo em suas entradas está variando — unstable RUSTFLAGS, caminhos absolutos baked em saída, ou compilação incremental interferindo. Um cache com taxa de hit pobre é pior que nenhum, porque você paga o custo de lookup por nada.

Ligação: a cauda serializada

Ligação é a fase que as pessoas esquecem, e é frequentemente o pior ofensor no loop edição-compilação-execução. Eis por quê: compilação paraleliza lindamente em cores, mas ligação tradicionalmente não. Você compila duzentos arquivos em dezesseis cores em vinte segundos, depois espera oito segundos enquanto um core liga. Em uma recompilação incremental — aonde você mudou um arquivo e apenas aquele arquivo recompila — a ligação pode ser a maioria de sua espera.

mold é um linker drop-in desenhado do zero para usar todos seus cores disponíveis. Ele rotineiramente corta tempos de ligação por uma ordem de magnitude, e porque a vitória desembarca precisamente no caminho de recompilação incremental, é a mudança que desenvolvedores sentem mais imediatamente. Adoção é genuinamente trivial: mold -run cargo build encapsula qualquer comando de compilação sem configuração, ou você adiciona -fuse-ld=mold às suas flags de linker para um setup permanente.

A razão para combinar mold com sccache é que eles atacam metades diferentes da mesma espera. sccache elimina compilação que você fez antes; mold acelera a ligação que permanece e não pode ser cached. Nenhum substitui o outro, e juntos tipicamente produzem uma melhoria maior que cada um sozinho.

Testes: isolamento e falhas honestas

A terceira fase é testes, e aqui cargo-nextest muda o modelo ao invés de apenas a velocidade. cargo test padrão executa todos os testes em um binário dentro de um processo; nextest executa cada teste em seu próprio processo. Isto rende várias consequências além do típico speedup 2–3x.

Isolamento se torna real. Testes não podem corromper estado global um do outro, então uma falha dependente de ordenação superfícies imediatamente ao invés de aparecer misteriosamente meses depois. Um crash é atribuível — se um teste segfaults ou aborta, você aprende qual, ao invés de perder os resultados de todo o binário. Testes flutuantes são nomeados como tais: com --retries, um teste que falha então passa é reportado como FLAKY ao invés de silenciosamente passar em rerun, que importa porque um teste flutuante é um problema diferente de um falhante e ocultá-lo é como suites apodrecem. E sharding de CI é integrado via --partition, então dividir uma suite através de runners é uma flag ao invés de um projeto de scripting.

A lacuna principal a saber: nextest não executa doctests, então o padrão de CI comum é cargo nextest run && cargo test --doc.

O loop interno: não execute compilações manualmente

A compilação mais rápida é aquela que você não teve que invocar. bacon roda em um terminal lateral, monitora seu source, e reroda cargo check, clippy, ou testes em cada save, mostrando um resumo de erro compacto sempre-atual. O ganho não é velocidade bruta — é que compilação sobrepõe com seu pensamento ao invés de bloqueá-lo, e você vê o primeiro erro proeminentemente ao invés de rolando saída.

Isto pareia naturalmente com as ferramentas de fase: bacon dá feedback contínuo, sccache e mold fazem cada um daqueles runs de background rápido o bastante para terminar antes de você ter terminado de ler o erro anterior. Para projetos não-Rust, watchexec fornece o mesmo padrão de feedback contínuo para qualquer comando.

Juntando isto, e verificando

Um setup completo é curto:

cargo install sccache cargo-nextest bacon --locked
sudo apt install mold        # ou brew install mold
# ~/.cargo/config.toml
[build]
rustc-wrapper = "sccache"

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

Então verificar cada peça independentemente, porque uma misconfiguration silenciosa é fácil: sccache --show-stats deveria mostrar uma taxa de hit subindo; readelf -p .comment ./target/debug/yourbin | grep -i mold confirma que mold realmente o ligou; cargo nextest run deveria visivelmente terminar mais rápido que cargo test. Medir o resultado end-to-end com hyperfine em uma mudança realista — touch um arquivo, rebuild — ao invés de em uma compilação limpa, uma vez que compilações incrementais são o que você realmente faz o dia inteiro.

Em CI, adicionar o backend de cache compartilhado (SCCACHE_GHA_ENABLED=true em GitHub Actions, ou um bucket S3) e usar --partition de nextest para shard através de runners. CI é aonde estas ferramentas pagam off mais dramaticamente, porque máquinas de CI são frias por padrão e repetem o mesmo trabalho desperdiçado em cada execução.

Além de configuração: o que realmente torna compilações lentas

Se você tem aplicado as ferramentas de fase e o loop ainda é doloroso, as causas restantes são estruturais — e valem entender mesmo que você decida não agir nelas, porque elas explicam por que alguns projetos resistem otimização.

Contagem e profundidade de dependência é a mais comum. Cada crate que você depende deve ser compilado pelo menos uma vez, e um gráfico de dependência profundo serializa: crate C não pode começar até B terminar, que aguardou A. cargo build --timings mostra isto diretamente como um caminho crítico longo com cores ociosos. Auditoria para dependências que você usa trivialmente — puxando em um crate grande para uma função helper — é frequentemente o fix de maior alavancagem estrutural, e reduz superfície de cadeia de suprimentos ao mesmo tempo.

Código genérico e macro-pesado custa tempo de compilação em proporção à instanciação. Uma função genérica usada com vinte tipos é compilada vinte vezes, e macros procedurais pesados executam código arbitrário em tempo de compilação. Aonde um genérico quente não precisa ser genérico, monomorfizá-lo manualmente ou estreitar seus bounds pode cortesemente cortar tempo de compilação. Isto é um real trade contra ergonomia, então medir antes de contorcer uma API.

Granularidade de crate corta ambos os jeitos. Um crate gigante não pode paralelizar internamente e força compilações completas para edições pequenas; centenas de tiny crates adicionam overhead per-crate e uma cadeia de dependência mais profunda. A heurística útil é dividir ao longo de limites que mudam em diferentes rates — código fundacional estável em seu próprio crate para que edições em código volátil não o recompile.

Debug information e settings de otimização são a alavancagem estrutural mais barata. Debug info completa é cara de gerar e ligar; debug = 1 (apenas tabelas de linha) é frequentemente suficiente para backtraces por uma fração do custo. E para dependências que você nunca step through, opt-level overrides em um profile permitem você otimizar seu próprio código sem pagar para otimizar tudo.

Nenhum destes são mudanças de configuração, que é por que pertencem depois das ferramentas. Mas quando um projeto fica lento apesar de caching e um linker rápido, a resposta é quase sempre nesta lista.

Sabendo quando parar

Uma cautela de encerramento: otimização de compilação é em si uma tarefa com retornos diminuindo, e é extraordinariamente boa em se sentir produtiva. Uma vez que seu incremental rebuild é alguns segundos, tuning adicional compra pouco, e as alavancas restantes ficam progressivamente mais invasivas — reestruturar limites de crate, cortar dependências, reworkar genéricos. Aqueles podem valer fazendo, mas são projetos de engenharia com risco real, não mudanças de configuração.

A sequência honesta é: medir primeiro, aplicar fixes rápidos específicas de fase (cache, linker, test runner), medir novamente, e parar quando o loop não quebra mais sua concentração. O objetivo nunca foi um número de benchmark — foi ficar em fluxo o bastante para terminar o pensamento que você tinha quando hit save.

A linha de fundo

Compilações lentas mudam comportamento, não apenas agendamentos, e as maiores fixes em 2026 são configuração ao invés de engenharia. Diagnostique qual fase realmente custa você — cargo build --timings e hyperfine batem intuição — então aplique a ferramenta que a endereça: sccache para parar de recompilar o que você ou CI já compilou, mold para paralelizar a ligação serializada que domina rebuilds incrementais, cargo-nextest para testes mais rápidos, isolados, flutuação-aware com sharding de CI integrado, e bacon para que compilação aconteça enquanto você pensa ao invés de enquanto você espera. Verificar cada um realmente engajado, aplicar o cache compartilhado em CI aonde o desperdício é maior, e parar de otimizar uma vez que o loop para de interrupção você.

Referências e Recursos

Ferramentas

Background e análise

Cheatsheets relacionadas 1337skills