Start Debugging

RyuJIT poda mais bounds checks no .NET 11 Preview 3: index-from-end e i + constante

.NET 11 Preview 3 ensina ao RyuJIT a eliminar bounds checks redundantes em acessos consecutivos index-from-end e em padrões i + constante < length, cortando pressão de branches em loops apertados.

A eliminação de bounds check é a otimização do JIT que decide silenciosamente quão rápido muito código .NET é. Todo array[i] e span[i] em código managed carrega um compare-and-branch implícito, e quando o RyuJIT consegue provar que o índice está no range, esse branch some. O .NET 11 Preview 3 estende essa prova a dois padrões comuns que antes pagavam o check mesmo assim.

As duas mudanças estão documentadas nas release notes do runtime e aparecem em destaque no anúncio do .NET 11 Preview 3 de 14 de abril de 2026.

Acesso back-to-back index-from-end

O operador index-from-end ^1, ^2, introduzido com C# 8, é syntactic sugar pra Length - 1, Length - 2. O JIT já conseguia elidir o bounds check no primeiro acesso há tempos, mas um segundo acesso logo depois era frequentemente tratado de forma independente e forçava um compare-and-branch redundante.

No .NET 11 Preview 3 a análise de range reutiliza a prova de length entre acessos consecutivos index-from-end:

static int TailSum(int[] values)
{
    // .NET 10: two bounds checks, one per access.
    // .NET 11 Preview 3: the JIT proves both are in range from a single length test.
    return values[^1] + values[^2];
}

Se você desassemblar TailSum no ASM viewer do Rider 2026.1, dá pra ver o segundo par cmp/ja simplesmente sumir. Código que caminha pela cauda de um buffer, accessors de ring-buffer, parsers que espiam o último token, ou comparadores de janela fixa, todos se beneficiam sem mudança de fonte.

Loops i + constante < length

A segunda melhoria mira um padrão que aparece o tempo todo em código numérico e de parsing. Um loop stride-2 parecia bem no papel mas ainda pagava um bounds check no segundo acesso:

static int SumPairs(ReadOnlySpan<int> buffer)
{
    int sum = 0;
    for (int i = 0; i + 1 < buffer.Length; i += 2)
    {
        // buffer[i] is trivially safe, but buffer[i + 1] used to
        // get its own bounds check, even though the loop condition
        // already proved it.
        sum += buffer[i] + buffer[i + 1];
    }
    return sum;
}

A condição do loop i + 1 < buffer.Length já prova que buffer[i + 1] está no range, mas o RyuJIT costumava tratar os dois acessos como independentes. Preview 3 ensina a análise a raciocinar sobre um índice mais uma constante pequena contra um length, então tanto buffer[i] quanto buffer[i + 1] compilam pra um load simples.

A mesma reescrita se aplica a i + 2, i + 3, e assim por diante, enquanto o offset constante bater com o que a condição do loop garante. Alargue a condição pra i + 3 < buffer.Length, e um inner loop stride-4 fica bounds-check-free nos quatro acessos.

Por que branches pequenos somam

Um único bounds check custa menos de um nanossegundo em CPUs modernas. A pressão real é de segunda ordem: o slot de branch que consome, as decisões de loop-unrolling que bloqueia, as oportunidades de vetorização que derrota. Quando o RyuJIT prova que um inner loop inteiro é bounds-safe, ele fica livre pra desenrolar mais agressivamente e entregar o bloco ao auto-vetorizador. É aí que uma micro-vitória de 1% no papel vira uma melhora de 10 a 20% num kernel numérico de verdade.

Tentando hoje

Nenhuma das otimizações precisa de feature flag. Rode qualquer SDK .NET 11 Preview 3 e elas entram automaticamente. Seteie DOTNET_JitDisasm=TailSum pra dumpar o código gerado, rode uma vez no .NET 10 e uma no Preview 3, e diff. Se você mantém hot loops em arrays ou spans, especialmente coisas que espiam o fim de um buffer ou caminham com stride fixo, esse é um speedup grátis esperando no Preview 3.

< Voltar