DEV Community

Cover image for .NET Native AOT: Parte 2 — JIT, R2R e AOT em Benchmarks
Lincoln Zocateli
Lincoln Zocateli

Posted on Originally published at zocate.li

.NET Native AOT: Parte 2 — JIT, R2R e AOT em Benchmarks

Introdução

Não existe um vencedor universal na comparação entre .NET Native AOT, JIT e ReadyToRun. Cada modelo de publicação otimiza uma parte diferente do sistema: Native AOT elimina o JIT em execução e reduz muito o tempo até a primeira resposta; ReadyToRun antecipa parte da compilação sem abandonar o runtime dinâmico; uma aplicação JIT framework-dependent pode compartilhar o runtime instalado com outros processos; e uma publicação JIT self-contained prioriza isolamento operacional. Escolher apenas pelo rótulo “CLI”, “servidor” ou “serverless” ignora as restrições que realmente controlam o resultado.

A decisão precisa começar pelo objetivo mensurável da aplicação. Tempo de startup importa para processos efêmeros, mas throughput aquecido, p95 e p99, CPU, memória residente, tamanho de distribuição, frequência de atualização, compatibilidade com código dinâmico e tempo de publicação também podem decidir o projeto. Neste artigo, eu comparo os quatro modelos com o mesmo código em .NET 10 e linux-x64, preservando os resultados brutos. A conclusão não é uma regra pronta: é uma matriz que relaciona métricas e restrições ao contexto de operação.

Este texto complementa o guia .NET Native AOT: Parte 1 — Ecossistema de Compilação. No guia conceitual, eu expliquei Roslyn, IL, ILC, trimming e compatibilidade. Aqui, o foco é corrigir simplificações comuns com um experimento reproduzível: separar aplicação framework-dependent de distribuição autocontida, separar startup de throughput e não confundir alocação gerenciada com RSS.

ℹ️ Informação: todos os números deste artigo vieram dos CSVs e JSONs do laboratório. Valores absolutos descrevem este workload, este hardware e este ambiente; as relações entre perfis são hipóteses para você testar, não garantias para outra aplicação.

Pré-requisitos

  • .NET SDK 10.0.400 ou uma imagem Docker equivalente.
  • Docker Desktop com containers Linux para reproduzir o ambiente Ubuntu 24.04 e o RID linux-x64.
  • Conhecimento básico de publicação framework-dependent, self-contained, ReadyToRun e Native AOT.
  • uv para executar o consolidador determinístico dos CSVs e JSONs.
  • Microsoft Crank e Bombardier para repetir a carga HTTP.

O que os comentários revelaram sobre a decisão AOT

Uma discussão técnica sobre o artigo conceitual expôs a principal falha das recomendações baseadas apenas no tipo da aplicação. Dizer que Native AOT serve para CLI e serverless, enquanto JIT serve para servidores longos, parece útil porque cabe em uma frase. O problema é que essa frase omite o objetivo. Uma CLI usada uma vez por mês pode não justificar o esforço de compatibilidade; um backend replicado centenas de vezes pode valorizar densidade e startup; um worker duradouro pode depender de plugins; um serviço estável de CPU pode ter um resultado diferente de um serviço com distribuição de tráfego variável.

Startup também não é sinônimo de cold start de provedor. O laboratório mede o intervalo entre iniciar um processo e receber uma resposta válida de /ready, com cache do sistema de arquivos aquecido. Um cold start de Azure Functions, AWS Lambda ou outra plataforma inclui decisões do provedor, criação de sandbox, pull de imagem, rede, configuração e inicialização da aplicação. Eu não medi esse caminho e, portanto, não atribuo ao Native AOT um ganho universal em serverless gerenciado.

Outro ponto importante é a previsibilidade. Remover o JIT evita compilação de métodos durante a execução, mas não remove pausas de GC, escalonamento do sistema operacional, contenção, I/O ou page faults. Da mesma forma, source generators reduzem descoberta dinâmica e deslocam erros para o build, mas não tornam automaticamente qualquer arquitetura mais rápida. São propriedades relevantes para a decisão, não selos de superioridade.

Minha regra revisada é mais exigente: defina uma meta, escolha as métricas capazes de refutá-la e compare distribuições equivalentes. Se o objetivo é reduzir primeira resposta, meça processos novos. Se é aumentar densidade, meça RSS sob a carga representativa. Se é sustentar throughput, aqueça a aplicação e registre latência de cauda e CPU junto com requisições por segundo.

