cargo-llvm-lines: Monomorfização Rust | Rust Brasil

Use cargo-llvm-lines para encontrar genéricos que geram muito LLVM IR em Rust. Guia com instalação, leitura do relatório, workspaces, LTO e otimização.

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:

  1. o código Rust é analisado e verificado;
  2. tipos, traits e empréstimos são resolvidos;
  3. funções genéricas são monomorfizadas para tipos concretos;
  4. o compilador produz uma representação intermediária do LLVM;
  5. o LLVM otimiza essa representação e gera código de máquina;
  6. 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:

ColunaO que representaComo usar
LinesTotal de linhas de LLVM IR de todas as instanciaçõesLocalizar grandes geradores de código
CopiesQuantidade de instanciações da funçãoIdentificar multiplicação por tipos genéricos
Function nameNome da função ou implementaçãoDescobrir 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 rustc e 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 é:

FerramentaPergunta principal
cargo-llvm-linesOnde estão muitas linhas de IR e instanciações?
medição de buildA mudança reduziu tempo e memória de compilação?
cargo-bloatA mudança reduziu código no executável?
benchmark/profilerA 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

  1. confirme se o problema é build, memória do compilador ou tamanho do artefato;
  2. fixe toolchain, Cargo.lock, target, profile e features;
  3. rode cargo llvm-lines e salve o total;
  4. rode novamente com --sort copies;
  5. filtre a crate e as funções do domínio;
  6. selecione um único candidato com muitas linhas e cópias;
  7. extraia lógica não genérica ou reduza combinações acidentais de tipos;
  8. compare o relatório antes/depois;
  9. meça build limpo, build incremental, binário e benchmark;
  10. 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.