Introdução
Quando penso em binários nativos com .NET, muitos devs imaginam imediatamente executáveis compactos, ultra-rápidos no startup, prontos para serverless. A realidade é mais nuançada. Native AOT (Ahead-Of-Time compilation) é poderoso — mas nem sempre é a resposta. Você pode compilar C# para nativo, sim. Mas qual é o preço real? Como funciona internamente? E em quais cenários faz realmente sentido investir tempo de compilação, limitações de reflection e binários maiores?
Eu criei este artigo porque vi muitos devs tentando usar Native AOT sem medir o trade-off completo. Neste guia, vou percorrer a stack de compilação desde o código C# processado pelo Roslyn até o código nativo emitido pelo ILC e mostrar quais restrições precisam entrar na decisão. A Parte 2 desta série complementa os conceitos com medições reproduzíveis de build, tamanho, startup, throughput e memória.
Pré-requisitos
- Conhecimento de C# e .NET: experiência com .NET 8+ e familiaridade com conceitos como assemblies, IL (Intermediate Language) e garbage collection.
- .NET SDK 9.0+: o artigo foca em .NET 9 e .NET 10 (LTS).
- Noções de compilação: entender diferenças entre JIT (Just-In-Time) e compilação antecipada é útil, mas vou explicar os conceitos conforme necessário.
- Familiaridade com performance: interpretação de métricas básicas de startup, memory footprint e throughput.
O que é Native AOT
Native AOT significa compilar seu código C# em binário nativo antes de executar, não durante a execução. Contraste com o modelo tradicional:
JIT (Just-In-Time):
- Você escreve C#, compila para IL (Intermediate Language)
- No startup do app, o JIT toma a IL
- JIT transforma IL em machine code dinamicamente conforme o código roda
- O .NET runtime (incluindo JIT, GC, todas as bibliotecas) rodam na memória
Native AOT:
- Você escreve C#, compila para IL
- Na máquina de build, o NativeAOT toolchain pega a IL, faz análise estática, remove código desnecessário (trimming)
- Transforma tudo em machine code nativo (x86-64, ARM, etc.) antes de você executar
- Resultado é um executável autocontido — sem JIT e sem exigir uma instalação prévia do .NET, mas com as partes necessárias do runtime
💡 Dica: Native AOT inclui uma versão reduzida das partes necessárias do CoreCLR e das bibliotecas, como GC e alocador. “Sem runtime instalado” é correto; “sem runtime” não é.
Variações de Compilação
Existe um espectro entre JIT puro e AOT puro:
- JIT Padrão (Tier 0): Compila tudo no startup; depois otimiza com Tier 1 conforme uso
- ReadyToRun (R2R): Compilação antecipada parcial; ainda precisa de runtime e JIT está disponível para código não pré-compilado
- Native AOT Completo: Tudo pré-compilado; sem JIT (restrito por trimming e análise estática)
Native AOT é o mais agressivo em tradeoffs: ganhos extremos em startup, mas restrições rígidas em reflection.