Metodologia: como tornar a comparação justa

O experimento usa Ubuntu 24.04 em Docker Desktop, processador Intel Core i5-1245U x64, aproximadamente 8 GB de memória disponíveis, SDK 10.0.400 e runtime 10.0.11. Todos os perfis executam em Release, ambiente Production, sem providers de logging e com o mesmo payload. O código não acessa banco, disco ou serviço externo durante a medição.

O workload recebe Seed=42, ItemCount=2048 e Iterations=8. Cada execução aluga um array com ArrayPool<int>, gera valores determinísticos, ordena os dados, calcula média e percentil 95 e produz um SHA-256. A Minimal API serializa a resposta com System.Text.Json source generation. Essa combinação exerce CPU, alocação, genéricos e serialização sem deixar I/O externo dominar.

Os nomes combinam o modo de compilação com o modelo de distribuição:

  • JIT (Just-In-Time) compila IL em código nativo durante a execução.
  • FDD (Framework-Dependent Deployment) publica somente a aplicação e exige um runtime .NET compatível instalado no host.
  • SCD (Self-Contained Deployment) inclui o runtime .NET no diretório publicado.
  • R2R (ReadyToRun) antecipa a compilação de assemblies elegíveis, mas preserva runtime e JIT.
  • Native AOT (Native Ahead-Of-Time) gera antes da execução um binário nativo autocontido, sem JIT em produção. Os quatro perfis são:
Perfil Runtime no diretório Código antecipado JIT em execução
JIT FDD (dependente do framework) Não; usa instalação compatível Bibliotecas do runtime podem conter R2R Sim
JIT SCD (autocontido) Sim Bibliotecas do runtime podem conter R2R Sim
R2R SCD (ReadyToRun autocontido) Sim Aplicação e dependências elegíveis Sim
Native AOT (nativo autocontido) Runtime reduzido incluído Aplicação fechada em código nativo Não

O harness restaura dependências antes do cronômetro, executa três pares de publish limpo e incremental e inicia cada perfil 30 vezes em ordem randomizada. Antes de medir startup, ele copia os publishes para o filesystem Linux local do container; executar binários grandes diretamente no bind mount do Windows adicionou quase dez segundos artificiais em testes preliminares.

Para estado aquecido, o BenchmarkDotNet usa jobs out-of-process reais para .NET 10 JIT e Native AOT. A carga HTTP usa Microsoft Crank com Bombardier, 64 conexões, 15 segundos de warm-up e 30 segundos de medição. Aplicação e gerador ficam em containers separados, mas na mesma máquina física. Isso contamina a capacidade absoluta, por isso eu uso os resultados apenas como comparação relativa deste ambiente.

Exemplo Prático: laboratório reproduzível em .NET 10

A API foi construída com o host reduzido do ASP.NET Core. O marcador escrito após Start() permite ao Crank sincronizar a carga sem habilitar logs diferentes entre perfis:

WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.ClearProviders();
builder.Services.Configure<JsonOptions>(options =>
    options.SerializerOptions.TypeInfoResolverChain.Insert(
        0,
        DecisionLabJsonContext.Default));

WebApplication app = builder.Build();

app.MapGet("/ready", () => TypedResults.Ok(new ReadyResponse("ready")));
app.MapPost("/work", (WorkloadRequest request) =>
    TypedResults.Ok(WorkloadProcessor.Process(request)));

app.Start();
Console.WriteLine("Application started.");
app.WaitForShutdown();
Enter fullscreen mode Exit fullscreen mode

📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog:
BlogSamples/NativeAot/DecisionLab/

O comando principal publica, mede tamanho e executa os 120 startups dentro da imagem fixada:

docker run --rm \
  --volume "$PWD:/workspace" \
  --volume decision-lab-nuget:/root/.nuget/packages \
  decision-lab:10.0.400 \
  run --project src/BlogSamples/NativeAot/DecisionLab/DecisionLab.Harness \
  --configuration Release -- --repository-root /workspace
Enter fullscreen mode Exit fullscreen mode

