2

Oi, maniero! Obrigado pela leitura cuidadosa, pelos elogios e principalmente pelas perguntas. Você acertou no ponto principal: eu simplifiquei demais a conclusão. Native AOT não deve ser escolhido só pelo tempo de startup, nem o tipo da aplicação determina sozinho a escolha. O que manda é o objetivo mensurado: latência inicial e de cauda, throughput sustentado, memória residente, tamanho de distribuição, restrições de ambiente e compatibilidade do código.

Também quero corrigir duas simplificações minhas. Quando escrevi que o Native AOT roda “sem .NET runtime”, a formulação ficou imprecisa: o aplicativo leva uma versão reduzida das partes necessárias do CoreCLR e das bibliotecas, só não leva o JIT nem exige uma instalação prévia do runtime. E o caminho que descrevi com crossgen2 mistura conceitos: o Native AOT é conduzido pelo compilador ILC, que usa o backend do RyuJIT para gerar código nativo; crossgen2 é a ferramenta mais associada ao ReadyToRun.

Tamanho de deploy: framework-dependent, self-contained e AOT

Sim, para uma aplicação JIT publicada como framework-dependent, o artefato da aplicação pode ter algo perto de 1 MB ou poucos MB, desde que a máquina já tenha o runtime compatível instalado. É uma economia real quando há várias aplicações na mesma máquina usando a mesma instalação do .NET.

Se o runtime não estiver presente, a comparação correta não é “JIT de 1 MB versus AOT de X MB”, mas sim:

ModeloRuntime pré-instaladoArtefato da aplicaçãoObservação
JIT framework-dependentSimPequenoCompartilha o runtime instalado na máquina.
JIT self-containedNãoMaiorLeva runtime e bibliotecas junto com a aplicação.
ReadyToRunDepende da publicaçãoMaior que o JIT equivalenteMantém IL e código pré-compilado; o JIT continua disponível.
Native AOTNãoVaria por aplicaçãoLeva o runtime necessário e código nativo, sem JIT em execução.

Então, sim: um Native AOT bem enxuto normalmente não chega perto de 50 MB. Uma CLI pequena pode ficar em poucos megabytes. Mas não é garantia, porque genéricos com muitas instanciações, dependências, recursos, símbolos de depuração e código preservado para reflexão podem aumentar bastante o resultado. Do outro lado, um publish JIT self-contained também não é um artefato de 1 MB. A única resposta séria aqui é comparar os diretórios de publicação e a memória residente da carga real.

Compartilhar um “core” entre apps AOT

Hoje o modelo padrão do Native AOT não é ter várias aplicações gerenciadas compartilhando um runtime AOT como acontece com o runtime instalado de uma aplicação framework-dependent. Cada executável Native AOT é publicado como uma unidade autocontida e inclui as partes de runtime de que precisa. Essa repetição existe tanto em disco quanto no artefato de deploy.

Há duas nuances importantes:

  • O sistema operacional pode compartilhar páginas de código somente leitura entre processos que executam o mesmo binário; isso ajuda memória quando há muitas instâncias da mesma aplicação, mas não transforma dois executáveis AOT diferentes em consumidores de um runtime comum.
  • É possível publicar uma biblioteca Native AOT para interoperabilidade nativa, exportando funções C com UnmanagedCallersOnly. Ela pode ser carregada ou ligada por código nativo. Isso não equivale a criar uma DLL gerenciada AOT compartilhada entre aplicações .NET, nem elimina o runtime necessário dentro do módulo publicado.

Para várias aplicações .NET independentes no mesmo host, o deploy framework-dependent continua sendo o modelo que realmente compartilha o runtime. Eu não encontrei, na documentação atual, um compromisso público que permita tratar um “runtime Native AOT comum” como roadmap; então prefiro não especular sobre isso.

PGO e desempenho sustentado

Aqui a sua provocação é muito boa. O JIT tem uma vantagem forte quando usa tiered compilation e PGO dinâmico: ele observa tipos e caminhos quentes da execução real e recompila métodos relevantes com essa evidência. Native AOT não pode fazer essa etapa durante a execução, pois não há JIT para recompilar o processo em produção.

Isso não significa que AOT seja condenado a perder throughput. Há otimizações estáticas, escolhas de compilação para tamanho ou velocidade e técnicas de PGO de build quando se dispõe de um perfil representativo. Em uma carga estável, esse perfil pode produzir código excelente, e o AOT também elimina custos de JIT, warm-up e certa variabilidade inicial. Mas existe uma condição grande: o perfil precisa representar a produção. Se ele envelhece ou a distribuição de tráfego muda, o JIT com PGO dinâmico pode se adaptar melhor.

Portanto, “JIT sempre vence em throughput” foi categórico demais no artigo. O correto é: o JIT costuma ter vantagem de adaptação em processos longos e com comportamento variável; Native AOT pode empatar ou vencer em cenários específicos, especialmente quando startup, previsibilidade e perfil estável pesam mais. É caso de benchmark, não de dogma.

Startup não é a única métrica

Concordo integralmente. Startup é o benefício mais visível, mas não o único. Eu deveria ter separado os objetivos assim:

  • Tempo até a primeira resposta: Native AOT e ReadyToRun merecem entrar no teste.
  • Throughput após aquecimento: JIT com tiering e PGO dinâmico é uma referência forte; AOT precisa ser medido com a carga real.
  • Memória residente e densidade de processos: Native AOT pode reduzir memória por instância e remover picos de compilação, mas binário maior e páginas efetivamente tocadas também contam.
  • Previsibilidade de latência: evitar JIT em runtime pode ser relevante em processos efêmeros, ambientes restritos e cargas que valorizam comportamento determinístico.
  • Operação e distribuição: runtime já instalado, tempo de build, suporte a plugins, diagnóstico, atualizações e matriz de RIDs mudam bastante a decisão.

