O .NET 10 traz uma série de melhorias em comparação ao .NET 9.0, com destaque para avanços no JIT (Just-In-Time Compiler). Toda aplicação .NET depende do JIT para transformar o código intermediário (IL) em instruções nativas executáveis pelo processador.
Por isso, sempre que o JIT evolui, aplicações existentes podem se beneficiar diretamente dessas otimizações, muitas vezes apenas atualizando o runtime, sem necessidade de alterações no código-fonte. Em diversos cenários, essas melhorias reduzem o overhead de abstrações e aumentam a eficiência do código gerado, resultando em ganhos de performance perceptíveis em workloads reais.
Antes, alguns conceitos básicos
Para entendermos o impacto de algumas melhorias no .NET 10, precisamos dar um passo atrás e revisar alguns conceitos fundamentais do C#/.NET que, apesar de básicos, passam despercebidos no dia a dia.
Em C#, os tipos podem ser classificados em value types e reference types. Value types incluem estruturas e tipos primitivos como int, double, float, bool, entre outros. Em muitos cenários, especialmente quando usados como variáveis locais dentro de um método, esses valores ficam armazenados na stack, uma região de memória altamente eficiente, organizada no modelo LIFO (Last In, First Out). Ao final da execução do método, o espaço reservado para essas variáveis é liberado automaticamente.
Já os reference types representam objetos mais complexos, como instâncias de classes, arrays, listas, strings e delegates. Nesse caso, o que normalmente fica armazenado na stack (ou em registradores) é apenas a referência para o objeto. O objeto em si é alocado na heap, uma região de memória gerenciada pelo runtime.
A heap é administrada pelo Garbage Collector (GC), responsável por identificar objetos que não estão mais sendo referenciados e liberar essa memória. Diferente da stack, onde a liberação ocorre de forma previsível e imediata, a coleta na heap ocorre de maneira não determinística, baseada em heurísticas e no comportamento de alocação da aplicação.
Como parte do processo de coleta, o GC pode pausar threads gerenciadas para garantir consistência no acesso à memória. Se essas pausas ocorrerem com frequência ou em momentos críticos, elas podem impactar diretamente a performance e a latência da aplicação, especialmente em sistemas de alta escala ou com alto volume de alocações.
Object Stack Allocation
Agora, com esse conhecimento básico revisitado, conseguimos entender uma das principais melhorias realizadas no .NET 10. Tradicionalmente, arrays são alocados na heap, o que significa que eles eventualmente precisarão ser rastreados e coletados pelo Garbage Collector.
Na prática, é comum criarmos arrays dentro de métodos apenas como estruturas intermediárias, utilizadas por poucos microsegundos para calcular um resultado. Esse padrão é bastante frequente em código moderno, especialmente quando usamos LINQ, helpers ou operações de transformação de dados, o que pode gerar alocações desnecessárias, aumentando a pressão sobre o GC.
Por exemplo:
public double Average()
{
var numbers = new int[] { 1,2,3,4,5,6,7,8,9,10 };
var sum = 0;
foreach (var number in numbers)
{
sum += number;
}
return (double)sum / numbers.Length;
}
O array numbers é utilizado apenas para computar o resultado final do método, ou seja, a média dos valores. Ele não “escapa” do método: não é retornado, nem armazenado em um campo, nem atribuído a nenhuma estrutura que sobreviva além do escopo da execução do método Average.
No .NET 10, uma das melhorias mais relevantes do runtime/JIT é a capacidade de identificar esse tipo de padrão e, em cenários específicos, evitar a alocação na heap. Em particular, o runtime passou a suportar a alocação de pequenos arrays de tipos de referência na stack, desde que eles não escapem do método. Isso reduz o custo de alocação e elimina pressão desnecessária sobre o Garbage Collector, diminuindo a frequência e o impacto das pausas de GC em aplicações com grande volume de alocações temporárias.
A partir do benchmark abaixo, é possível observar que na nova versão do .NET não houve alocação na heap para esse cenário.