O manifesto registra commit, estado do worktree, fingerprint dos fontes e ambiente. O fingerprint é necessário porque um hash de HEAD não identifica alterações ainda não commitadas. O consolidador calcula mediana, mínimo, máximo, desvio absoluto mediano (MAD) e razão contra JIT FDD; diferenças de até 3% são marcadas como dentro do limiar operacional de ruído, sem fingir significância estatística.

Publish, tamanho e startup de processo

Native AOT deslocou custo para o build. O publish limpo mediano levou 98,145 segundos, contra 11,589 segundos do JIT FDD, 28,450 segundos do JIT SCD e 25,812 segundos do R2R SCD. No publish incremental, a distância diminuiu: Native AOT ficou em 13,398 segundos; JIT FDD, 11,679; JIT SCD, 14,552; e R2R SCD, 13,787.

Perfil Publish limpo mediano Publish incremental mediano
JIT FDD 11,589 s 11,679 s
JIT SCD 28,450 s 14,552 s
R2R SCD 25,812 s 13,787 s
Native AOT 98,145 s 13,398 s

O tamanho exige uma comparação cuidadosa. Os 165.687 bytes do JIT FDD representam somente os arquivos da aplicação; a instalação compartilhada do .NET fica fora do diretório. JIT SCD e R2R SCD incluem runtime e bibliotecas e ficaram perto de 109,7 MB. Native AOT incluiu o runtime reduzido necessário e chegou a 28,3 MB. Compactados, os diretórios ocuparam 81.471 bytes, 48,2 MB, 48,2 MB e 10,9 MB, respectivamente.

Perfil Unidade comparada Descompactado ZIP
JIT FDD Aplicação, sem runtime instalado 165.687 B 81.471 B
JIT SCD Distribuição completa 109.703.386 B 48.156.909 B
R2R SCD Distribuição completa 109.745.882 B 48.177.185 B
Native AOT Distribuição completa 28.306.209 B 10.865.327 B

No startup de processo, Native AOT apresentou mediana de 63,794 ms e MAD de 8,948 ms. JIT FDD registrou 587,644 ms; JIT SCD, 633,357 ms; e R2R SCD, 585,283 ms. R2R e JIT FDD ficaram dentro do limiar operacional de 3%, portanto este experimento não sustenta declarar R2R vencedor no startup.

Perfil Mediana de startup MAD Amostras
JIT FDD 587,644 ms 100,856 ms 30
JIT SCD 633,357 ms 93,885 ms 30
R2R SCD 585,283 ms 79,038 ms 30
Native AOT 63,794 ms 8,948 ms 30

⚠️ Atenção: esses números medem processo novo com cache de filesystem aquecido. Eles não medem boot da máquina, pull de container nem cold start de um provedor serverless.

Runtime compartilhado e unidades autocontidas

Framework-dependent é uma escolha eficiente quando várias aplicações usam uma instalação compatível do .NET no mesmo host. O custo do runtime não desaparece; ele é instalado, atualizado e potencialmente compartilhado fora do artefato da aplicação. Em containers isolados ou hosts sem runtime, essa premissa muda.

Cada publicação Native AOT é uma unidade autocontida e leva as partes do runtime necessárias. Não existe, no modelo de publicação documentado, um runtime Native AOT comum consumido por vários executáveis gerenciados independentes. O sistema operacional pode compartilhar páginas somente leitura entre processos do mesmo binário, mas isso é um comportamento de memória virtual e não transforma executáveis diferentes em clientes de um runtime comum.

ℹ️ Informação: UnmanagedCallersOnly pode exportar entry points C de um módulo Native AOT para interoperabilidade nativa. Isso não cria uma DLL gerenciada AOT compartilhada entre aplicações .NET.

Throughput, latência e memória sob carga

A campanha HTTP usa o mesmo endpoint e payload nos quatro perfis. Eu registro mediana e dispersão de requisições por segundo, p50, p95, p99, CPU máxima do processo e working set máximo em cinco rodadas randomizadas por perfil. Como aplicação e Bombardier compartilham a mesma máquina física, os números não representam a capacidade máxima de produção.

As 20 rodadas terminaram com zero respostas inválidas. JIT FDD apresentou a maior mediana, 932 RPS, seguido por Native AOT com 810 RPS, JIT SCD com 803 RPS e R2R SCD com 734 RPS. A dispersão foi alta: o MAD variou de 82 RPS no JIT SCD a 240 RPS no R2R SCD, e os intervalos observados se sobrepõem. Neste host compartilhado, a ordem das medianas não sustenta uma regra geral de throughput.

