O cargo-llvm-lines é a ferramenta certa quando você suspeita que genéricos, iteradores ou macros estão fazendo um projeto Rust gerar código demais durante a compilação. Instale com cargo install cargo-llvm-lines --locked e execute cargo llvm-lines. O relatório ordena funções pelo total de linhas de LLVM IR e mostra quantas instanciações — ou “cópias” — foram geradas para cada uma.
A leitura mais útil é: uma função com muitas Lines e muitas Copies merece investigação. Isso não significa que ela esteja errada. A monomorfização é justamente o mecanismo que permite a abstrações genéricas entregarem código especializado e sem dispatch dinâmico. O problema aparece quando a especialização se multiplica sem trazer benefício proporcional para o produto.
Este guia explica como usar cargo-llvm-lines, interpretar o relatório, analisar workspaces, distinguir sinal de ruído e testar alternativas sem trocar performance por uma redução apenas estética na quantidade de IR.
O que o cargo-llvm-lines mede
O compilador Rust transforma o programa por várias etapas antes de produzir o executável. De forma simplificada:
- o código Rust é analisado e verificado;
- tipos, traits e empréstimos são resolvidos;
- funções genéricas são monomorfizadas para tipos concretos;
- o compilador produz uma representação intermediária do LLVM;
- o LLVM otimiza essa representação e gera código de máquina;
- o linker monta o artefato final.
O cargo-llvm-lines atua próximo da quarta etapa. Ele compila o target emitindo LLVM IR e conta as linhas associadas às funções encontradas. Segundo a documentação oficial do projeto, a ferramenta foi criada para mostrar quais partes do código oferecem maior alavancagem na melhoria de métricas de compilação.
Ela produz três informações principais:
| Coluna | O que representa | Como usar |
|---|---|---|
Lines | Total de linhas de LLVM IR de todas as instanciações | Localizar grandes geradores de código |
Copies | Quantidade de instanciações da função | Identificar multiplicação por tipos genéricos |
Function name | Nome da função ou implementação | Descobrir qual API investigar |
A contagem não é um cronômetro e também não é o tamanho final do binário. É um proxy de geração de código. Mais IR pode contribuir para build mais lento, mais memória usada pelo compilador e executável maior, mas cada hipótese precisa ser confirmada com outra medição.
O que é monomorfização em Rust
Considere uma função genérica simples:
fn duplicar<T: Clone>(valor: T) -> (T, T) {
(valor.clone(), valor)
}
fn main() {
let _numeros = duplicar(vec![1_u64, 2, 3]);
let _texto = duplicar(String::from("Rust Brasil"));
}
O compilador pode gerar uma versão especializada de duplicar para Vec<u64> e outra para String. Essa estratégia evita uma chamada indireta em runtime e abre espaço para inlining e outras otimizações.
Agora imagine uma função genérica grande chamada com dezenas de tipos, em vários módulos e crates. O corpo especializado pode se repetir muitas vezes. O resultado potencial é:
- mais trabalho de compilação;
- mais memória usada por
rustce LLVM; - mais código para o otimizador processar;
- crescimento do executável;
- builds incrementais menos previsíveis em certos cenários.
Nada disso torna genéricos ruins. Traits, iteradores e abstrações de custo zero são parte central de Rust. O objetivo do cargo-llvm-lines é encontrar os poucos pontos em que a expansão ficou desproporcional.
Para uma base conceitual, revise nosso guia de traits e generics em Rust e o artigo sobre iteradores Rust.
Instalação e primeiro relatório
Instale a ferramenta pelo Cargo:
cargo install cargo-llvm-lines --locked
cargo llvm-lines --version
Na raiz de um projeto:
cargo llvm-lines
Para analisar o perfil de release:
cargo llvm-lines --release
A saída começa com um total e segue com funções ordenadas por linhas. Um exemplo resumido seria:
Lines Copies Function name
----- ------ -------------
18240 420 (TOTAL)
2100 (11.5%) 28 minha_crate::processar
1450 ( 7.9%) 41 core::option::Option<T>::map
980 ( 5.3%) 7 minha_crate::serializar
Os números acima são ilustrativos. A primeira pergunta é: as linhas estão concentradas em uma função com muitas cópias ou em uma função grande com poucas cópias?
- Muitas linhas e muitas cópias sugerem expansão por monomorfização;
- muitas linhas e uma cópia sugerem uma função grande, macro expandida ou código complexo;
- muitas cópias e poucas linhas por cópia podem ser aceitáveis;
- uma função da biblioteca padrão no topo pode ter sido instanciada pelo seu código, não ser um problema da biblioteca.
Ordene por cópias e filtre funções
Por padrão, o relatório é ordenado por linhas. Para destacar quantidade de instanciações:
cargo llvm-lines --sort copies
Para ordenar pelo nome:
cargo llvm-lines --sort name
Também é possível filtrar nomes com expressão regular:
cargo llvm-lines --filter 'minha_crate::'
cargo llvm-lines --filter 'serializ|parse|format'
O filtro é útil quando uma dependência ou a biblioteca padrão domina a tabela e você quer examinar uma área específica. Ainda assim, não esconda o relatório completo cedo demais: uma API chamada no seu código pode aparecer com um caminho de função que você não esperava.
Uma sequência prática é:
cargo llvm-lines > /tmp/llvm-lines-total.txt
cargo llvm-lines --sort copies > /tmp/llvm-lines-copies.txt
cargo llvm-lines --filter 'nome_da_crate::' > /tmp/llvm-lines-local.txt
Assim você mantém a visão geral, a lista de multiplicação e o recorte do código próprio.
Como analisar workspaces e múltiplos targets
Em Cargo workspaces, selecione o pacote com -p ou --package:
cargo llvm-lines -p minha-biblioteca
cargo llvm-lines -p minha-api --release
Quando o pacote tem mais de um target, seja explícito:
cargo llvm-lines -p minha-api --bin servidor --release
cargo llvm-lines -p minha-biblioteca --lib --release
cargo llvm-lines -p ferramentas --example importador
A ferramenta aceita opções comuns de seleção do Cargo, incluindo --features, --all-features, --no-default-features, --target e --manifest-path. Isso permite comparar configurações reais:
cargo llvm-lines --release --no-default-features
cargo llvm-lines --release --features json,postgres
cargo llvm-lines --release --all-features
Essa comparação costuma revelar que uma feature opcional introduz várias instanciações ou que o perfil “com tudo” não representa o binário realmente entregue.
A limitação mais importante em projetos com várias crates
Por padrão, cargo llvm-lines mostra a contribuição da crate raiz analisada; dependências não aparecem como um inventário completo. Genéricos também são monomorfizados na crate que os usa, e não necessariamente naquela que os define.
Para inspecionar uma crate intermediária, use:
cargo llvm-lines -p nome-da-dependencia
Para obter uma visão mais ampla com a geração de código concentrada na crate raiz, a documentação sugere habilitar LTO fat temporariamente:
CARGO_PROFILE_RELEASE_LTO=fat cargo llvm-lines --release
Use esse comando como diagnóstico separado. LTO muda o pipeline e pode tornar a execução bem mais lenta; não compare o resultado com uma baseline sem LTO como se apenas uma função tivesse mudado.
Como criar uma baseline reproduzível
Um relatório isolado é menos valioso do que uma comparação antes/depois. Registre ambiente, commit e configuração:
rustc --version --verbose
cargo --version
git rev-parse HEAD
cargo llvm-lines --release > /tmp/llvm-antes.txt
Faça uma alteração por vez e execute:
cargo llvm-lines --release > /tmp/llvm-depois.txt
diff -u /tmp/llvm-antes.txt /tmp/llvm-depois.txt
Mantenha constantes:
- versão do Rust e do LLVM;
Cargo.lock;- target de compilação;
- profile;
- features;
- variáveis em
RUSTFLAGS; - pacote e target selecionados.
Uma atualização da toolchain pode mudar o IR mesmo sem alteração no código-fonte. Por isso, a baseline deve registrar rustc --version --verbose, e não apenas “stable”.
Padrões que merecem investigação
Função genérica grande usada com muitos tipos
fn transformar<T, U>(entrada: T) -> U
where
T: Validar + Normalizar + Serializar,
U: From<T> + Persistir,
{
// muitas etapas e branches
todo!()
}
Se o corpo é grande e a função recebe dezenas de combinações de T e U, cada especialização pode carregar boa parte da lógica. Uma alternativa é separar uma camada genérica pequena de um núcleo não genérico.
Cadeias longas de iteradores em pontos muito repetidos
Iteradores normalmente geram código excelente. Porém, closures e adaptadores combinados em muitos tipos podem aparecer com várias instanciações. Não reescreva tudo como for por princípio; compare legibilidade, benchmark e relatório.
Serialização genérica espalhada pela aplicação
APIs como fn responder<T: Serialize>(valor: T) são convenientes. Se cada endpoint instancia uma cadeia grande com um tipo diferente, pode valer concentrar etapas comuns depois que a serialização já virou Vec<u8> ou outro formato concreto.
Logging e formatação em funções genéricas
Macros de logging e formatação podem expandir bastante dentro de uma função que será monomorfizada muitas vezes. Mover mensagens invariantes ou etapas não genéricas para uma função auxiliar pode reduzir repetição sem alterar a API pública.
Enums, builders e macros geradas
Código gerado por derive ou macros pode criar implementações extensas. O relatório mostra o efeito compilado, embora a origem esteja em um #[derive(...)] aparentemente pequeno. Confirme com expansão de macro e documentação da crate antes de remover recursos.
Estratégias de redução sem destruir a API
Extraia um núcleo não genérico
Em vez de manter toda a lógica no caminho genérico:
fn processar<T: AsRef<[u8]>>(entrada: T) -> Resultado {
let bytes = entrada.as_ref();
validar(bytes);
decodificar(bytes)
}
Separe a conversão da implementação principal:
fn processar<T: AsRef<[u8]>>(entrada: T) -> Resultado {
processar_bytes(entrada.as_ref())
}
fn processar_bytes(bytes: &[u8]) -> Resultado {
validar(bytes);
decodificar(bytes)
}
A função genérica fica pequena, enquanto o trabalho maior vive em uma única função concreta. O compilador ainda pode fazer inlining; valide o resultado real.
Use trait objects quando a troca fizer sentido
Trocar T: Trait por &dyn Trait reduz algumas especializações, mas introduz dispatch dinâmico e pode limitar otimizações. É adequado quando:
- a operação não está em um hot path;
- diferentes implementações já precisam coexistir em runtime;
- o custo da chamada indireta é irrelevante diante do trabalho realizado;
- a redução foi confirmada e a API continua clara.
Não use trait objects apenas para diminuir Copies em uma tabela.
Faça type erasure depois da fronteira de performance
Outra estratégia é manter a etapa crítica genérica e converter o resultado para um tipo concreto antes de um pipeline grande. Assim, você preserva especialização onde ela traz valor e evita replicá-la no restante da aplicação.
Reduza combinações de tipos acidentais
Às vezes o mesmo conceito circula como String, &str, Cow<'_, str>, Arc<str> e wrappers diferentes sem necessidade. Padronizar a fronteira interna pode reduzir instanciações e também simplificar a arquitetura.
Confirme com tempo de build e tamanho do binário
Uma queda de 20% em linhas de IR não garante build 20% mais rápido. Depois da alteração, meça pelo menos:
cargo clean
/usr/bin/time -v cargo build --release --locked
Repita algumas vezes em ambiente controlado. Para uma visão complementar das etapas, use os recursos descritos no guia de como reduzir o tempo de compilação Rust.
Para verificar o artefato final:
cargo install cargo-bloat --locked
cargo bloat --release --crates
cargo bloat --release -n 20
Nosso guia de cargo-bloat para reduzir binários Rust explica essa análise em detalhes. A combinação correta é:
| Ferramenta | Pergunta principal |
|---|---|
cargo-llvm-lines | Onde estão muitas linhas de IR e instanciações? |
| medição de build | A mudança reduziu tempo e memória de compilação? |
cargo-bloat | A mudança reduziu código no executável? |
| benchmark/profiler | A mudança preservou performance em runtime? |
Use também sccache para cache de compilação e mold para acelerar o link quando o gargalo não for geração repetida de código.
Erros comuns ao usar cargo-llvm-lines
Tratar Lines como tempo
Linhas de IR são um indicador. O custo de otimização varia conforme a natureza do código, e partes diferentes podem exigir trabalhos diferentes do LLVM.
Culpar a biblioteca padrão
Uma função de core, alloc ou outra crate pode aparecer porque seu código a instancia muitas vezes. Investigue o call site e os tipos usados antes de atribuir culpa à dependência.
Comparar debug com release
Profiles diferentes alteram geração de código e otimização. Compare configurações equivalentes.
Otimizar apenas o total
Reduzir o (TOTAL) e piorar a API, a segurança ou a performance é uma derrota. O total ajuda a localizar candidatos; não é uma meta de produto.
Usar dyn Trait em hot paths sem benchmark
Menos monomorfização pode significar menos oportunidade de inlining e vetorização. Meça a carga real.
Ignorar features e targets
O relatório de um binário Linux sem TLS não representa uma release Windows com todas as features. Analise o produto que você realmente distribui.
Colocar um limite rígido na CI cedo demais
A contagem pode mudar com a toolchain. Primeiro salve relatórios e observe a variação normal. Se adotar um orçamento, fixe o Rust e compare deltas relevantes, não um número eterno.
Um fluxo de investigação em 10 passos
- confirme se o problema é build, memória do compilador ou tamanho do artefato;
- fixe toolchain,
Cargo.lock, target, profile e features; - rode
cargo llvm-linese salve o total; - rode novamente com
--sort copies; - filtre a crate e as funções do domínio;
- selecione um único candidato com muitas linhas e cópias;
- extraia lógica não genérica ou reduza combinações acidentais de tipos;
- compare o relatório antes/depois;
- meça build limpo, build incremental, binário e benchmark;
- mantenha a mudança apenas se o ganho justificar a complexidade.
Esse processo também é uma competência relevante para engenharia de compiladores, sistemas, embedded e plataformas internas. Quem sabe explicar monomorfização com dados demonstra mais maturidade do que quem apenas repete que Rust “compila devagar”. Veja as vagas Rust e as empresas que usam Rust para acompanhar oportunidades do ecossistema.
cargo-llvm-lines, Rust e outras linguagens de sistemas
A tensão entre especialização e crescimento de código não é exclusiva de Rust. Linguagens e toolchains escolhem diferentes momentos para executar metaprogramação, gerar especializações ou compartilhar implementações. Para comparar outra abordagem moderna, o portal Zig Brasil explica comptime e metaprogramação em Zig.
A comparação útil não é “qual linguagem gera menos linhas”. É entender como a abstração escolhida afeta tempo de build, tamanho, performance, depuração e manutenção no contexto do produto.
Perguntas frequentes
O que é cargo-llvm-lines?
É uma ferramenta que compila um target Rust emitindo LLVM IR e conta linhas e instanciações por função. Ela destaca genéricos e outros pontos que geram muito código intermediário.
Como instalar e executar?
cargo install cargo-llvm-lines --locked
cargo llvm-lines
cargo llvm-lines --release
Em workspaces, use -p nome-do-pacote. Para targets específicos, combine com --bin, --lib, --example, --test ou --bench.
O que significam Lines e Copies?
Lines é o total de linhas de LLVM IR somado entre as instanciações daquela função. Copies é a quantidade de instanciações; para genéricos, aproxima quantas combinações concretas de tipos foram geradas.
A ferramenta mede tempo de compilação?
Não. Ela mede geração de IR. Use o resultado para criar uma hipótese e confirme com builds temporizados, consumo de memória e ferramentas como cargo build --timings.
Qual é a diferença para cargo-bloat?
cargo-llvm-lines investiga geração de LLVM IR e monomorfização. cargo-bloat investiga bytes de código no executável compilado. Uma função pode gerar muito IR e ser bastante reduzida pelas otimizações; por isso as duas medidas não são equivalentes.
Conclusão
O cargo-llvm-lines torna a monomorfização observável. Em vez de culpar genericamente traits, iteradores ou o compilador, você consegue apontar quais funções geram mais IR, quantas instanciações existem e onde uma mudança arquitetural pequena pode ter maior impacto.
O fluxo recomendado é: gere uma baseline reproduzível, ordene por linhas e cópias, investigue um candidato, extraia o núcleo não genérico quando fizer sentido e confirme tudo com build, cargo-bloat e benchmark. Não persiga a menor tabela possível. Persiga um projeto que compile dentro do orçamento e continue entregando segurança, performance e uma API sustentável.
Para continuar a investigação, leia otimização de performance em Rust, ferramentas essenciais do Cargo e o guia de cargo-zigbuild para cross-compilation. Juntas, essas ferramentas ajudam a separar gargalo de compilação, gargalo de link, tamanho de artefato e performance de runtime — quatro problemas diferentes que pedem medições diferentes.