Collections
Iterar sobre coleções usando foreach é uma das operações mais comuns em aplicações .NET. Internamente, esse tipo de iteração normalmente é baseado no conceito de enumerator, um mecanismo que permite percorrer uma coleção elemento por elemento através de operações como MoveNext() e Current.
Em C#, o foreach é essencialmente uma sintaxe conveniente que o compilador traduz para chamadas ao enumerator correspondente (geralmente via GetEnumerator). Dependendo do tipo da coleção, isso pode envolver chamadas de interface e outras abstrações que, embora elegantes, podem introduzir overhead em cenários de alta performance.
Considere o exemplo abaixo, que recebe uma lista genérica e calcula a soma de seus elementos:
public int Sum(IEnumerable<int> values)
{
var sum = 0;
foreach(var value in values)
{
sum += value;
}
return sum;
}
Método super simples, certo? Em alto nível, estamos apenas percorrendo a sequência, somando cada elemento e retornando o total. Porém, ao usar foreach sobre um IEnumerable<int>, não estamos “iterando diretamente” sobre a coleção: o compilador traduz esse loop para o uso de um enumerator, responsável por avançar na sequência e expor o item atual.
De forma simplificada, o foreach acima pode ser reescrito (em C# equivalente) mais ou menos assim:
public int Sum(IEnumerable<int> values)
{
int num = 0;
IEnumerator<int> enumerator = values.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int current = enumerator.Current;
num += current;
}
return num;
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
}
Como podemos perceber, o foreach é apenas uma sintaxe conveniente: por baixo dos panos, o compilador traduz esse loop para um padrão baseado em enumerator (GetEnumerator, MoveNext, Current e Dispose). Isso torna a versão “expandida” mais verbosa do que o código original, e também deixa explícito onde existe overhead de abstração, principalmente quando o tipo está exposto como interface (IEnumerable<T>).
Outro detalhe importante é o tempo de vida desse enumerator. No nosso exemplo, ele é criado e consumido inteiramente dentro do método Sum e não é retornado nem armazenado em estruturas que sobrevivam ao escopo do método, ou seja, ele não “escapa”.
É justamente esse tipo de padrão que o .NET 10 passou a explorar melhor. O runtime/JIT recebeu melhorias que reduzem o custo de iteração via interfaces e, em cenários específicos, permitem que o enumerator (inclusive quando ocorre boxing) seja tratado de forma mais eficiente, podendo evitar alocações desnecessárias e até habilitar stack allocation quando as condições de escape analysis são satisfeitas.
Vamos analisar esse comportamento a partir da execução do benchmark abaixo.
[MemoryDiagnoser]
[ShortRunJob(runtimeMoniker: RuntimeMoniker.Net90)]
[ShortRunJob(runtimeMoniker: RuntimeMoniker.Net10_0)]
public class StackObjectAllocation
{
private IEnumerable<int> _enumerable;
[Params(500, 5000, 15000)]
public int Count { get; set; }
[GlobalSetup]
public void Setup() => _enumerable = [.. Enumerable.Range(0, Count)];
[Benchmark]
public int Sum()
{
var sum = 0;
foreach (var value in _enumerable)
{
sum += value;
}
return sum;
}
}
Nesse benchmark, vamos avaliar o comportamento do método Sum usando três tamanhos diferentes de entrada (500, 5.000 e 15.000 itens). A ideia é comparar o custo de iterar um array quando ele é exposto através da abstração IEnumerable<int>.
Além disso, vamos executar o mesmo código em .NET 9.0 e .NET 10.0 para fins comparativos, observando tanto o tempo de execução quanto às métricas de alocação reportadas pelo MemoryDiagnoser. Esse cenário é especialmente relevante porque o .NET 10 introduz otimizações para reduzir o overhead de enumerar arrays via IEnumerable, e em alguns casos o runtime/JIT consegue aplicar escape analysis condicional para evitar alocações associadas ao enumerator.

A partir do benchmark, podemos observar que no .NET 10, independentemente do tamanho da coleção, o método Sum não realizou alocações adicionais na heap durante a iteração (0 B reportados pelo MemoryDiagnoser). Isso indica que o runtime/JIT conseguiu otimizar o uso do enumerator nesse cenário, eliminando alocações temporárias que existiam em versões anteriores.
Além disso, o benchmark também mostra um ganho significativo de performance. Em todos os três cenários testados, o tempo médio de execução no .NET 10 foi aproximadamente 3x menor, indicando uma execução cerca de 3x mais rápida em comparação ao .NET 9.0.
Conclusão
Essa foi apenas uma das melhorias apresentadas na versão .NET 10. No post oficial, é possível explorar outras melhorias realizadas no runtime, no JIT e nas bibliotecas base. Segundo o próprio post oficial, foram cerca de 300 pull requests aplicados no repositório oficial do runtime com foco exclusivo em melhorias de performance.
Esse tipo de evolução contínua reforça um dos pontos mais fortes da plataforma: a cada nova versão do .NET, o desenvolvedor consegue continuar escrevendo código simples, expressivo e em alto nível, enquanto o runtime se torna progressivamente mais eficiente em executar esse código com melhor desempenho e menor custo de recursos.
Vale lembrar que as versões 8.0 e 9.0 do .NET encerram o período de suporte em novembro de 2026. Recomendamos fortemente a atualização para o .NET 10 pelo fato do mesmo ser LTS.