Perfil RPS mediana MAD RPS p50 p95 p99
JIT FDD 932 130 63,936 ms 136,854 ms 194,880 ms
JIT SCD 803 82 78,570 ms 142,288 ms 210,696 ms
R2R SCD 734 240 83,483 ms 191,594 ms 312,714 ms
Native AOT 810 186 79,518 ms 145,527 ms 210,021 ms

O sinal mais nítido apareceu em recursos. Native AOT registrou mediana de 41 MB para o working set máximo, contra 103 MB no JIT FDD, 101 MB no JIT SCD e 96 MB no R2R SCD. A CPU máxima mediana foi 62% no Native AOT, 83% no JIT FDD, 79% no JIT SCD e 86% no R2R SCD. São máximos amostrados pelo Crank, não consumo médio nem memória privada.

Perfil CPU máxima mediana Working set máximo mediano MAD do working set
JIT FDD 83% 103 MB 2 MB
JIT SCD 79% 101 MB 2 MB
R2R SCD 86% 96 MB 2 MB
Native AOT 62% 41 MB 1 MB

ℹ️ Informação: nesta campanha, Native AOT usou cerca de 40% do working set máximo mediano do JIT FDD. O resultado é específico deste workload AOT-friendly e não prevê o RSS de outra aplicação.

JIT, Dynamic PGO e Native AOT no código quente

ReadyToRun não elimina o JIT. Com tiered compilation habilitada, métodos usados com frequência podem ter o código R2R substituído por código produzido pelo optimizing JIT. Dynamic PGO trabalha junto com esse mecanismo: instrumenta a execução, observa tipos e caminhos quentes e usa esses dados para recompilar métodos relevantes.

Native AOT não dispõe de JIT em produção, então não pode adaptar o código durante a execução da mesma forma. Isso é uma diferença de capacidade, não uma prova de que JIT sempre vence. Código AOT evita warm-up de compilação e pode empatar ou superar o JIT em workloads específicos; o JIT pode se beneficiar quando o processo dura o suficiente e a distribuição observada orienta otimizações úteis.

No SDK 10.0.400 e nas fontes públicas consultadas, eu não confirmei um fluxo estável e documentado de PGO de build para Native AOT que pudesse ser recomendado como procedimento de produção neste laboratório. Por isso, a comparação usa os defaults suportados e não inclui opções experimentais. Mesmo quando um perfil offline estiver disponível, ele precisa representar produção: mudanças de tipos, rotas ou distribuição de tráfego podem envelhecer a evidência coletada.

Os dois jobs concluídos pelo mesmo BenchmarkDotNet produziram uma mediana de 1,034 ms para JIT e 1,638 ms para Native AOT. Neste workload aquecido, Native AOT levou 1,585 vez o tempo do baseline JIT. As alocações gerenciadas ficaram próximas: 8.984 bytes por operação no JIT e 9.370 bytes no Native AOT, diferença de 4,3%.

Runtime Mediana MAD Amostras Alocação por operação
.NET 10 JIT 1,034 ms 0,133 ms 92 8.984 B
Native AOT 10 1,638 ms 0,161 ms 99 9.370 B

O BenchmarkDotNet marcou a distribuição Native AOT como bimodal (mValue = 3,25) e removeu um outlier; no JIT, removeu oito outliers. Portanto, o resultado refuta a hipótese de empate neste ensaio, mas não demonstra que JIT sempre vence código quente. Ele mostra apenas que, para esta carga, neste ambiente e com os defaults suportados, a adaptação disponível no runtime JIT coincidiu com menor duração mediana.

Source generators e reflection mudam o custo

O workload usa System.Text.Json source generation. O contexto JSON registra os contratos usados pela API, permitindo que o compilador veja quais metadados precisam ser preservados. Isso reduz descoberta baseada em reflection e torna o cenário deliberadamente compatível com Native AOT.

Reflection não é um bloco único. Acesso analisável a metadados ou membros preservados pode funcionar com anotações e testes. Os problemas mais fortes aparecem quando o conjunto de código só é conhecido em execução: Assembly.LoadFile, Reflection.Emit, plugins abertos e serializers que descobrem tipos arbitrários. O trimmer precisa enxergar o grafo no build; caso contrário, ele não consegue provar o que pode remover.

