.NET 11 Preview 5: todas as novidades do runtime
O runtime do .NET 11 recebeu no Preview 5 um conjunto expressivo de melhorias, que vão do código assíncrono ao Garbage Collector, passando pelo JIT, por intrínsecos Arm, pelo CoreCLR no WebAssembly e pelos diagnósticos. Neste artigo, você acompanha cada área em detalhes, com números das notas de lançamento oficiais e explicações sobre o impacto no dia a dia.
Além disso, o texto mostra como experimentar alguns recursos em um projeto real. Assim, você consegue avaliar o preview com dados concretos, e não apenas com a lista de mudanças.
O que muda no runtime do .NET 11 Preview 5
O Preview 5 chega em um momento de maturidade do ciclo. Por isso, boa parte do trabalho no runtime do .NET 11 consiste em lapidar recursos que já apareceram nas versões anteriores, em vez de inaugurar conceitos novos.
Se você acompanhou a série desde o início, vale revisitar as novidades do runtime no .NET 11 Preview 1. Afinal, ali começa a história do runtime-async, que ganha o maior destaque deste preview.
As mudanças se agrupam em seis frentes principais:
- Runtime-async com suspensão e retomada mais rápidas.
- Otimizações do JIT para spans, aritmética e casts.
- Novos intrínsecos Arm, incluindo predicados SVE2.
- Melhorias de trimming e compactação no GC.
- Avanços do CoreCLR no browser e no WebAssembly.
- Diagnósticos mais leves e mensagens de carregamento mais claras.
Para ver o panorama geral do lançamento, incluindo SDK e bibliotecas, consulte também o artigo sobre o lançamento do .NET 11 Preview 5. Aqui, o foco fica exclusivamente no runtime.
Runtime-async mais rápido no runtime do .NET 11
O runtime-async, também chamado de Runtime Async V2, é o mecanismo nativo do .NET 11 para métodos assíncronos. Em vez de o compilador C# gerar state machines, o próprio runtime gerencia a suspensão e a retomada de cada await.
Na prática, isso produz stack traces mais limpos, depuração mais simples e menos overhead. Além disso, as bibliotecas do runtime já saem compiladas com esse modelo, o que valida o recurso em larga escala.
Integração com on-stack replacement
A maior melhoria do Preview 5 nessa área envolve o on-stack replacement (OSR) do JIT. O OSR permite que um método de longa execução troque seu código inicial por código otimizado enquanto ainda roda, sem encerrar a chamada.
Antes do Preview 5, contudo, havia um custo escondido. Quando um método assíncrono otimizado por OSR retomava após uma suspensão, o runtime percorria o caminho de transição de propósito geral do OSR, com overhead de 10x a 20x.
Agora, o runtime-async retoma esses métodos diretamente no código otimizado e ignora a transição geral. Com isso, um benchmark focado em suspensões caiu de 6357.1 ms para 457.1 ms, uma redução de aproximadamente 93%.
Código menor, menos TLS e continuações reutilizadas
Além da integração com OSR, o preview entrega outras três melhorias no runtime-async. Primeiro, os caminhos de suspensão mais comuns agora geram código menor.
Nesse caso, o microbenchmark de referência registrou ganho de aproximadamente 8%. O código gerado, por sua vez, encolheu de 766 bytes para 751 bytes.
Em seguida, a suspensão e a retomada passaram a fazer menos trabalho com thread-local storage. Elas também evitam diversas write barriers nos caminhos quentes, e o benchmark de referência caiu de 350.3 ms para 291.3 ms.
Por fim, o runtime-async reutiliza continuações quando um ValueTask baseado em IValueTaskSource suspende. Dessa forma, o runtime elimina uma alocação nesse caminho, algo relevante para bibliotecas de rede e I/O que usam esse padrão.
Como habilitar o runtime-async no projeto
Embora as bibliotecas do runtime do .NET 11 já usem o recurso, projetos de aplicação ainda precisam optar explicitamente. Para isso, basta adicionar a propriedade Features ao arquivo de projeto:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<!-- Habilita o runtime-async no código da aplicação -->
<Features>runtime-async=on</Features>
</PropertyGroup>
Depois disso, o compilador deixa de gerar state machines para os métodos do seu projeto. Então, vale rodar a suíte de testes e um profiler para comparar throughput e alocações antes e depois.
Otimizações do JIT no runtime do .NET 11
Várias otimizações do JIT chegaram no Preview 5. Todas beneficiam código C# típico sem exigir nenhuma alteração na aplicação, porque atuam durante a compilação para código nativo.
Remoção de verificações redundantes em spans
O JIT agora remove mais range checks em loops de span que fatiam repetidamente um prefixo de largura fixa. Esses loops geralmente usam uma verificação de comprimento para saber se ainda há dados suficientes.
No exemplo abaixo, a guarda data.Length >= Vector128<int>.Count prova que as operações seguintes ficam dentro dos limites ao longo da borda de retorno do loop:
int Sum(ReadOnlySpan<int> data)
{
Vector128<int> sum = default;
// A guarda de comprimento permite ao JIT eliminar o range check extra
while (data.Length >= Vector128<int>.Count)
{
sum += Vector128.Create(data);
data = data.Slice(Vector128<int>.Count);
}
int result = Vector128.Sum(sum);
foreach (int item in data)
result += item;
return result;
}
Como resultado, o JIT eliminou o bloco de range check adicional. O código gerado caiu de 113 bytes para 79 bytes.
Uma melhoria relacionada de value numbering também remove verificações redundantes em span.Slice(span.Length - constant). Assim, padrões como ler os quatro últimos bytes de um span compilam diretamente para a instrução de load após a guarda inicial.
Além disso, a propagação de null check agora atravessa nós PHI que mesclam um valor recém-criado com um valor não nulo. Para o padrão (_inner ??= new Inner()).Do(n), por exemplo, o JIT descarta a verificação explícita de null antes do branch.
Aritmética menor e casts mais rápidos
O JIT agora transforma CONST - x em x ^ CONST quando informações de intervalo provam que as duas expressões são equivalentes. Parece um detalhe, mas reduz instruções em código de baixo nível.
Para um byte, 255 vale 1111_1111 em binário. Portanto, x ^ 255 inverte exatamente esses oito bits e produz o mesmo resultado que 255 - x.
Na prática, o código gerado para 255 - byte passa de neg mais add para um único xor. Já para -1 - x, o JIT emite apenas not.
Em processadores x86 com AVX-512 ou AVX10.2, os casts de ponto flutuante para long e ulong também melhoraram. Agora eles usam instruções de conversão de hardware em todos os casos sem overflow.
O diff típico de double para long, por exemplo, substitui a chamada ao helper CORINFO_HELP_DBL2LNG pela instrução vcvttpd2qq com tratamento de máscara. Com isso, código numérico intenso ganha desempenho sem nenhuma reescrita.
Novos intrínsecos Arm no runtime do .NET 11
O namespace System.Runtime.Intrinsics.Arm ganhou overloads para inteiros nativos em vários intrínsecos escalares. Dessa forma, código portável que trabalha com nint e nuint deixa de precisar de conversões manuais.
Os métodos ArmBase.LeadingZeroCount, ArmBase.ReverseElementBits, Crc32.ComputeCrc32 e Crc32.ComputeCrc32C passam a aceitar nint ou nuint. Então, o JIT baixa a forma de 64 bits diretamente para a largura de instrução Arm64 correspondente:
using System.Runtime.Intrinsics.Arm;
// Verifica o suporte do hardware antes de usar o intrínseco
if (Crc32.IsSupported)
{
nuint value = (nuint)0x1234_5678;
uint crc = Crc32.ComputeCrc32(0, value);
}
Para SVE2, o runtime do .NET 11 adiciona 14 métodos de geração de predicados, nas famílias CreateWhileGreaterThanMask* e CreateWhileReadAfterWriteMask*. Os primeiros cobrem elementos byte, signed byte, 16 bits, 32 bits, 64 bits, double e single.
Já os métodos CreateWhileReadAfterWriteMask* criam máscaras a partir de intervalos de ponteiros. Com isso, o runtime do .NET 11 amplia bastante a superfície de vetorização em hardware Arm com SVE2, algo útil para processamento de imagens, compressão e cargas numéricas.
GC no runtime do .NET 11: trimming e compactação
O Garbage Collector também evoluiu no runtime do .NET 11 Preview 5. Se você quiser revisar os fundamentos antes, o artigo sobre o Garbage Collector em C# explica gerações, coletas e compactação.
Novo switch de trimming do GC
O Preview 5 introduz um novo switch de configuração do GC, chamado DOTNET_GCTrimYoungestKeepPercent. Ele atua junto do modo de latência com footprint de memória reduzido, ativado por DOTNET_GCLatencyLevel=0.
Com o switch, esse modo mantém uma porcentagem configurável da geração mais jovem durante o trimming. Assim, a aplicação ganha mais uma forma de equilibrar economia de memória e custo de startup:
# Ativa o modo de footprint reduzido e mantém parte da geração mais jovem
$env:DOTNET_GCLatencyLevel = "0"
$env:DOTNET_GCTrimYoungestKeepPercent = "0xF"
Note que o valor usa notação hexadecimal, como outras configurações do GC. Portanto, 0xF corresponde a 15%.
Trata-se de uma opção avançada, voltada a cenários em que a memória importa muito, como containers pequenos. Por isso, ajuste o valor com benchmarks que representem a carga real da aplicação.
Correção na compactação do heap
Na compactação, a equipe corrigiu um problema em que o watermark heap_segment_used ficava desatualizado após a relocação de objetos em uma região. Esse bug causava a retenção de dados sujos em certas configurações.
Isso acontecia quando large pages ou never_decommit_p faziam decommit_region limpar apenas até used. Como consequência, sobrava uma lacuna obsoleta na região.
Com a correção, os benchmarks de cinco minutos em large pages mostraram aumento de 2,1% no throughput médio. Além disso, o pico de throughput subiu 9,4% e o número de eventos de falta de memória caiu 35%.
CoreCLR no browser com o runtime do .NET 11
O CoreCLR para browser continua seu processo de ativação no runtime do .NET 11. Historicamente, o WebAssembly no .NET roda sobre o Mono, então essa migração representa uma mudança estrutural.
Neste ciclo, quatro melhorias chegaram. Primeiro, o carregador do browser CoreCLR passou a implementar invokeLibraryInitializers, o mesmo hook de inicialização de bibliotecas que o Mono usa no WebAssembly.
Em seguida, o retry de download virou o comportamento padrão no loader do browser. O preview também melhora o sequenciamento de retry para assets do framework, o que aumenta a resiliência em conexões instáveis.
O WASM RyuJIT, por sua vez, agora gera código para caminhos de reverse P/Invoke e UnmanagedCallersOnly. Isso habilita interoperabilidade com código não gerenciado em aplicações WebAssembly baseadas em CoreCLR.
Por fim, o browser CoreCLR consegue resolver código do interpretador quando FEATURE_PORTABLE_ENTRYPOINTS está ativo. Esse ajuste suporta caminhos de diagnóstico, como sampling e lookups de profiler.
Como experimentar o CoreCLR em um projeto Blazor WebAssembly
Neste preview, o CoreCLR para browser funciona por opt-in. Para habilitá-lo em um projeto Blazor WebAssembly que já usa net11.0, adicione a propriedade UseMonoRuntime ao arquivo de projeto:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<!-- Troca o runtime Mono pelo CoreCLR no browser -->
<UseMonoRuntime>false</UseMonoRuntime>
</PropertyGroup>
Para confirmar que o aplicativo roda em CoreCLR, abra o console do navegador e execute o comando abaixo:
// Disponível apenas quando a aplicação roda sobre CoreCLR
globalThis.getDotnetRuntime(0).INTERNAL.GetDotNetRuntimeHeap()
O CoreCLR expõe o hook GetDotNetRuntimeHeap, que retorna um Uint8Array. Como o runtime Mono não expõe esse hook, uma chamada bem-sucedida confirma a execução sobre CoreCLR.
Vale lembrar, contudo, que AOT e os caminhos de build nativo ainda dependem do Mono neste preview. Então, use o CoreCLR no browser para testes e feedback, não para produção.
Diagnósticos no runtime do .NET 11
Os diagnósticos também ficaram mais eficientes no runtime do .NET 11. Em primeiro lugar, heap dumps agora saem menores e mais rápidos por padrão, porque o createdump passou a usar HEAP2.
O HEAP2 usa um caminho unificado de enumeração de regiões de memória para os dados que as ferramentas de diagnóstico precisam. Dessa forma, ele ignora caminhos antigos que deixavam os dumps lentos e grandes.
As variáveis de ambiente que antes serviam de workaround para dumps lentos deixaram de existir. Além disso, as ferramentas de análise continuam funcionando normalmente, já que o dump continua semanticamente equivalente.
Os conflitos de versão de assembly também ganharam mensagens melhores. Quando um assembly de versão menor já está carregado, a mensagem de FileLoadException agora inclui a identidade e o caminho dele:
A different copy of assembly 'LibA' is already loaded.
Loaded assembly: 'LibA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
from 'C:\repos\helloworld\bin\Debug\net10.0\LibA.dll'
Essa melhoria facilita muito o diagnóstico em aplicações com dependências complexas. Em cenários de plugins, por exemplo, descobrir qual cópia de um assembly venceu a disputa costumava exigir ferramentas extras.
Correções de bugs no runtime do .NET 11 Preview 5
O Preview 5 inclui um volume expressivo de correções, distribuídas entre as principais áreas do runtime do .NET 11. Embora não chamem tanta atenção, elas pesam na estabilidade da versão final.
Correções no JIT e no GC
Na geração de código, a equipe corrigiu o tratamento de data breakpoints após inlining de CORINFO_HELP_ARRADDR_ST em x64. Também resolveu inconsistências de perfil em otimizações de flow graph e problemas de alocação de registradores em torno de kills implícitos e liveness local.
Ainda no JIT, houve ajustes no runtime-async com ReadyToRun em RISC-V64 e em loads e stores de vetores e máscaras alocados na stack em Arm64. Além disso, uma falha SIGILL em plataformas Arm64 com SME, mas sem SVE, deixou de ocorrer.
No GC, a afinidade de CPU passou a funcionar em máquinas Unix com mais de 1024 CPUs. A equipe também desabilitou um modo agressivo de coleta de large pages para cenários ReadyToRun.
Correções no NativeAOT, no cDAC e no Mono
No NativeAOT, as correções envolvem GC roots após transições universais e um bug em acesso secundário de dependent handle com GC standalone. Também entraram a preservação do estado de execução abortada nas informações de GC e o parsing hexadecimal de configurações com prefixos 0x e 0X.
Em diagnósticos, o cDAC ampliou o suporte a estado de attach do depurador, flags do compilador, segmentos de heap e APIs de contexto de tipo genérico. Da mesma forma, passou a cobrir assinaturas vararg, layouts de tipo e array, statics de tipos coletáveis e mais enumeração de metadados.
A equipe ainda resolveu dados obsoletos em lastThrownObjectHandle durante o dispatch ativo de exceções. Também eliminou uma falha SIGSEGV no createdump ao incluir frames ativos do interpretador.
No Mono e no interpretador, por fim, os signal handlers voltaram a funcionar durante o encadeamento de crash. O interpretador também ganhou suporte a stack walking no cDAC e um tratamento correto de breakpoints para exceções nativas de first chance.
Como avaliar o preview com segurança
Um preview do runtime do .NET 11 não tem suporte para produção. Mesmo assim, testar cedo ajuda a encontrar regressões e a planejar a migração com antecedência.
Uma abordagem prática envolve três passos. Primeiro, instale o SDK do Preview 5 lado a lado com a versão estável e crie uma branch de avaliação.
Em seguida, rode os benchmarks e a suíte de testes com e sem runtime-async=on. Assim, você isola o efeito do novo modelo assíncrono das demais melhorias do JIT.
Por último, registre os resultados e reporte qualquer regressão no repositório oficial. A equipe do .NET pede explicitamente feedback sobre throughput e tamanho de bibliotecas, como indica a página What's new in the .NET 11 runtime.
Conclusão
O Preview 5 mostra um runtime do .NET 11 maduro, com ganhos quantificados em várias frentes. A redução de 93% no custo de suspensão do runtime-async integrado ao OSR é o destaque, com impacto direto em aplicações de alta concorrência.
Além disso, as otimizações do JIT para spans, aritmética e casts beneficiam código existente sem mudanças. Os novos intrínsecos Arm, por sua vez, ampliam a vetorização em hardware moderno.
As melhorias no GC, no CoreCLR para browser e nos diagnósticos completam um preview equilibrado. Dessa forma, o terreno fica preparado para o lançamento final do .NET 11, previsto para novembro de 2026.
FAQ
O runtime-async vem habilitado por padrão em projetos .NET 11?
As bibliotecas do runtime já usam runtime-async=on, mas projetos de aplicação precisam optar com <Features>runtime-async=on</Features>. Sem essa propriedade, o compilador continua gerando state machines.
As melhorias de OSR valem para todos os métodos assíncronos?
Não, valem apenas para métodos que o JIT otimizou por OSR, ou seja, métodos de longa execução recompilados enquanto rodavam. Para os demais, as melhorias de código gerado e de TLS continuam valendo.
O CoreCLR para browser substitui o Mono no Preview 5?
Ainda não, porque ele funciona por opt-in com <UseMonoRuntime>false</UseMonoRuntime>. AOT e build nativo para WebAssembly continuam dependendo do Mono neste preview.
O switch DOTNET_GCTrimYoungestKeepPercent serve para produção?
É uma opção avançada para cenários em que o footprint de memória é crítico e a aplicação já usa DOTNET_GCLatencyLevel=0. Ajuste o valor com benchmarks da carga real.
As otimizações de span e null check exigem mudança no código?
Não. O JIT aplica essas otimizações automaticamente durante a compilação, então o código existente se beneficia sem alterações.
O createdump com HEAP2 continua compatível com as ferramentas atuais?
Sim, porque o HEAP2 produz um dump semanticamente equivalente por um caminho de enumeração mais eficiente. As ferramentas de diagnóstico existentes continuam funcionando.
Os overloads com nint e nuint exigem hardware específico?
Sim, exigem hardware Arm64 com as instruções correspondentes, como CRC32. Por isso, verifique IsSupported antes de chamar o intrínseco.