Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este artigo descreve novos recursos e melhorias de desempenho no tempo de execução do .NET para .NET 9.
Modelo de atributo para comutadores de recursos com suporte a corte
Dois novos atributos tornam possível definir opções de recursos que as bibliotecas .NET (e você) podem usar para alternar áreas de funcionalidade. Se um recurso não for suportado, os recursos não suportados (e, portanto, não utilizados) serão removidos ao cortar ou compilar com AOT nativo, o que mantém o tamanho do aplicativo menor.
FeatureSwitchDefinitionAttribute é usado para tratar uma propriedade de switch de recurso como uma constante ao cortar, e o código morto protegido pelo switch pode ser removido:
if (Feature.IsSupported) Feature.Implementation(); public class Feature { [FeatureSwitchDefinition("Feature.IsSupported")] internal static bool IsSupported => AppContext.TryGetSwitch("Feature.IsSupported", out bool isEnabled) ? isEnabled : true; internal static void Implementation() => ...; }Quando o aplicativo é cortado com as seguintes configurações de recurso no arquivo de projeto,
Feature.IsSupportedé tratado comofalse, eFeature.Implementationo código é removido.<ItemGroup> <RuntimeHostConfigurationOption Include="Feature.IsSupported" Value="false" Trim="true" /> </ItemGroup>FeatureGuardAttribute é usado para tratar uma propriedade de switch de recurso como um protetor para código anotado com RequiresUnreferencedCodeAttribute, RequiresAssemblyFilesAttributeou RequiresDynamicCodeAttribute. Por exemplo:
if (Feature.IsSupported) Feature.Implementation(); public class Feature { [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] internal static bool IsSupported => RuntimeFeature.IsDynamicCodeSupported; [RequiresDynamicCode("Feature requires dynamic code support.")] internal static void Implementation() => ...; // Uses dynamic code }Quando criada com
<PublishAot>true</PublishAot>o , a chamada paraFeature.Implementation()não produz o aviso do analisador IL3050 eFeature.Implementationo código é removido durante a publicação.
UnsafeAccessorAttribute suporta parâmetros genéricos
O UnsafeAccessorAttribute recurso permite acesso não seguro a membros do tipo que são inacessíveis para o chamador. Esse recurso foi projetado no .NET 8, mas implementado sem suporte para parâmetros genéricos. O .NET 9 adiciona suporte para parâmetros genéricos para CoreCLR e cenários AOT nativos. O código a seguir mostra o exemplo de uso.
using System.Runtime.CompilerServices;
public class Class<T>
{
private T? _field;
private void M<U>(T t, U u) { }
}
class Accessors<V>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field")]
public extern static ref V GetSetPrivateField(Class<V> c);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "M")]
public extern static void CallM<W>(Class<V> c, V v, W w);
}
internal class UnsafeAccessorExample
{
public void AccessGenericType(Class<int> c)
{
ref int f = ref Accessors<int>.GetSetPrivateField(c);
Accessors<int>.CallM<string>(c, 1, string.Empty);
}
}
Recolha de lixo
A adaptação dinâmica a tamanhos de aplicativos (DATAS) agora está habilitada por padrão. Ele visa se adaptar aos requisitos de memória do aplicativo, o que significa que o tamanho da pilha do aplicativo deve ser aproximadamente proporcional ao tamanho dos dados de longa duração. DATA foi introduzido como um recurso de aceitação no .NET 8 e foi significativamente atualizado e melhorado no .NET 9.
Para obter mais informações, consulte Adaptação dinâmica a tamanhos de aplicativos (DATAS).
Tecnologia de aplicação do fluxo de controle
A tecnologia de imposição de fluxo de controle (CET) é habilitada por padrão para aplicativos no Windows. Ele melhora significativamente a segurança, adicionando proteção de pilha imposta por hardware contra explorações de programação orientada ao retorno (ROP). É o mais recente .NET Runtime Security Mitigation.
A CET impõe algumas limitações aos processos habilitados para CET e pode resultar em uma pequena regressão de desempenho. Existem vários controlos para optar por não participar na CET.
Comportamento de pesquisa de instalação do .NET
Os aplicativos .NET agora podem ser configurados para como eles devem procurar o tempo de execução do .NET. Esse recurso pode ser usado com instalações privadas de tempo de execução ou para controlar mais fortemente o ambiente de execução.
Melhorias de desempenho
As seguintes melhorias de desempenho foram feitas para o .NET 9:
- Otimizações de loop
- Melhorias no inlining
- Melhorias no PGO: Verificações de tipo e moldes
- Vetorização Arm64 em bibliotecas .NET
- Geração de código Arm64
- Exceções mais rápidas
- Layout do código
- Exposição reduzida ao endereço
- Suporte AVX10v1
- Geração de código intrínseco de hardware
- Dobragem constante para operações de ponto flutuante e SIMD
- Suporte SVE Arm64
- Alocação de pilha de objetos para caixas
Otimizações de loop
Melhorar a geração de código para loops é uma prioridade para o .NET 9. As seguintes melhorias já estão disponíveis:
- Alargamento da variável de indução
- Endereçamento pós-indexado
- Redução de força
- Direção variável do contador de loops
Observação
O alargamento da variável de indução e o endereçamento pós-indexado são semelhantes: ambos otimizam os acessos à memória com variáveis de índice de loop. No entanto, eles adotam abordagens diferentes, já que o Arm64 oferece uma capacidade de CPU e o x64 não. O alargamento da variável de indução foi implementado para x64 devido a diferenças na capacidade e necessidades de CPU/ISA.
Alargamento da variável de indução
O compilador de 64 bits apresenta uma nova otimização chamada alargamento da variável de indução (IV).
Um IV é uma variável cujo valor muda à medida que o loop de contenção itera. No loop seguinte for , i é um IV: for (int i = 0; i < 10; i++). Se o compilador puder analisar como o valor de um IV evolui ao longo das iterações de seu loop, ele poderá produzir código com mais desempenho para expressões relacionadas.
Considere o seguinte exemplo que itera através de uma matriz:
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
A variável de índice, i, tem 4 bytes de tamanho. No nível de assembly, os registradores de 64 bits são normalmente usados para manter índices de matriz em x64 e, em versões anteriores do .NET, o compilador gerou código que se estendeu i a zero para 8 bytes para o acesso à matriz, mas continuou a ser tratado i como um inteiro de 4 bytes em outro lugar. No entanto, estender i para 8 bytes requer uma instrução adicional em x64. Com o alargamento IV, o compilador JIT de 64 bits agora aumenta i para 8 bytes em todo o loop, omitindo a extensão zero. Looping sobre matrizes é muito comum, e os benefícios desta remoção de instrução rapidamente se somam.
Endereçamento pós-indexado no Arm64
As variáveis de índice são frequentemente usadas para ler regiões sequenciais da memória. Considere o loop idiomática for :
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
Para cada iteração do loop, a variável i de índice é usada para ler um inteiro em e, em numsseguida i , é incrementada. Na montagem do Arm64, essas duas operações têm a seguinte aparência:
ldr w0, [x1]
add x1, x1, #4
ldr w0, [x1] carrega o inteiro no endereço de memória em x1w0; isso corresponde ao acesso de nums[i] no código-fonte. Em seguida, add x1, x1, #4 aumenta o endereço em x1 quatro bytes (o tamanho de um inteiro), movendo-se para o próximo inteiro em nums. Esta instrução corresponde à i++ operação executada no final de cada iteração.
O Arm64 suporta endereçamento pós-indexado, onde o registro "índice" é incrementado automaticamente depois que seu endereço é usado. Isso significa que duas instruções podem ser combinadas em uma, tornando o loop mais eficiente. A CPU só precisa decodificar uma instrução em vez de duas, e o código do loop agora é mais amigável ao cache.
Veja como é o assembly atualizado:
ldr w0, [x1], #0x04
O #0x04 no final significa que o endereço in x1 é incrementado em quatro bytes depois de ser usado para carregar um inteiro em w0. O compilador de 64 bits agora usa endereçamento pós-indexado ao gerar código Arm64.
Redução de força
A redução de força é uma otimização do compilador onde uma operação é substituída por uma operação mais rápida e logicamente equivalente. Esta técnica é especialmente útil para otimizar loops. Considere o loop idiomática for :
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
O seguinte código de assembly x64 mostra um trecho do código gerado para o corpo do loop:
add ecx, dword ptr [rax+4*rdx+0x10]
inc edx
Estas instruções correspondem às expressões sum += nums[i] e i++, respectivamente.
rcx
ecx( contém os 32 bits inferiores deste registo) contém o valor de sum, rax contém o endereço base de nums, e rdx contém o valor de i. Para calcular o endereço de nums[i], o índice em rdx é multiplicado por quatro (o tamanho de um inteiro). Esse deslocamento é então adicionado ao endereço base no rax, além de algum preenchimento. (Depois que o inteiro em nums[i] é lido, ele é adicionado e rcx o índice em rdx é incrementado.) Em outras palavras, cada acesso ao array requer uma multiplicação e uma operação de adição.
A multiplicação é mais cara do que a adição, e substituir a primeira pela segunda é uma motivação clássica para a redução da força. Para evitar o cálculo do endereço do elemento em cada acesso à memória, você pode reescrever o exemplo para acessar os inteiros usando nums um ponteiro em vez de uma variável de índice:
static int Sum2(Span<int> nums)
{
int sum = 0;
ref int p = ref MemoryMarshal.GetReference(nums);
ref int end = ref Unsafe.Add(ref p, nums.Length);
while (Unsafe.IsAddressLessThan(ref p, ref end))
{
sum += p;
p = ref Unsafe.Add(ref p, 1);
}
return sum;
}
O código-fonte é mais complicado, mas é logicamente equivalente à implementação inicial. Além disso, a montagem parece melhor:
add ecx, dword ptr [rdx]
add rdx, 4
rcx
ecx( contém os 32 bits inferiores deste registo) ainda detém o valor de , mas sum agora detém o endereço apontado rdxpor p, pelo que aceder aos elementos em nums apenas exige que desreferenciamosrdx. Toda a multiplicação e adição do primeiro exemplo foi substituída por uma única add instrução para mover o ponteiro para a frente.
No .NET 9, o compilador JIT transforma automaticamente o primeiro padrão de indexação no segundo sem exigir que você reescreva nenhum código.
Direção variável do contador de loops
O compilador de 64 bits agora reconhece quando a variável de contador de um loop é usada apenas para controlar o número de iterações e transforma o loop em contagem regressiva em vez de para cima.
No padrão idiomático for (int i = ...) , a variável contadora normalmente aumenta. Considere o seguinte exemplo:
for (int i = 0; i < 100; i++)
{
DoSomething();
}
No entanto, em muitas arquiteturas, é mais eficiente diminuir o contador do loop, assim:
for (int i = 100; i > 0; i--)
{
DoSomething();
}
Para o primeiro exemplo, o compilador precisa emitir uma instrução para incrementar i, seguida por uma instrução para executar a i < 100 comparação, seguida por um salto condicional para continuar o loop se a condição ainda trueestiver —são três instruções no total. No entanto, se a direção do contador for invertida, é necessária uma instrução a menos. Por exemplo, em x64, o compilador pode usar a dec instrução para diminuir i, quando i atinge zero, a dec instrução define um sinalizador de CPU que pode ser usado como condição para uma instrução de salto imediatamente após o dec.
A redução do tamanho do código é pequena, mas se o loop for executado para um número não trivial de iterações, a melhoria de desempenho pode ser significativa.
Melhorias no inlining
Um dos . Os objetivos da NET para o inliner do compilador JIT é remover o maior número possível de restrições que impedem que um método seja embutido. O .NET 9 permite a integração de:
Genéricos partilhados que exigem consultas em tempo de execução.
Como exemplo, considere os seguintes métodos:
static bool Test<T>() => Callee<T>(); static bool Callee<T>() => typeof(T) == typeof(int);Quando
Té um tipo de referência comostring, o tempo de execução cria genéricos compartilhados, que são instanciações especiais de eTestque são compartilhados por todos os tipos deCalleetipoTref. Para fazer isso funcionar, o tempo de execução cria dicionários que mapeiam tipos genéricos para tipos internos. Estes dicionários são especializados por tipo genérico (ou por método genérico), e são acedidos em tempo de execução para obter informações sobreTe tipos que dependem deT. Historicamente, o código compilado just-in-time só era capaz de realizar estas consultas em tempo de execução contra o dicionário do método raiz. Isso significava que o compilador JIT não podia ser embutidoCalleeemTest—não havia como o código embutido acessarCalleeo dicionário adequado, mesmo que ambos os métodos fossem instanciados sobre o mesmo tipo.O .NET 9 levantou esta restrição ao permitir livremente consultas de tipos em tempo de execução em callees, o que significa que o compilador JIT pode agora aplicar métodos em linha como
CalleeemTest.Suponhamos que chamamos
Test<string>outro método. No pseudocódigo, o inlining tem esta aparência:static bool Test<string>() => typeof(string) == typeof(int);Essa verificação de tipo pode ser calculada durante a compilação, de modo que o código final tem esta aparência:
static bool Test<string>() => false;Melhorias no inliner do compilador JIT podem ter efeitos compostos em outras decisões de inline, resultando em ganhos significativos de desempenho. Por exemplo, a decisão de inline
Calleepode permitir que a chamadaTest<string>também seja embutida, e assim por diante. Isto produziu centenas de melhorias nos parâmetros de referência, com pelo menos 80 valores de referência a melhorarem em 10% ou mais.Acesso a estáticas thread-local no Windows x64, Linux x64 e Linux Arm64.
Para
staticos membros da classe, existe exatamente uma instância do membro em todas as instâncias da classe, que "compartilham" o membro. Se o valor de umstaticmembro for exclusivo para cada thread, tornar esse valor thread-local pode melhorar o desempenho, pois elimina a necessidade de uma primitiva de simultaneidade para acessar com segurança ostaticmembro a partir de seu thread de contenção.Anteriormente, os acessos a estáticas locais de thread em programas compilados por AOT nativo exigiam que o compilador emitisse uma chamada para o tempo de execução para obter o endereço base do armazenamento local de thread. Agora, o compilador pode inserir essas chamadas, resultando em muito menos instruções para acessar esses dados.
Melhorias no PGO: Verificações de tipo e moldes
O .NET 8 habilitou a otimização guiada por perfil dinâmico (PGO) por padrão. NET 9 expande a implementação PGO do compilador JIT para criar perfis de mais padrões de código. Quando a compilação em camadas está habilitada, o compilador JIT já insere instrumentação em seu programa para criar o perfil de seu comportamento. Quando recompila com otimizações, o compilador aproveita o perfil que construiu em tempo de execução para tomar decisões específicas para a execução atual do seu programa. No .NET 9, o compilador JIT usa dados PGO para melhorar o desempenho de verificações de tipo.
Determinar o tipo de um objeto requer uma chamada para o tempo de execução, que vem com uma penalidade de desempenho. Quando o tipo de objeto precisa ser verificado, o compilador JIT emite essa chamada por uma questão de correção (os compiladores geralmente não podem descartar quaisquer possibilidades, mesmo que pareçam improváveis). No entanto, se os dados PGO sugerirem que um objeto provavelmente será um tipo específico, o compilador JIT agora emite um caminho rápido que verifica esse tipo de forma barata e recorre ao caminho lento de chamar para o tempo de execução somente se necessário.
Vetorização Arm64 em bibliotecas .NET
Uma nova EncodeToUtf8 implementação aproveita a capacidade do compilador JIT de emitir instruções de carregamento/armazenamento de vários registros no Arm64. Esse comportamento permite que os programas processem partes maiores de dados com menos instruções. Os aplicativos .NET em vários domínios devem ver melhorias na taxa de transferência no hardware Arm64 que oferece suporte a esses recursos. Alguns índices de referência reduzem o seu tempo de execução em mais de metade.
Geração de código Arm64
O compilador JIT já tem a capacidade de transformar sua representação de cargas contíguas para usar a ldp instrução (para valores de carregamento) no Arm64. O .NET 9 amplia essa capacidade de armazenar operações.
A str instrução armazena dados de um único registro para a memória, enquanto a instrução armazena stp dados de um par de registros. Usar stp em vez de meios a mesma tarefa pode ser realizada com menos operações de armazenamento, o que melhora o tempo de str execução. Cortar uma instrução pode parecer uma pequena melhoria, mas se o código for executado em um loop para um número não trivial de iterações, os ganhos de desempenho podem ser adicionados rapidamente.
Por exemplo, considere o seguinte trecho:
class Body { public double x, y, z, vx, vy, vz, mass; }
static void Advance(double dt, Body[] bodies)
{
foreach (Body b in bodies)
{
b.x += dt * b.vx;
b.y += dt * b.vy;
b.z += dt * b.vz;
}
}
Os valores de b.x, b.ye b.z são atualizados no corpo do loop. No nível de montagem, cada membro pode ser armazenado com uma str instrução, ou usando stp, duas das lojas (b.x e b.y, ou b.yb.ze , porque esses pares são contíguos na memória) podem ser manipulados com uma instrução. Para usar a stp instrução para armazenar para b.x e b.y simultaneamente, o compilador também precisa determinar que os cálculos b.x + (dt * b.vx) e b.y + (dt * b.vy) são independentes uns dos outros e podem ser executados antes de armazenar para b.x e b.y.
Exceções mais rápidas
O tempo de execução do CoreCLR adotou uma nova abordagem de tratamento de exceções que melhora o desempenho do tratamento de exceções. A nova implementação é baseada no modelo de tratamento de exceções do tempo de execução do NativeAOT. A alteração remove o suporte para Windows structured exception handling (SEH) e sua emulação no Unix. A nova abordagem é suportada em todos os ambientes, exceto no Windows x86 (32 bits).
A nova implementação de manipulação de exceções é 2 a 4 vezes mais rápida, por alguns micro-benchmarks de manipulação de exceções. As seguintes melhorias de perf foram medidas no laboratório de perf:
- Windows x64: https://github.com/dotnet/perf-autofiling-issues/issues/32280
- Windows Arm64: https://github.com/dotnet/perf-autofiling-issues/issues/32016
- Linux x64: https://github.com/dotnet/perf-autofiling-issues/issues/31367
- Linux Arm64: https://github.com/dotnet/perf-autofiling-issues/issues/31631
A nova implementação está habilitada por padrão. No entanto, se você precisar voltar para o comportamento de tratamento de exceção herdado, poderá fazer isso de uma das seguintes maneiras:
- Definido
System.Runtime.LegacyExceptionHandlingcomotruenoruntimeconfig.jsonarquivo. - Defina a
DOTNET_LegacyExceptionHandlingvariável de ambiente como1.
Layout do código
Os compiladores normalmente raciocinam sobre o fluxo de controle de um programa usando blocos básicos, onde cada bloco é um pedaço de código que só pode ser inserido na primeira instrução e saído através da última instrução. A ordem dos blocos básicos é importante. Se um bloco termina com uma instrução de ramificação, o fluxo de controle é transferido para outro bloco. Um objetivo da reordenação de bloco é reduzir o número de instruções de ramificação no código gerado, maximizando o comportamento de queda . Se cada bloco básico for seguido por seu sucessor mais provável, ele pode "cair" em seu sucessor sem precisar de um salto.
Até recentemente, a reordenação de blocos no compilador JIT era limitada pela implementação do fluxograma. No .NET 9, o algoritmo de reordenação de blocos do compilador JIT foi substituído por uma abordagem mais simples e global. As estruturas de dados do fluxograma foram refatoradas para:
- Remova algumas restrições em torno do pedido de bloco.
- Probabilidade de execução enraizada em cada mudança de fluxo de controle entre blocos.
Além disso, os dados de perfil são propagados e mantidos à medida que o fluxograma do método é transformado.
Exposição reduzida ao endereço
No .NET 9, o compilador JIT pode controlar melhor o uso de endereços variáveis locais e evitar a exposição desnecessária de endereços.
Quando o endereço de uma variável local é usado, o compilador JIT deve tomar precauções extras ao otimizar o método. Por exemplo, suponha que o compilador esteja otimizando um método que passa o endereço de uma variável local em uma chamada para outro método. Como o destinatário pode usar o endereço para acessar a variável local, para manter a correção, o compilador evita transformar a variável. Locais expostos a endereços podem inibir significativamente o potencial de otimização do compilador.
Suporte AVX10v1
Novas APIs foram adicionadas para o AVX10, que é um novo conjunto de instruções SIMD da Intel. Você pode acelerar seus aplicativos .NET em hardware habilitado para AVX10 com operações vetorizadas usando as novas Avx10v1 APIs.
Geração de código intrínseco de hardware
Muitas APIs intrínsecas de hardware esperam que os usuários passem valores constantes para determinados parâmetros. Essas constantes são codificadas diretamente na instrução subjacente do intrínseco, em vez de serem carregadas em registros ou acessadas a partir da memória. Se uma constante não for fornecida, o intrínseco será substituído por uma chamada para uma implementação de fallback funcionalmente equivalente, mas mais lenta.
Considere o seguinte exemplo:
static byte Test1()
{
Vector128<byte> v = Vector128<byte>.Zero;
const byte size = 1;
v = Sse2.ShiftRightLogical128BitLane(v, size);
return Sse41.Extract(v, 0);
}
O uso de size in the call to Sse2.ShiftRightLogical128BitLane pode ser substituído pela constante 1 e, em circunstâncias normais, o compilador JIT já é capaz dessa otimização de substituição. Mas ao determinar se deve gerar o código acelerado ou de fallback para Sse2.ShiftRightLogical128BitLane, o compilador deteta que uma variável está sendo passada em vez de uma constante e decide prematuramente não "intrinsificar" a chamada. A partir do .NET 9, o compilador reconhece mais casos como este e substitui o argumento da variável pelo seu valor constante, gerando assim o código acelerado.
Dobragem constante para operações de ponto flutuante e SIMD
O dobramento constante é uma otimização existente no compilador JIT. Folding constante refere-se à substituição de expressões que podem ser calculadas em tempo de compilação pelas constantes que avaliam, eliminando assim os cálculos em tempo de execução. O .NET 9 adiciona novos recursos de dobramento constante:
- Para operações binárias de ponto flutuante, onde um dos operandos é uma constante:
-
x + NaNagora está dobrado emNaN. -
x * 1.0agora está dobrado emx. -
x + -0agora está dobrado emx.
-
- Para intrínsecos de hardware. Por exemplo, supondo
xque é umVector<T>:-
x + Vector<T>.Zeroagora está dobrado emx. -
x & Vector<T>.Zeroagora está dobrado emVector<T>.Zero. -
x & Vector<T>.AllBitsSetagora está dobrado emx.
-
Suporte SVE Arm64
O .NET 9 introduz suporte experimental para a SVE (Scalable Vetor Extension), um conjunto de instruções SIMD para CPUs ARM64. O .NET já suportava o conjunto de instruções NEON, portanto, em hardware compatível com NEON, seus aplicativos podem aproveitar registradores vetoriais de 128 bits. O SVE suporta comprimentos vetoriais flexíveis até 2048 bits, desbloqueando mais processamento de dados por instrução. No .NET 9, Vector<T> tem 128 bits de largura ao direcionar SVE, e o trabalho futuro permitirá o dimensionamento de sua largura para corresponder ao tamanho do registro vetorial da máquina de destino. Você pode acelerar seus aplicativos .NET em hardware compatível com SVE usando as novas System.Runtime.Intrinsics.Arm.Sve APIs.
Observação
O suporte a SVE no .NET 9 é experimental. As APIs abaixo System.Runtime.Intrinsics.Arm.Sve estão marcadas com ExperimentalAttribute, o que significa que estão sujeitas a alterações em versões futuras. Além disso, a revisão do depurador e os pontos de interrupção por meio do código gerado por SVE podem não funcionar corretamente, resultando em falhas no aplicativo ou corrupção de dados.
Alocação de pilha de objetos para caixas
Os tipos de valor, como int e struct, normalmente são alocados na pilha em vez da pilha. No entanto, para habilitar vários padrões de código, eles são frequentemente "encaixotados" em objetos.
Considere o seguinte trecho:
static bool Compare(object? x, object? y)
{
if ((x == null) || (y == null))
{
return x == y;
}
return x.Equals(y);
}
public static int RunIt()
{
bool result = Compare(3, 4);
return result ? 0 : 100;
}
Compare é convenientemente escrito de tal forma que, se você quisesse comparar outros tipos, como cadeias de caracteres ou double valores, poderia reutilizar a mesma implementação. Mas, neste exemplo, ele também tem a desvantagem de desempenho de exigir que todos os tipos de valor que são passados para ele sejam encaixotados.
O código de assembly x64 gerado para RunIt é o seguinte:
push rbx
sub rsp, 32
mov rcx, 0x7FFB9F8074D0 ; System.Int32
call CORINFO_HELP_NEWSFAST
mov rbx, rax
mov dword ptr [rbx+0x08], 3
mov rcx, 0x7FFB9F8074D0 ; System.Int32
call CORINFO_HELP_NEWSFAST
mov dword ptr [rax+0x08], 4
add rbx, 8
mov ecx, dword ptr [rbx]
cmp ecx, dword ptr [rax+0x08]
sete al
movzx rax, al
xor ecx, ecx
mov edx, 100
test eax, eax
mov eax, edx
cmovne eax, ecx
add rsp, 32
pop rbx
ret
As chamadas para CORINFO_HELP_NEWSFAST são as alocações de heap para os argumentos inteiros em caixa. Além disso, observe que não há nenhuma chamada para Compare, o compilador decidiu inseri-lo em RunIt. Este inlining significa que as caixas nunca "escapam". Em outras palavras, ao longo da execução do , ele conhece Compare e x são realmente inteiros, e eles podem ser desencaixotados com segurança sem afetar a lógica de ycomparação.
A partir do .NET 9, o compilador de 64 bits aloca caixas sem escape na pilha, o que desbloqueia várias outras otimizações. Neste exemplo, o compilador agora omite as alocações de heap, mas como ele sabe x e y são 3 e 4, ele também pode omitir o corpo de ; o compilador pode determinar Compare que é falso em tempo de x.Equals(y)compilação, então RunIt deve sempre retornar 100. Aqui está o assembly atualizado:
mov eax, 100
ret