Evitar código dinâmico pode ser uma decisão arquitetural saudável, principalmente quando source generators e contratos explícitos antecipam erros. Também pode ser inviável para um produto cujo valor depende de extensibilidade aberta. Compatibilidade deve ocupar uma linha própria na matriz de decisão; não deve ser tratada como detalhe resolvido por um atributo genérico que preserva tudo.

Onde as conclusões não se aplicam diretamente

Hard real-time exige previsibilidade de ponta a ponta. Ausência de JIT não controla pausas de GC, escalonamento do sistema operacional, interrupções, I/O, locks ou page faults. Soft real-time, por outro lado, pode funcionar com um processo JIT aquecido e medido. Este laboratório não mede deadlines nem usa um sistema operacional de tempo real.

Blazor WebAssembly AOT é outro pipeline. Ele compila código .NET para WebAssembly executado pelo navegador e normalmente troca um download maior por melhor desempenho em trechos intensivos de CPU. Não é o mesmo modelo de executável Native AOT para Linux, Windows ou macOS. Blazor Server se aproxima mais da decisão de um backend ASP.NET Core, mas recursos e dependências precisam ser avaliados separadamente.

Também não há resultados de preview do .NET 11, bibliotecas Native AOT exportadas, provedores serverless ou aplicações com banco de dados. Incluir esses cenários sem medi-los aumentaria o alcance aparente e reduziria a utilidade da conclusão.

Matriz de decisão orientada ao objetivo

Objetivo ou restrição Pergunta de medição Opções que merecem baseline
Primeira resposta Quanto leva processo novo até responder? Native AOT, R2R e JIT
Throughput aquecido Qual perfil sustenta RPS com p95/p99 aceitáveis? JIT com tiering/PGO e Native AOT
Memória e densidade Qual é o RSS sob carga por réplica? Native AOT, JIT FDD e JIT SCD
Distribuição Quantos bytes são transferidos e atualizados? FDD se runtime existe; AOT/SCD se não existe
Runtime compartilhado Várias aplicações usam o mesmo host administrado? JIT FDD
Plugins e código dinâmico O grafo de código é conhecido no build? JIT; validar trimming antes de AOT
Tempo de build A frequência de entrega tolera publish AOT? JIT/R2R como baseline
Operação Ferramentas de diagnóstico cobrem o modo escolhido? Validar por ambiente e incidente esperado

Uma CLI distribuída para máquinas sem .NET pode valorizar Native AOT por startup e empacotamento. Muitas aplicações em um host administrado podem aproveitar FDD. Um backend duradouro e variável pode se beneficiar da adaptação do JIT. Edge e IoT podem priorizar bytes, memória e restrições de execução. Esses exemplos orientam quais testes executar; nenhum substitui a medição.

Limitações do experimento

O hardware é um notebook com Docker Desktop 4.81.0, e aplicação e gerador de carga disputam a mesma CPU. O RID é apenas linux-x64, o sistema é Ubuntu 24.04 e o plano de energia Equilibrado foi observado durante a campanha. Isso não controla frequência, temperatura nem tarefas de fundo do host, que podem aumentar a dispersão. A campanha usa cache de filesystem aquecido e não reinicia o sistema operacional entre amostras.

O workload é sintético, determinístico e deliberadamente AOT-friendly. Ele não representa acesso a banco, rede externa, plugins, descoberta aberta de tipos nem uma base de código grande. A memória reportada pelo Crank é working set máximo, enquanto o BenchmarkDotNet registra alocação gerenciada por operação; essas métricas respondem perguntas diferentes.

Três publishes e 30 startups permitem observar dispersão operacional, mas não transformam o laboratório em estudo universal. As cinco rodadas HTTP ajudam a detectar instabilidade, e o limiar de 3% evita anunciar diferenças pequenas como vencedores. Ainda assim, o limiar não é um teste formal de significância.