Sobre real time: você tem razão em contestar a frase. Eu quis apontar que o JIT pode otimizar caminhos quentes depois do aquecimento, não que ele seja automaticamente a escolha errada para sistemas de tempo real. Para hard real-time, o problema maior é previsibilidade de ponta a ponta: pausas de GC, escalonamento do SO, I/O, locks e alocações costumam importar mais do que a discussão isolada de JIT versus AOT. E para soft real-time, um processo JIT aquecido e bem medido pode funcionar muito bem.

Também concordo que memória pode invalidar uma conclusão obtida só com requisições por segundo. Um benchmark útil precisa registrar pelo menos latência p50/p95/p99, throughput, RSS ou working set, alocações, CPU, tamanho do artefato e tempo de build. Sem isso, o resultado fica bonito e pouco acionável.

Reflection e tamanho do deploy

Estou alinhado com a ideia de que reflexão dinâmica não é uma virtude obrigatória. Muitas aplicações robustas funcionam perfeitamente sem ela, e source generators, serialização gerada e DI mais explícita melhoram previsibilidade e antecipam erros para o build.

Eu ajustaria apenas a palavra “limitação”: não é uma limitação conceitual de toda reflexão. Há uso de metadados e reflexão que pode ser preservado com anotações e testes. O problema real para Native AOT é código dinâmico que o compilador não consegue provar nem preservar corretamente, como carregamento dinâmico de assemblies, Reflection.Emit, mecanismos genéricos de plugin e serialização baseada em descoberta aberta de tipos. Para um produto que deliberadamente evita esses recursos, isso deixa de ser obstáculo e pode até ser um filtro arquitetural saudável.

E sim, tamanho de deploy raramente é o fator decisivo sozinho. Ele vira relevante em edge, atualizações frequentes, distribuição de CLIs, imagens de container e ambientes com muitas réplicas. Fora disso, é só mais uma métrica da tabela, não um veredito.

Blazor e .NET 11

Ainda não rodei um benchmark próprio com os previews do .NET 11, então não quero transformar impressão de release notes em conclusão. No Blazor WebAssembly, vale separar os termos: o AOT do WebAssembly compila assemblies .NET para WebAssembly; ele não é o mesmo modelo de executável Native AOT para Windows, Linux ou macOS.

O trade-off clássico do Blazor WebAssembly continua sendo baixar mais bytes para reduzir trabalho de compilação no navegador e melhorar a execução depois que o app carrega. Para uma aplicação pequena ou acessada uma vez, o custo de download pode não compensar. Para uma aplicação grande, recorrente e com trechos computacionalmente intensos, pode compensar bastante. Eu avaliaria por rota e por dispositivo alvo, medindo tamanho comprimido, LCP, tempo até interação e desempenho do fluxo quente, em vez de habilitar AOT para tudo por padrão.

No Blazor Server, por sua vez, a conversa é mais próxima de qualquer backend ASP.NET Core: Native AOT depende do subconjunto de recursos compatíveis, e o benefício deve ser medido contra o custo de ecossistema e operação. Quando eu testar o .NET 11, a ideia é publicar números reproduzíveis, com projeto, RID, flags de publish e cenário de carga bem explícitos.

Valeu mesmo pelo puxão de orelha técnico. A melhor conclusão é exatamente a sua: não existe fórmula mágica. Eu vou revisar o artigo para trocar regras absolutas por uma matriz de decisão e deixar mais claro que o objetivo da aplicação, acompanhado de métricas, vem antes do rótulo “CLI”, “servidor” ou “serverless”.

Carregando publicação patrocinada...
1

Eu tinha entendido sobre o runtime, mas fica para quem ainda não sabe como ele funciona.

Pessoalmente acho que se a aplicação usa reflexão dificilmente deveria ser AOT. Se a aplicação é essencialmente dinâmica não é fácil pensar em AOT.

É, PGO em AOT requer um certo compromisso de quem cuida da aplicação, quando é possível.

Estou querendo ver a evolução do Blazor porque sempre achei o modo Wasm dele uma tremenda gambiarra (gambiarras podem ser muito úteis e justificáveis), vamos ver se ele gere todo Wasm, mesmo que não tudo na 11.

Eu não sou muito fã de aplicações web, mas sei que é uma batalha perdida. Já que as pessoas fazem e algumas tem motivos para usar Blazor, eu acho que pelo menos deveria poder ser algo grande. Se é necessário ser pequeno, é quase certo que deveria ser feito em JS/TS, e quem ainda insiste no Blazor que pague o preço de uma decisão ruim :D Tem um pouco de simplificação aqui... Não vou aprofundar porque é outro assunto e para deixar bem claro precisaria de um texto bem longo. Adoro a ideia do Wasm, mas a forma como eles está implmentado me faz entender que quase sempre adotá-lo é uma gambiarra (novamente...)

Não estou respondendo tudo só porque é o que eu já esperava, não sobrou dúvidas ou contestações.

Você demonstra ter um conhecimento completo da computação e capacidade mental ampla, o que costuma ser raro. Esper ote manter por perto ;)

Muito obrigado por responder tão bem. Espero ter ajudado de alguma forma, assim todos aprendem um pouco mais.