Stack de Compilação: Roslyn até Binário Nativo
Aqui está o coração técnico. Vou descrever exatamente o que acontece quando você roda dotnet publish -c Release --self-contained -r win-x64 /p:PublishAot=true.
1. Análise Estática com Roslyn
Roslyn é o compilador C# da Microsoft. Ele:
- Lê
.csfiles - Monta uma árvore sintática (AST)
- Resolve tipos, namespaces, símbolos
- Gera IL (Intermediate Language) — um pseudocódigo agnóstico de plataforma
Para Native AOT, Roslyn não faz nada especial nesta etapa — a emissão de IL é idêntica. A “mágica” acontece depois.
2. IL (Intermediate Language)
IL é como “bytecode universal” do .NET. Um exemplo simples:
| |
Vira IL assim:
| |
Isso é agnóstico de plataforma. Qualquer processador pode executar essas instruções (com uma VM ou compilador).
3. ILCompiler & Trimming (Análise de Reachability)
ILCompiler é o núcleo da magia Native AOT. Ele:
- Lê a IL gerada por Roslyn
- Analisa reachability: Começa do entry point (Main), segue cada chamada, cada campo acessado. “Se Main chamar Foo(), e Foo() chamar Bar(), então Bar() é alcançável.”
- Remove código desnecessário — tree-shaking. Se MetodoX nunca é chamado, é deletado do executável final.
- Detecta reflection problemática: Se você escreve
Type.GetType("MinhaClasse"), o compilador não consegue saber estaticamente qual tipo você quer. Isso é um problema. - Emite código nativo com o ILC usando o backend do RyuJIT
4. ILC e backend RyuJIT
O ILC conduz a compilação Native AOT e usa o backend do RyuJIT para transformar IL em código nativo para x86-64, ARM64 e outras arquiteturas compatíveis. O crossgen2 pertence ao pipeline mais associado ao ReadyToRun e não é uma etapa do publish Native AOT descrito aqui.
Não é o JIT tradicional (que roda em tempo de execução). É um compilador offline, com mais tempo para otimização.
Output: .obj files (objetos nativos), depois linkados em executável final.
5. Linkedição & Executável Final
Os .obj files são linkados com:
- Runtime libraries (.NET GC, allocators, etc.)
- Stdlib linkada (Collections, I/O, etc.)
- Seu código compilado
Resultado: app.exe no Windows ou um executável sem extensão no Linux. A publicação é autocontida: não exige uma instalação prévia do .NET, mas inclui as partes necessárias do runtime e pode depender de bibliotecas nativas do sistema operacional.
Fluxo Visual Completo
| |
📝 Exemplo: Um app “Hello World” simples em JIT ocupa ~100MB (runtime + libs). Em AOT, o executável sozinho é ~5MB.
Trimming e Análise Estática: O Grande Obstáculo
Trimming é onde Native AOT brilha — e onde quebra.
O Problema da Reflection
Reflection permite código descobrir tipos, campos, métodos em tempo de execução:
| |
Por que isso quebra AOT? O compilador estático não consegue saber em tempo de build qual tipo você vai pedir em Type.GetType(). Então, por segurança:
- Ou compila todos os tipos (aumenta o binário)
- Ou falha em runtime (crash)
Solução: TrimmerRootAssembly e Atributos
A Microsoft oferece configuração MSBuild e atributos para informar ao trimmer quais membros precisam ser preservados:
| |
| |
Esses mecanismos não tornam qualquer código automaticamente compatível com AOT. Eles explicitam partes do grafo que a análise estática não conseguiria inferir e ainda exigem testes do artefato publicado.
Análise Conservative vs Aggressive
- Conservative: Trimmer presume que qualquer Type.GetType() pode ser usado; mantém tudo (maior binário, mas seguro)
- Aggressive: Trimmer remove tudo que não é provado alcançável (menor binário, mas pode quebrar em runtime)
Artigos sobre Native AOT sempre recomendam “teste bastante” — e com razão.
Trade-offs de Performance
Native AOT não é uma bala de prata. Aqui estão os reais tradeoffs:
1. Startup: vantagem frequente do Native AOT
Native AOT remove a compilação JIT durante a execução e frequentemente reduz o tempo até a primeira resposta. No laboratório da Parte 2, a mediana foi 63,794 ms para Native AOT e 587,644 ms para JIT framework-dependent em 30 processos. Esses valores descrevem um workload, hardware e ambiente específicos; em serverless gerenciado, o cold start também inclui decisões do provedor, sandbox, rede e configuração.
⚠️ Atenção: AOT coloca todo o tempo de compilação na máquina de build, não no cliente. Se você demora 15 minutos compilando sua app em AOT, bem… existem tradeoffs.
2. Memória e tamanho de distribuição
Uma publicação JIT framework-dependent pode ter poucos arquivos porque pressupõe um runtime compatível instalado fora do artefato. JIT self-contained, ReadyToRun self-contained e Native AOT incluem o runtime necessário, portanto a comparação precisa usar unidades equivalentes. No laboratório da Parte 2, Native AOT ocupou 28,3 MB descompactados e 41 MB de working set máximo mediano; JIT FDD ocupou 165,7 KB de artefato, sem contar o runtime instalado, e 103 MB de working set máximo mediano.
Esses números não estabelecem uma proporção universal. Dependências, genéricos, recursos, símbolos, código preservado para reflection e páginas efetivamente tocadas alteram tanto o tamanho quanto a memória residente.
3. Throughput e Otimização
| Aspecto | JIT | AOT |
|---|---|---|
| Tiering | Sim (Tier 0 → 1 otimização) | Não (fixo) |
| Profiling | Jit ajusta conforme uso | Estático (sem profiling) |
| Hotpath | Melhora com tempo | Fixo (sem warm-up) |
O JIT tem vantagem de adaptação em processos longos porque tiered compilation e dynamic PGO podem observar tipos e caminhos quentes e recompilar métodos. Isso não garante vitória: Native AOT pode empatar ou superar o JIT em workloads específicos e estáveis. Throughput, latência de cauda, CPU e memória precisam ser medidos na aplicação real.
Resumo: Quando Usar?
- Primeira resposta e processos efêmeros: compare Native AOT e ReadyToRun, medindo também publish, distribuição e compatibilidade.
- Throughput e latência aquecidos: compare JIT com tiering/dynamic PGO e Native AOT sob a mesma carga.
- Densidade e operação: inclua RSS/working set, runtime compartilhado, observabilidade, plugins e frequência de atualização.
Exemplo Prático: CLI Tool em Native AOT
Vou mostrar um exemplo real: um String Analyzer CLI que conta frequência de caracteres em arquivos.
A razão de escolher CLI: reflection mínima, I/O puro, ideal para AOT.
Estrutura do Projeto
| |
Arquivo .csproj com AOT Enabled
| |
Destaques:
PublishAot=trueativa o compilador Native AOTTrimMode=linkativa trimming agressivoRuntimeIdentifierslista plataformas alvo- O projeto não adiciona dependências externas
Código Exemplo: Program.cs
| |
Por que isso é AOT-friendly:
- Parsing explícito com APIs da biblioteca padrão
- Nenhum carregamento dinâmico de assemblies ou emissão de código
- I/O simples, sem patterns avançados
Compilar e Executar
| |
📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/NativeAot/CliExample/
Dicas e Boas Práticas
Prefira parsing explícito ou uma biblioteca validada para AOT O exemplo usa apenas APIs da biblioteca padrão. Ao adotar uma biblioteca de CLI, publique e teste o artefato Native AOT para detectar reflection dinâmica ou avisos de trimming.
Minimize reflection; use source generators quando possível Se você precisa de serialização, use
System.Text.Jsoncom source generators (JsonSourceGenerationContext). EviteNewtonsoft.Jsonou reflection pura em AOT.Teste AOT warnings durante desenvolvimento Compile com
dotnet builde cheque warnings. Não deixe para descobrir problemas em produção. Use-Werrorpara converter warnings em erros durante CI.Meça o tamanho da distribuição Compare o diretório publicado e o artefato compactado, declarando se o runtime está incluído. Às vezes, remover uma dependência economiza mais do que alterar o modo de compilação.
Considere ReadyToRun como alternativa Se você quer startup rápido mas não pode abrir mão de reflection,
ReadyToRuné um meio-termo. Compile comPublishReadyToRun=trueem vez dePublishAot=true.Teste em ambientes similares à produção AOT binário compilado em Windows pode não rodar em Linux (arquivos Object linkam diferente). Sempre compile/teste para o target runtime específico.
Compare unidades equivalentes Um assembly IL framework-dependent exclui o runtime instalado, enquanto Native AOT é autocontido. Para edge, IoT ou atualizações frequentes, compare bytes efetivamente transferidos e armazenamento total.
Documente dependências AOT-compatibility Se você usa bibliotecas de terceiros, confirme explicitamente que são AOT-compatible. Mantenha isso em seu README de deployment.
Resumo Objetivo
Native AOT — compilação antecipada de C# em código nativo para um RID específico, eliminando o JIT em execução e incluindo as partes necessárias do runtime sem exigir instalação prévia do .NET.
Stack de compilação — Roslyn gera IL; o ILC analisa alcance e compatibilidade, usa o backend RyuJIT para emitir código nativo e entrega objetos ao linker da plataforma.
Trimming — remoção automática de código não alcançável (tree-shaking); quebra com reflection dinâmica (
Type.GetType()), requerTrimmerRootAssemblyeDynamicallyAccessedMemberspara avisar o compilador.Startup time — Native AOT remove o trabalho de JIT em execução, mas o ganho deve ser medido no hardware e no ambiente de hospedagem reais.
Throughput — JIT com tiering e dynamic PGO adapta código quente em execução; Native AOT pode empatar ou vencer em cargas específicas, portanto não há resultado universal.
Decisão orientada a métricas — primeira resposta, p95/p99, throughput, CPU, memória residente, tamanho de distribuição, compatibilidade e tempo de build devem preceder rótulos como CLI ou servidor.
Trade-offs — startup, memória, tamanho, throughput, compatibilidade e tempo de build variam conforme o workload; a decisão exige medir distribuições equivalentes.
Leia Também
- .NET Native AOT: Parte 2 — JIT, R2R e AOT em Benchmarks
- Async/Await em .NET: Fundamentos de Programação Assíncrona
- Performance em .NET: Profiling, Benchmarking e Otimização
- Microservices em .NET: Padrões, Comunicação e Resiliência
Referências
- Microsoft Docs — Native AOT — documentação oficial, configuração e limitações
- RyuJIT and Native Code Generation — source code do compilador JIT/AOT
- Roslyn — C# Compiler — analysis e code generation
- IL Compiler & Trimming — NativeAOT toolchain
- System.CommandLine — AOT-Compatible CLI — library para argumentos sem reflection
- Trimming .NET Applications — guia de trimming e troubleshooting

Ao comentar, você concorda com nossa Política de Privacidade, Termos de Uso e Política de Exclusão de Dados.