Dicas e Boas Práticas

  • Comece com um baseline simples. Compare primeiro a publicação padrão que sua equipe já opera. Native AOT e ReadyToRun precisam melhorar uma métrica relevante o suficiente para pagar o custo de compatibilidade, build e manutenção.

  • Execute performance em Release e Production. Debug e ambiente Development adicionam trabalho e configuração que distorcem resultados. Mantenha logging, GC, payload e funcionalidade iguais entre os perfis sempre que o modo de publicação permitir.

  • Meça distribuições, não uma única execução. Use processos novos para startup, warm-up explícito para steady state e ordem randomizada. Registre mediana e dispersão para não confundir ruído térmico, cache ou tarefa de fundo com efeito do compilador.

  • Separe tamanho de aplicação e tamanho de distribuição. FDD app-only pressupõe runtime instalado; SCD e Native AOT incluem runtime. Declare a unidade comparada e, quando transferência importar, meça também o artefato compactado.

  • Preserve dados brutos e ambiente. CSV, JSON, manifesto, commit e fingerprint dos fontes tornam cada tabela auditável. Copiar números manualmente para Markdown sem rastreabilidade facilita erros de unidade e arredondamento.

  • Trate respostas inválidas como falha do teste. RPS alto com HTTP 400, 500 ou timeout não é desempenho da aplicação. Automatize um gate que exija zero respostas ruins antes de consolidar a rodada.

  • Acompanhe regressões no CI com tolerância ao ruído. Um benchmark isolado não impede que dependências ou mudanças de workload alterem o resultado. Use uma máquina estável e só alerte quando a diferença superar a variabilidade histórica do cenário.

Resumo Objetivo

  • Decisão entre JIT, ReadyToRun e Native AOT — não existe vencedor universal; primeira resposta, throughput, latência de cauda, CPU, memória, distribuição e compatibilidade respondem perguntas diferentes.
  • Startup Native AOT neste ambiente — a mediana foi 63,794 ms em 30 processos, contra 587,644 ms no JIT FDD; a medição usa cache de filesystem aquecido e não representa cold start de provedor.
  • Publish Native AOT neste ambiente — o publish limpo mediano levou 98,145 s, enquanto o incremental levou 13,398 s; o custo mais alto concentrou-se na compilação limpa.
  • Tamanho JIT FDD — os 165.687 bytes medidos cobrem somente a aplicação e pressupõem um runtime compatível instalado, portanto não são diretamente comparáveis às distribuições autocontidas completas.
  • Tamanho Native AOT — a distribuição completa ficou em 28.306.209 bytes descompactados e 10.865.327 bytes em ZIP, incluindo as partes necessárias do runtime sem JIT.
  • ReadyToRun — mantém IL, runtime, JIT e tiered compilation; neste startup, R2R SCD e JIT FDD ficaram dentro do limiar operacional de 3%.
  • Carga HTTP neste ambiente — JIT FDD teve mediana de 932 RPS, Native AOT 810, JIT SCD 803 e R2R SCD 734; Native AOT registrou 41 MB de working set máximo mediano contra 103 MB no JIT FDD, mas a alta dispersão impede generalizar a capacidade.
  • Código quente neste workload — a mediana foi 1,034 ms no JIT e 1,638 ms no Native AOT; o relatório marcou a distribuição AOT como bimodal, portanto a conclusão permanece restrita ao ensaio.

Leia Também

  • .NET Native AOT: Parte 1 — Ecossistema de Compilação
  • Paralelismo em C#: Parallel, Tasks e PLINQ
  • .NET Worker com BackgroundService para Alto Volume
  • C# como Script no .NET: Como Usar e Quando Vale a Pena

Referências

  • Native AOT deployment overview — modelo de publicação, requisitos, runtime reduzido e limitações oficiais.
  • Native code interop with Native AOT — P/Invoke direto e exports C com UnmanagedCallersOnly.
  • ReadyToRun deployment overview — formato R2R, impacto no tamanho e interação com tiered compilation.
  • Compilation config settings — tiered compilation, ReadyToRun e dynamic PGO.
  • Known trimming incompatibilities — serializers por reflection, geração dinâmica e carregamento de assemblies.
  • Blazor WebAssembly AOT — pipeline WebAssembly e trade-off entre download e execução.
  • BenchmarkDotNet NativeAOT — jobs e toolchains out-of-process para runtimes distintos.
  • Microsoft Crank — controller, agents e jobs de carga usados pelo time .NET.
  • ASP.NET Core load and stress testing — orientação para executar carga em Release e Production. 📬

👉 Artigo completo com todos os exemplos de código: .NET Native AOT: Parte 2 — JIT, R2R e AOT em Benchmarks

Top comments (0)