Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Questo articolo descrive le nuove funzionalità e i miglioramenti delle prestazioni nel runtime .NET per .NET 9.
Modello di attributo per i commutatori di funzionalità con supporto di taglio
Due nuovi attributi consentono di definire opzioni di funzionalità che le librerie .NET (ed è possibile usare) per attivare/disattivare le aree di funzionalità. Se una funzionalità non è supportata, le funzionalità non supportate (e quindi inutilizzate) vengono rimosse durante la rimozione o la compilazione con AOT nativo, che mantiene le dimensioni dell'app più piccole.
FeatureSwitchDefinitionAttribute viene usato per considerare una proprietà del commutatore di funzionalità come costante durante il taglio e il codice inattivo sorvegliato dall'opzione può essere rimosso:
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 l'app viene tagliata con le impostazioni di funzionalità seguenti nel file di progetto,
Feature.IsSupportedviene considerata comefalseeFeature.Implementationil codice viene rimosso.<ItemGroup> <RuntimeHostConfigurationOption Include="Feature.IsSupported" Value="false" Trim="true" /> </ItemGroup>FeatureGuardAttribute viene usato per considerare una proprietà del commutatore di funzionalità come protezione per il codice annotato con RequiresUnreferencedCodeAttribute, RequiresAssemblyFilesAttributeo RequiresDynamicCodeAttribute. Per esempio:
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 viene compilato con
<PublishAot>true</PublishAot>, la chiamata aFeature.Implementation()non genera l'avviso dell'analizzatore IL3050 eFeature.Implementationil codice viene rimosso durante la pubblicazione.
UnsafeAccessorAttribute supporta parametri generici
La UnsafeAccessorAttribute funzionalità consente l'accesso non sicuro ai membri di tipo inaccessibili al chiamante. Questa funzionalità è stata progettata in .NET 8, ma implementata senza supporto per i parametri generici. .NET 9 aggiunge il supporto per i parametri generici per gli scenari CoreCLR e AOT nativi. Il codice seguente illustra l'utilizzo di esempio.
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);
}
}
Raccolta dei rifiuti
L'adattamento dinamico alle dimensioni dell'applicazione (DATAS) è ora abilitato per impostazione predefinita. Mira ad adattarsi ai requisiti di memoria dell'applicazione, ovvero le dimensioni dell'heap dell'applicazione devono essere approssimativamente proporzionali alle dimensioni dei dati di lunga durata. DATAS è stato introdotto come funzionalità di consenso esplicito in .NET 8 ed è stato notevolmente aggiornato e migliorato in .NET 9.
Per altre informazioni, vedere Adattamento dinamico alle dimensioni dell'applicazione (DATAS).
Tecnologia di imposizione del flusso di controllo
La tecnologia di imposizione del flusso di controllo èabilitata per impostazione predefinita per le app in Windows. Migliora significativamente la sicurezza aggiungendo la protezione dello stack applicata dall'hardware dagli exploit di programmazione orientata al ritorno. È la più recente mitigazione della sicurezza del runtime .NET.
CET impone alcune limitazioni per i processi abilitati per la cet e può comportare una regressione delle prestazioni ridotta. Ci sono vari controlli per rifiutare esplicitamente la cet.
Comportamento di ricerca per l'installazione di .NET
Le app .NET possono ora essere configurate per la modalità di ricerca del runtime .NET. Questa funzionalità può essere usata con installazioni di runtime private o per controllare maggiormente l'ambiente di esecuzione.
Miglioramenti delle prestazioni
Sono stati apportati i miglioramenti delle prestazioni seguenti per .NET 9:
- Ottimizzazioni dei cicli
- Miglioramenti dell'inlining
- Miglioramenti pgo: controlli dei tipi e cast
- Vettorizzazione arm64 nelle librerie .NET
- Generazione di codice Arm64
- Eccezioni più veloci
- Layout del codice
- Riduzione dell'esposizione degli indirizzi
- Supporto di AVX10v1
- Generazione di codice intrinseco hardware
- Riduzione costante per operazioni a virgola mobile e SIMD
- Supporto SVE arm64
- Allocazione dello stack di oggetti per caselle
Ottimizzazioni dei cicli
Il miglioramento della generazione di codice per i cicli è una priorità per .NET 9. Sono ora disponibili i miglioramenti seguenti:
- Ampliare la variabile di induzione
- Indirizzamento post-indicizzato
- Riduzione della forza
- Direzione della variabile del contatore del ciclo
Annotazioni
L'estensione delle variabili di induzione e l'indirizzamento post-indicizzato sono simili: ottimizzano entrambi gli accessi alla memoria con variabili di indice ciclo. Tuttavia, accettano approcci diversi perché Arm64 offre una funzionalità della CPU e x64 non lo è. L'ampliamento delle variabili di induzione è stato implementato per x64 a causa delle differenze nelle funzionalità e nelle esigenze della CPU/ISA.
Ampliare la variabile di induzione
Il compilatore a 64 bit include una nuova ottimizzazione denominata estensione della variabile di induzione (IV).
Un IV è una variabile il cui valore cambia come iterazione del ciclo contenitore. Nel ciclo seguente for è i un IV: for (int i = 0; i < 10; i++). Se il compilatore può analizzare l'evoluzione del valore di un IV sulle iterazioni del ciclo, può produrre codice più efficiente per le espressioni correlate.
Si consideri l'esempio seguente che scorre una matrice:
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
La variabile di indice, i, è di 4 byte. A livello di assembly, i registri a 64 bit vengono in genere usati per contenere indici di matrice su x64 e nelle versioni precedenti di .NET, il codice generato dal compilatore che viene esteso i a zero a 8 byte per l'accesso alla matrice, ma continua a trattare i come intero a 4 byte altrove. Tuttavia, l'estensione i a 8 byte richiede un'istruzione aggiuntiva su x64. Con l'estensione IV, il compilatore JIT a 64 bit ora si estende i a 8 byte durante il ciclo, omettendo l'estensione zero. Il ciclo sulle matrici è molto comune e i vantaggi di questa rimozione delle istruzioni si sommano rapidamente.
Indirizzamento post-indicizzato su Arm64
Le variabili di indice vengono spesso usate per leggere aree sequenziali di memoria. Si consideri il ciclo idiomatico for :
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
Per ogni iterazione del ciclo, la variabile i di indice viene usata per leggere un numero intero in numse quindi i viene incrementata. Nell'assembly Arm64 queste due operazioni hanno un aspetto simile al seguente:
ldr w0, [x1]
add x1, x1, #4
ldr w0, [x1] carica l'intero in corrispondenza dell'indirizzo di memoria in x1w0. Corrisponde all'accesso di nums[i] nel codice sorgente. Aumenta quindi add x1, x1, #4 l'indirizzo in x1 di quattro byte (la dimensione di un numero intero), passando al numero intero successivo in nums. Questa istruzione corrisponde all'operazione i++ eseguita alla fine di ogni iterazione.
Arm64 supporta l'indirizzamento post-indicizzato, in cui il registro "index" viene incrementato automaticamente dopo l'uso del relativo indirizzo. Ciò significa che due istruzioni possono essere combinate in una, rendendo il ciclo più efficiente. La CPU deve decodificare solo un'istruzione anziché due e il codice del ciclo è ora più semplice da memorizzare nella cache.
Ecco l'aspetto dell'assembly aggiornato:
ldr w0, [x1], #0x04
L'oggetto #0x04 alla fine indica che l'indirizzo in x1 viene incrementato di quattro byte dopo che viene usato per caricare un numero intero in w0. Il compilatore a 64 bit usa ora l'indirizzamento post-indicizzato durante la generazione di codice Arm64.
Riduzione della forza
La riduzione della forza è un'ottimizzazione del compilatore in cui un'operazione viene sostituita con un'operazione più veloce e logicamente equivalente. Questa tecnica è particolarmente utile per ottimizzare i cicli. Si consideri il ciclo idiomatico for :
static int Sum(int[] nums)
{
int sum = 0;
for (int i = 0; i < nums.Length; i++)
{
sum += nums[i];
}
return sum;
}
Il codice assembly x64 seguente mostra un frammento di codice generato per il corpo del ciclo:
add ecx, dword ptr [rax+4*rdx+0x10]
inc edx
Queste istruzioni corrispondono rispettivamente alle espressioni sum += nums[i] e i++.
rcx (ecx contiene i 32 bit inferiori di questo registro) contiene il valore di sum, rax contiene l'indirizzo di base di numse rdx contiene il valore di i. Per calcolare l'indirizzo di nums[i], l'indice in rdx viene moltiplicato per quattro (la dimensione di un numero intero). Questo offset viene quindi aggiunto all'indirizzo di base in rax, più una spaziatura interna. Dopo aver letto l'intero in nums[i] , viene aggiunto a rcx e l'indice in rdx viene incrementato. In altre parole, ogni accesso alla matrice richiede una moltiplicazione e un'operazione di addizione.
La moltiplicazione è più costosa dell'aggiunta e la sostituzione del primo con quest'ultima è una motivazione classica per la riduzione della forza. Per evitare il calcolo dell'indirizzo dell'elemento in ogni accesso alla memoria, è possibile riscrivere l'esempio per accedere ai numeri interi usando nums un puntatore anziché una variabile di indice:
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;
}
Il codice sorgente è più complicato, ma è logicamente equivalente all'implementazione iniziale. Inoltre, l'assembly ha un aspetto migliore:
add ecx, dword ptr [rdx]
add rdx, 4
rcx (ecx contiene i 32 bit inferiori di questo registro) mantiene ancora il valore di sum, ma rdx ora contiene l'indirizzo a cui punta , quindi l'accesso agli pelementi in nums richiede solo di dereferenziare rdx. Tutte le moltiplicazioni e l'addizione del primo esempio sono state sostituite da una singola add istruzione per spostare il puntatore in avanti.
In .NET 9 il compilatore JIT trasforma automaticamente il primo modello di indicizzazione nel secondo senza dover riscrivere alcun codice.
Direzione della variabile del contatore del ciclo
Il compilatore a 64 bit riconosce ora quando la variabile del contatore di un ciclo viene usata solo per controllare il numero di iterazioni e trasforma il ciclo in modo che venga conteggiato anziché verso l'alto.
Nel modello idiomatico for (int i = ...) , la variabile del contatore aumenta in genere. Si consideri l'esempio seguente:
for (int i = 0; i < 100; i++)
{
DoSomething();
}
Tuttavia, in molte architetture, è più efficiente decrementare il contatore del ciclo, come illustrato di seguito:
for (int i = 100; i > 0; i--)
{
DoSomething();
}
Per il primo esempio, il compilatore deve generare un'istruzione per incrementare i, seguita da un'istruzione per eseguire il i < 100 confronto, seguita da un salto condizionale per continuare il ciclo se la condizione è ancora true, ovvero tre istruzioni in totale. Tuttavia, se la direzione del contatore viene capovolta, è necessaria una minore istruzione. Ad esempio, in x64, il compilatore può usare l'istruzione per decrementare dec; quando i raggiunge zero, l'istruzione i imposta un flag CPU che può essere usato come condizione per un'istruzione jump immediatamente successiva a dec.dec
La riduzione delle dimensioni del codice è ridotta, ma se il ciclo viene eseguito per un numero nontriviale di iterazioni, il miglioramento delle prestazioni può essere significativo.
Miglioramenti dell'inlining
Uno di . Gli obiettivi di NET per il compilatore JIT sono rimuovere il maggior numero possibile di restrizioni che impediscono l'inlining di un metodo. .NET 9 abilita l'inlining di:
Generics condivisi che richiedono interrogazioni a tempo di esecuzione.
Si considerino ad esempio i metodi seguenti:
static bool Test<T>() => Callee<T>(); static bool Callee<T>() => typeof(T) == typeof(int);Quando
Tè un tipo riferimento comestring, il runtime crea generics condivisi, ovvero istanze speciali diTesteCalleecondivise da tutti i tipi di riferimentoT. Per eseguire questa operazione, il runtime compila dizionari che eseguono il mapping dei tipi generici ai tipi interni. Questi dizionari sono specializzati per tipo generico (o per metodo generico) e sono accessibili in fase di esecuzione per ottenere informazioni suTe tipi che dipendono daT. Storicamente, il codice compilato just-in-time era in grado di eseguire queste ricerche di runtime solo sul dizionario del metodo radice. Ciò significa che il compilatore JIT non è riuscito a inlineCalleeinTest. Non c'era modo per il codice inlined daCalleeper accedere al dizionario appropriato, anche se entrambi i metodi sono stati creata un'istanza sullo stesso tipo..NET 9 ha revocato questa restrizione abilitando liberamente le ricerche dei tipi di runtime nei chiamati, il che significa che il compilatore JIT può ora inserire metodi come
CalleeinTest.Si supponga di chiamare
Test<string>in un altro metodo. In pseudocodice l'inlining è simile al seguente:static bool Test<string>() => typeof(string) == typeof(int);Il controllo del tipo può essere calcolato durante la compilazione, in modo che il codice finale sia simile al seguente:
static bool Test<string>() => false;I miglioramenti apportati all'inliner del compilatore JIT possono avere effetti composti su altre decisioni di inlining, ottenendo risultati significativi sulle prestazioni. Ad esempio, la decisione di inline potrebbe consentire anche l'inlining
CalleeTest<string>della chiamata e così via. Questo ha prodotto centinaia di miglioramenti del benchmark, con almeno 80 benchmark che migliorano di 10% o più.Accesso agli statici locali del thread in Windows x64, Linux x64 e Linux Arm64.
Per
statici membri della classe, esiste esattamente un'istanza del membro in tutte le istanze della classe , che "condividono" il membro. Se il valore di unstaticmembro è univoco per ogni thread, rendendo tale valore thread-local in grado di migliorare le prestazioni, perché elimina la necessità di una primitiva di concorrenza per accedere in modo sicuro alstaticmembro dal thread contenitore.In precedenza, gli accessi agli statici locali del thread nei programmi compilati da AOT nativo richiedevano al compilatore di generare una chiamata al runtime per ottenere l'indirizzo di base dell'archiviazione locale del thread. A questo punto, il compilatore può inline queste chiamate, ottenendo un minor numero di istruzioni per accedere a questi dati.
Miglioramenti pgo: controlli dei tipi e cast
L'ottimizzazione PGO (Dynamic Profile Guided Optimization) abilitata per .NET 8 è abilitata per impostazione predefinita. NET 9 espande l'implementazione PGO del compilatore JIT per profilare più modelli di codice. Quando la compilazione a livelli è abilitata, il compilatore JIT inserisce già la strumentazione nel programma per profilarne il comportamento. Quando ricompila con le ottimizzazioni, il compilatore sfrutta il profilo compilato in fase di esecuzione per prendere decisioni specifiche per l'esecuzione corrente del programma. In .NET 9 il compilatore JIT usa i dati PGO per migliorare le prestazioni dei controlli dei tipi.
Per determinare il tipo di un oggetto è necessaria una chiamata al runtime, con una riduzione delle prestazioni. Quando è necessario controllare il tipo di un oggetto, il compilatore JIT genera questa chiamata per motivi di correttezza (i compilatori in genere non possono escludere alcuna possibilità, anche se sembrano improbabili). Tuttavia, se i dati PGO suggeriscono che un oggetto sia un tipo specifico, il compilatore JIT genera ora un percorso rapido che controlla a basso costo tale tipo e esegue il fallback sul percorso lento di chiamata nel runtime solo se necessario.
Vettorizzazione arm64 nelle librerie .NET
Una nuova EncodeToUtf8 implementazione sfrutta la capacità del compilatore JIT di generare istruzioni di caricamento/archiviazione multiregistro in Arm64. Questo comportamento consente ai programmi di elaborare blocchi di dati più grandi con un minor numero di istruzioni. Le app .NET in vari domini dovrebbero vedere miglioramenti della velocità effettiva nell'hardware Arm64 che supporta queste funzionalità. Alcuni benchmark tagliano il tempo di esecuzione di più della metà.
Generazione di codice Arm64
Il compilatore JIT ha già la possibilità di trasformare la relativa rappresentazione di caricamenti contigui per usare l'istruzione ldp (per il caricamento dei valori) in Arm64. .NET 9 estende questa possibilità di archiviare le operazioni.
L'istruzione str archivia i dati da un singolo registro alla memoria, mentre l'istruzione stp archivia i dati da una coppia di registri. L'uso stp di invece di significa che la stessa attività può essere eseguita con un minor numero di operazioni di archiviazione, migliorando il tempo di str esecuzione. La rimozione di un'istruzione potrebbe sembrare un piccolo miglioramento, ma se il codice viene eseguito in un ciclo per un numero nontriviale di iterazioni, i miglioramenti delle prestazioni possono essere aggiunti rapidamente.
Si consideri ad esempio il frammento di codice seguente:
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;
}
}
I valori di b.x, b.ye b.z vengono aggiornati nel corpo del ciclo. A livello di assembly, ogni membro può essere archiviato con un'istruzione str oppure usando stp, due degli archivi (b.x e b.yo e b.yb.z, perché queste coppie sono contigue in memoria) possono essere gestite con un'unica istruzione. Per usare l'istruzione stp per archiviare b.x e b.y contemporaneamente, il compilatore deve anche determinare che i calcoli b.x + (dt * b.vx) e sono indipendenti l'uno dall'altro e b.y + (dt * b.vy) possono essere eseguiti prima di archiviare in b.x e b.y.
Eccezioni più veloci
Il runtime CoreCLR ha adottato un nuovo approccio di gestione delle eccezioni che migliora le prestazioni della gestione delle eccezioni. La nuova implementazione si basa sul modello di gestione delle eccezioni del runtime NativeAOT. La modifica rimuove il supporto per la gestione delle eccezioni strutturata di Windows (SEH) e l'emulazione in Unix. Il nuovo approccio è supportato in tutti gli ambienti ad eccezione di Windows x86 (32 bit).
La nuova implementazione di gestione delle eccezioni è 2-4 volte più veloce, per alcune eccezioni che gestiscono micro benchmark. I miglioramenti delle prestazioni seguenti sono stati misurati nel lab delle prestazioni:
- 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
La nuova implementazione è abilitata per impostazione predefinita. Tuttavia, se è necessario tornare al comportamento di gestione delle eccezioni legacy, è possibile farlo in uno dei modi seguenti:
- Impostare su
System.Runtime.LegacyExceptionHandlingtruenelruntimeconfig.jsonfile. - Impostare la
DOTNET_LegacyExceptionHandlingvariabile di ambiente su1.
Layout del codice
I compilatori in genere motivino il flusso di controllo di un programma usando blocchi di base, in cui ogni blocco è un blocco di codice che può essere immesso solo alla prima istruzione e chiuso tramite l'ultima istruzione. L'ordine dei blocchi di base è importante. Se un blocco termina con un'istruzione di ramo, il flusso di controllo viene trasferita a un altro blocco. Uno degli obiettivi del riordinamento dei blocchi consiste nel ridurre il numero di istruzioni di ramo nel codice generato ottimizzando il comportamento di fall-through . Se ogni blocco di base è seguito dal suo successore più probabile, può "cadere in" il suo successore senza bisogno di un salto.
Fino a poco tempo fa, il riordinamento del blocco nel compilatore JIT era limitato dall'implementazione del diagramma di flusso. In .NET 9, l'algoritmo di riordinamento dei blocchi del compilatore JIT è stato sostituito con un approccio più semplice e più globale. Le strutture di dati del flowgraph sono state sottoposti a refactoring per:
- Rimuovere alcune restrizioni relative all'ordinamento dei blocchi.
- Le probabilità di esecuzione restrittive in ogni modifica del flusso di controllo tra blocchi.
Inoltre, i dati del profilo vengono propagati e mantenuti man mano che il diagramma di flusso del metodo viene trasformato.
Riduzione dell'esposizione degli indirizzi
In .NET 9, il compilatore JIT può tenere traccia dell'utilizzo degli indirizzi delle variabili locali ed evitare l'esposizione di indirizzi non necessari.
Quando si usa l'indirizzo di una variabile locale, il compilatore JIT deve adottare precauzioni aggiuntive durante l'ottimizzazione del metodo. Si supponga, ad esempio, che il compilatore stia ottimizzando un metodo che passa l'indirizzo di una variabile locale in una chiamata a un altro metodo. Poiché il chiamato potrebbe usare l'indirizzo per accedere alla variabile locale, per mantenere la correttezza, il compilatore evita di trasformare la variabile. Le variabili locali esposte indirizzate possono inibire significativamente il potenziale di ottimizzazione del compilatore.
Supporto di AVX10v1
Sono state aggiunte nuove API per AVX10, che è un nuovo set di istruzioni SIMD di Intel. È possibile accelerare le applicazioni .NET su hardware abilitato per AVX10 con operazioni vettorializzate usando le nuove Avx10v1 API.
Generazione di codice intrinseco hardware
Molte API intrinseche hardware prevedono che gli utenti passino valori costanti per determinati parametri. Queste costanti vengono codificate direttamente nell'istruzione sottostante dell'oggetto intrinseco, anziché caricate in registri o a cui si accede dalla memoria. Se non viene fornita una costante, l'intrinseco viene sostituito con una chiamata a un'implementazione di fallback equivalente a livello funzionale, ma più lenta.
Si consideri l'esempio seguente:
static byte Test1()
{
Vector128<byte> v = Vector128<byte>.Zero;
const byte size = 1;
v = Sse2.ShiftRightLogical128BitLane(v, size);
return Sse41.Extract(v, 0);
}
L'uso di size nella chiamata a Sse2.ShiftRightLogical128BitLane può essere sostituito con la costante 1 e, in circostanze normali, il compilatore JIT è già in grado di eseguire questa ottimizzazione della sostituzione. Tuttavia, quando si determina se generare il codice accelerato o di fallback per Sse2.ShiftRightLogical128BitLane, il compilatore rileva che una variabile viene passata anziché una costante e prematuramente decide di "intrinsificare" la chiamata. A partire da .NET 9, il compilatore riconosce più casi come questo e sostituisce l'argomento della variabile con il relativo valore costante, generando così il codice accelerato.
Riduzione costante per operazioni a virgola mobile e SIMD
La riduzione costante è un'ottimizzazione esistente nel compilatore JIT. La riduzione costante si riferisce alla sostituzione delle espressioni che possono essere calcolate in fase di compilazione con le costanti che restituiscono, eliminando così i calcoli in fase di esecuzione. .NET 9 aggiunge nuove funzionalità di riduzione costante:
- Per le operazioni binarie a virgola mobile, in cui uno degli operandi è una costante:
-
x + NaNè ora piegato inNaN. -
x * 1.0è ora piegato inx. -
x + -0è ora piegato inx.
-
- Per gli intrinseci hardware. Si supponga
x, ad esempio, che sia :Vector<T>-
x + Vector<T>.Zeroè ora piegato inx. -
x & Vector<T>.Zeroè ora piegato inVector<T>.Zero. -
x & Vector<T>.AllBitsSetè ora piegato inx.
-
Supporto SVE arm64
.NET 9 introduce il supporto sperimentale per l'estensione SVE (Scalable Vector Extension), un set di istruzioni SIMD per LE CPU ARM64. .NET supporta già il set di istruzioni NEON, quindi sull'hardware compatibile con NEON, le applicazioni possono sfruttare i registri vettoriali a 128 bit. SVE supporta lunghezze di vettore flessibili fino a 2048 bit, sbloccando più elaborazione dati per ogni istruzione. In .NET 9, Vector<T> è a 128 bit quando la destinazione è SVE e il lavoro futuro consentirà il ridimensionamento della larghezza in modo che corrisponda alle dimensioni del registro vettoriale del computer di destinazione. È possibile accelerare le applicazioni .NET nell'hardware che supporta SVE usando le nuove System.Runtime.Intrinsics.Arm.Sve API.
Annotazioni
Il supporto SVE in .NET 9 è sperimentale. Le API in System.Runtime.Intrinsics.Arm.Sve sono contrassegnate con ExperimentalAttribute, il che significa che sono soggette a modifiche nelle versioni future. Inoltre, le istruzioni e i punti di interruzione del debugger tramite codice generato da SVE potrebbero non funzionare correttamente, causando arresti anomali dell'applicazione o danneggiamento dei dati.
Allocazione dello stack di oggetti per caselle
I tipi valore, ad esempio int e struct, vengono in genere allocati nello stack anziché nell'heap. Tuttavia, per abilitare vari modelli di codice, vengono spesso "boxed" in oggetti.
Si consideri il frammento di codice seguente:
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 viene scritto in modo pratico in modo che, se si desidera confrontare altri tipi, ad esempio stringhe o double valori, è possibile riutilizzare la stessa implementazione. In questo esempio, tuttavia, presenta anche lo svantaggio delle prestazioni della necessità di qualsiasi tipo valore passato per la boxing.
Il codice assembly x64 generato per RunIt è il seguente:
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
Le chiamate a CORINFO_HELP_NEWSFAST sono le allocazioni dell'heap per gli argomenti integer boxed. Si noti inoltre che non esiste alcuna chiamata a Compare. Il compilatore ha deciso di inlinerlo in RunIt. Questo inlining significa che le caselle non "escape". In altre parole, durante l'esecuzione di Compare, sa x e y sono in realtà numeri interi, e possono essere unboxed in modo sicuro senza influire sulla logica di confronto.
A partire da .NET 9, il compilatore a 64 bit alloca caselle senza caratteri di escape nello stack, che sblocca diverse altre ottimizzazioni. In questo esempio, il compilatore ora omette le allocazioni dell'heap, ma poiché sa x e y sono 3 e 4, può anche omettere il corpo di Compare. Il compilatore può determinare x.Equals(y) che è false in fase di compilazione, quindi RunIt dovrebbe restituire sempre 100. Ecco l'assembly aggiornato:
mov eax, 100
ret