Seu código ficou mais rápido sem você mexer em nada: o salto do JIT no .NET 10

1 mês atrás

7 min de leitura

Sumário Executivo

A evolução do JIT no .NET 10 é uma das melhorias mais relevantes para performance “de graça” na plataforma. Quando o runtime passa a gerar código nativo mais eficiente, aplicações existentes podem capturar ganhos reais apenas com a atualização, reduzindo overhead de abstrações, diminuindo pressão no Garbage Collector e melhorando latência em cenários de alto volume de alocações e iterações.

Este texto descreve (1) os fundamentos que explicam por que stack e heap impactam performance (value types, reference types e GC), (2) como o .NET 10 aplica escape analysis para evitar alocações temporárias em padrões comuns, como arrays e enumerators que não escapam do método, e (3) evidências via benchmark mostrando eliminação de alocações na heap e ganhos expressivos de execução ao iterar coleções via IEnumerable, reforçando o motivo prático para priorizar a atualização de runtime.

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.

Escrito por
Com mais de 10 anos de experiência na área de TI, tem contribuído para projetos de alta escala em startups e grandes empresas. Sua área de atuação inclui integração de sistemas, modernização de sistemas legados e arquitetura de software. Como Tech Lead, Bruno tem liderado equipes em projetos que demandam soluções inovadoras e escaláveis, sempre com foco em entregar sistemas robustos e eficientes. Sua trajetória reflete um compromisso contínuo com o desenvolvimento técnico e a capacidade de enfrentar desafios complexos com eficiência.

Você também pode gostar

Explicando a Arquitetura do OpenClaw, na prática

Sumário Executivo

A transição da interface conversacional para a interface agêntica muda o jogo: em vez de apenas responder, o sistema passa a agir, lembrar, orquestrar e executar, tornando-se infraestrutura operacional e não só uma “IA para conversa”.

Este texto descreve (1) uma arquitetura de referência para AI Agents baseada em três blocos com fluxos claros (Interaction, Core e Resources), (2) como esses blocos ganham forma concreta demonstrando a utilização em uma assistente virtual construída sobre o OpenClaw e (3) como maximizar o resultado preservando salvaguardas práticas importante, cobrindo riscos como prompt injection, data exfiltration e excessive agency, além de e práticas como least privilege, isolamento e human-in-the-loop.

Microfrontends como estratégia arquitetural de modernização

Modernização de frontend legado sem reescrita total utilizando microfrontends como estratégia de arquitetura incremental para migração gradual e convivência com legado.

FinOps e governança de custos para inteligência artificial

Guia prático de FinOps para IA/GenAI: pare de olhar custo de GPU e meça custo por resposta. Entenda como aplicar guardrails para controlar consumo sem perder qualidade.

DDD (Domain-Driven Design) faz sentido no frontend?

Organização do frontend com DDD (Domain Driven Design) ao desenvolver uma aplicação faz sentido? Como estruturar o frontend?

Aplicação Node.js em produção sem telemetria é operar no escuro

Guia prático de observabilidade em Node.js com OpenTelemetry e Grafana: una logs, métricas e traces, comece com auto-instrumentação e evolua para diagnóstico rápido usando OTel Collector, Tempo, Loki e Prometheus com correlação por traceId.

A Importância do Refinamento de Dados para Modelos de IA: Por que algoritmos bilionários continuam falhando com dados de centavos

Recomendações práticas de técnicas de refinamento de dados para garantir resultados precisos em modelos de Inteligência Artificial.

Por que usar mensageria se posso chamar o outro serviço via HTTP?

Quando HTTP síncrono vira o caminho crítico, falhas e latência se propagam em cascata. Veja quando a mensageria deixa de ser opcional, como ela desacopla serviços e absorve picos, e quais disciplinas (idempotência, DLQ e rastreabilidade) evitam colapsos em sistemas distribuídos.

Exemplo completo de implementação de Open Telemetry aplicação Node.js

Exemplo pronto de OpenTelemetry em Node.js para reduzir tempo de investigação e aumentar previsibilidade. Inclui instrumentação, OTel Collector e visualização no Grafana (Tempo/Loki/Prometheus). Ideal para usar como referência e acelerar a adoção no seu time.

Modelo de Governança para tratar itens urgentes

Aprenda a governar a urgência e evitar o colapso da engenharia. Descubra como repriorizações sem critérios destroem a produtividade e o fluxo técnico.

Guia técnico: Comunicação Síncrona ou Assíncrona

HTTP ou Mensageria? Entenda os impactos do acoplamento temporal e saiba quando o modelo síncrono se torna um gargalo para sistemas distribuídos.

O mito do rewrite na modernização de legado

Descubra por que a modernização incremental é mais segura que o rewrite total. Evite armadilhas técnicas e preserve o conhecimento do seu negócio.

Você Não Quer Desenvolvedores Cuidadosos. Você Quer Fly-by-Wire

Substitua a dependência do erro humano pela Engenharia Fly-by-Wire: crie envelopes operacionais para garantir entregas rápidas, seguras e escaláveis.

7 Controles de FinOps que Cortam Gastos na Nuvem: Estratégias AWS e Multicloud

Recomendações das estratégias FinOps mais eficientes para reduzir até 40% dos custos de nuvem em 90 dias, em cenários reais multicloud, com governança e otimização sem sacrificar performance.

Assine nossa newsletter.

Assine nossa newsletter para ficar por dentro de todas as novidades de tecnologia.