Freigeben über


Neuerungen in der .NET 9-Laufzeit

In diesem Artikel werden neue Features und Leistungsverbesserungen in der .NET-Laufzeit für .NET 9 beschrieben.

Attributmodell für Featureoptionen mit Kürzungsunterstützung

Mit zwei neuen Attributen können Featureoptionen definiert werden, mit denen die .NET-Bibliotheken (und Sie) Bereiche der Funktionalität umschalten können. Wenn ein Feature nicht unterstützt wird, werden die nicht unterstützten (und damit nicht verwendeten) Features beim Kürzen oder Kompilieren mit nativem AOT entfernt, wodurch die App-Größe kleiner bleibt.

  • FeatureSwitchDefinitionAttribute wird verwendet, um eine Feature-Switch-Eigenschaft beim Kürzen als Konstante zu behandeln, und toter Code, der von der Option geschützt wird, kann entfernt werden:

    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() => ...;
    }
    

    Wenn die App mit den folgenden Featureeinstellungen in der Projektdatei gekürzt wird, Feature.IsSupported wird behandelt als false, und Feature.Implementation Code wird entfernt.

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="Feature.IsSupported" Value="false" Trim="true" />
    </ItemGroup>
    
  • FeatureGuardAttribute wird verwendet, um eine Feature-Switch-Eigenschaft als Schutz für Code zu behandeln, der mit RequiresUnreferencedCodeAttribute, RequiresAssemblyFilesAttributeoder RequiresDynamicCodeAttribute. Beispiel:

    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
    }
    

    Bei der Erstellung mit <PublishAot>true</PublishAot>diesem Aufruf erzeugt der Aufruf Feature.Implementation() keine Analysewarnung IL3050, und Feature.Implementation code wird beim Veröffentlichen entfernt.

UnsafeAccessorAttribute unterstützt generische Parameter

Das UnsafeAccessorAttribute Feature ermöglicht den unsicheren Zugriff auf Elemente, auf die der Aufrufer nicht zugreifen kann. Dieses Feature wurde in .NET 8 entwickelt, aber ohne Unterstützung für generische Parameter implementiert. .NET 9 fügt Unterstützung für generische Parameter für CoreCLR- und systemeigene AOT-Szenarien hinzu. Der folgende Code zeigt die Beispielverwendung.

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);
    }
}

Müllabfuhr

Die dynamische Anpassung an Anwendungsgrößen (DATAS) ist jetzt standardmäßig aktiviert. Sie zielt darauf ab, die Anforderungen an den Anwendungsspeicher anzupassen, d. h. die Anwendungshapgröße sollte ungefähr proportional zur langlebigen Datengröße sein. DATAS wurde als Opt-In-Feature in .NET 8 eingeführt und wurde in .NET 9 erheblich aktualisiert und verbessert.

Weitere Informationen finden Sie unter Dynamische Anpassung an Anwendungsgrößen (DATAS).

Steuerungsfluss-Erzwingungstechnologie

Die Steuerungsflusserzwingungstechnologie (CET) ist standardmäßig für Apps unter Windows aktiviert . Dadurch wird die Sicherheit erheblich verbessert, indem hardwaregezwungener Stapelschutz gegen rückgabeorientierte Programmierungs-Exploits (ROP) hinzugefügt wird. Es ist die neueste .NET-Runtime-Sicherheitsminderung.

CET erzwingt einige Einschränkungen für CET-fähige Prozesse und kann zu einer kleinen Leistungsregression führen. Es gibt verschiedene Steuerelemente zum Abmelden von CET.

.NET-Installationssuchverhalten

.NET-Apps können jetzt so konfiguriert werden, wie sie nach der .NET-Laufzeit suchen sollen. Diese Funktion kann mit privaten Laufzeitinstallationen verwendet werden oder um die Ausführungsumgebung stärker zu steuern.

Leistungsverbesserungen

Die folgenden Leistungsverbesserungen wurden für .NET 9 vorgenommen:

Schleifenoptimierungen

Die Verbesserung der Codegenerierung für Schleifen ist eine Priorität für .NET 9. Die folgenden Verbesserungen sind jetzt verfügbar:

Hinweis

Induktionsvariable Verbreiterung und postindizierte Adressierung sind ähnlich: Beide optimieren Speicherzugriffe mit Schleifenindexvariablen. Sie nehmen jedoch unterschiedliche Ansätze, da Arm64 eine CPU-Funktion bietet und x64 nicht. Die Induktionsvariable Verbreiterung wurde für x64 aufgrund von Unterschieden bei CPU/ISA-Funktionen und -Anforderungen implementiert.

Induktionsvariable Verbreiterung

Der 64-Bit-Compiler verfügt über eine neue Optimierung namens Induktionsvariablen (IV).

Ein IV ist eine Variable, deren Wert sich als enthaltende Schleife durchläuft. In der folgenden for Schleife i ist ein IV: for (int i = 0; i < 10; i++). Wenn der Compiler analysieren kann, wie sich der Wert eines IV über die Iterationen der Schleife weiterentwickelt, kann er für verwandte Ausdrücke leistungsfähigeren Code erzeugen.

Betrachten Sie das folgende Beispiel, das ein Array durchläuft:

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

Die Indexvariable ist i4 Byte groß. Auf Assemblyebene werden 64-Bit-Register in der Regel verwendet, um Arrayindizes auf x64 zu speichern, und in früheren .NET-Versionen generierte der Compiler Code, der auf 8 Bytes für den Arrayzugriff erweitert i wurde, aber weiterhin als eine 4-Byte-Ganzzahl an anderer Stelle behandelt i wurde. Für die Erweiterung i auf 8 Byte ist jedoch eine zusätzliche Anweisung für x64 erforderlich. Bei der Erweiterung von IV breitet i sich der 64-Bit-JIT-Compiler nun auf 8 Bytes in der Schleife aus, wobei die Nullerweiterung weggelassen wird. Das Durchlaufen von Arrays ist sehr häufig, und die Vorteile dieser Anweisungsentfernung werden schnell addiert.

Postindizierte Adressierung auf Arm64

Indexvariablen werden häufig zum Lesen sequenzieller Speicherbereiche verwendet. Betrachten Sie die idiomatische for Schleife:

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

Für jede Iteration der Schleife wird die Indexvariable i verwendet, um eine ganze Zahl in numszu lesen und dann i inkrementiert. In der Arm64-Assembly sehen diese beiden Vorgänge wie folgt aus:

ldr w0, [x1]
add x1, x1, #4

ldr w0, [x1] lädt die ganze Zahl an der Speicheradresse in x1w0; dies entspricht dem Zugriff nums[i] im Quellcode. Erhöht dann add x1, x1, #4 die Adresse um x1 vier Byte (die Größe einer ganzen Zahl), und wechselt zur nächsten ganzzahligen Zahl in nums. Diese Anweisung entspricht dem Vorgang, der i++ am Ende jeder Iteration ausgeführt wird.

Arm64 unterstützt postindizierte Adressierung, bei der das Indexregister nach der Verwendung seiner Adresse automatisch erhöht wird. Dies bedeutet, dass zwei Anweisungen in einer kombiniert werden können, wodurch die Schleife effizienter wird. Die CPU muss nur eine Anweisung anstelle von zwei decodieren, und der Code der Schleife ist jetzt mehr cachefreundlicher.

So sieht die aktualisierte Assembly aus:

ldr w0, [x1], #0x04

Die #0x04 Adresse am Ende bedeutet, dass die Adresse x1 um vier Byte erhöht wird, nachdem sie zum Laden einer ganzen Zahl verwendet w0wurde. Der 64-Bit-Compiler verwendet jetzt postindizierte Adressierung beim Generieren von Arm64-Code.

Festigkeitsreduktion

Die Festigkeitsreduzierung ist eine Compileroptimierung, bei der ein Vorgang durch einen schnelleren, logisch gleichwertigen Vorgang ersetzt wird. Diese Technik ist besonders nützlich für die Optimierung von Schleifen. Betrachten Sie die idiomatische for Schleife:

static int Sum(int[] nums)
{
    int sum = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        sum += nums[i];
    }

    return sum;
}

Der folgende x64-Assemblycode zeigt einen Codeausschnitt des Codes, der für den Textkörper der Schleife generiert wird:

add ecx, dword ptr [rax+4*rdx+0x10]
inc edx

Diese Anweisungen entsprechen den Ausdrücken sum += nums[i] bzw i++. rcx (ecx enthält die unteren 32 Bits dieses Registers) enthält den Wert von sum, rax enthält die Basisadresse von nums, und rdx enthält den Wert von i. Um die Adresse zu nums[i]berechnen, wird der Index in rdx mit vier multipliziert (die Größe einer ganzen Zahl). Dieser Offset wird dann der Basisadresse hinzugefügt , raxsowie einige Abstände. (Nachdem die ganze Zahl gelesen nums[i] wurde, wird sie hinzugefügt rcx und der Index rdx inkrementiert.) Mit anderen Worten, jeder Arrayzugriff erfordert eine Multiplikation und einen Additionsvorgang.

Multiplikation ist teurer als Addition, und das Ersetzen des ehemaligen durch letzteres ist eine klassische Motivation für die Kraftreduktion. Um die Berechnung der Adresse des Elements für jeden Speicherzugriff zu vermeiden, können Sie das Beispiel neu schreiben, um auf die ganzzahlen zuzugreifen, indem nums Sie einen Zeiger anstelle einer Indexvariable verwenden:

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;
}

Der Quellcode ist komplizierter, entspricht aber logisch der ursprünglichen Implementierung. Außerdem sieht die Assembly besser aus:

add ecx, dword ptr [rdx]
add rdx, 4

rcx (ecx enthält die unteren 32 Bits dieses Registers) enthält weiterhin den Wert von sum, enthält aber rdx jetzt die Adresse, auf die pverwiesen wird, so dass der Zugriff auf Elemente in nums nur erfordert, rdxdass wir ableiten. Alle Multiplikationen und Ergänzungen aus dem ersten Beispiel wurden durch eine einzelne add Anweisung ersetzt, um den Zeiger vorwärts zu bewegen.

In .NET 9 wandelt der JIT-Compiler automatisch das erste Indizierungsmuster in die zweite um, ohne dass Sie Code neu schreiben müssen.

Schleifenzählervariable Richtung

Der 64-Bit-Compiler erkennt jetzt, wenn die Zählervariable einer Schleife nur verwendet wird, um die Anzahl der Iterationen zu steuern, und transformiert die Schleife so, dass sie anstelle von oben nach unten gezählt wird.

Im idiomatischen for (int i = ...) Muster erhöht sich die Zählervariable in der Regel. Betrachten Sie das folgenden Beispiel:

for (int i = 0; i < 100; i++)
{
    DoSomething();
}

Bei vielen Architekturen ist es jedoch leistungsfähiger, den Zähler der Schleife wie folgt zu erhöhen:

for (int i = 100; i > 0; i--)
{
    DoSomething();
}

Im ersten Beispiel muss der Compiler eine Anweisung zum Inkrement iausgeben, gefolgt von einer Anweisung zum Ausführen des i < 100 Vergleichs, gefolgt von einem bedingten Sprung, um die Schleife fortzusetzen, wenn die Bedingung noch truebesteht – das sind drei Anweisungen insgesamt. Wenn die Richtung des Zählers jedoch gekippt wird, ist eine weniger Anweisung erforderlich. Bei x64 kann der Compiler beispielsweise die dec Anweisung verwenden, um die Dekrementierung izu erhöhen; wenn i die Anweisung null erreicht, legt die dec Anweisung ein CPU-Flag fest, das als Bedingung für eine Sprunganweisung verwendet werden kann, die unmittelbar auf die .dec

Die Codegrößenreduzierung ist klein, aber wenn die Schleife für eine nichttrivielle Anzahl von Iterationen ausgeführt wird, kann die Leistungsverbesserung erheblich sein.

Verbesserungen beim Inlining

Einer von . Nets Ziele für den Inliner des JIT-Compilers besteht darin, so viele Einschränkungen zu entfernen, die verhindern, dass eine Methode so inlineiert wird wie möglich. .NET 9 ermöglicht die Inlineierung von:

  • Gemeinsame Generika, die Laufzeit-Lookups erfordern.

    Betrachten Sie beispielsweise die folgenden Methoden:

    static bool Test<T>() => Callee<T>();
    static bool Callee<T>() => typeof(T) == typeof(int);
    

    Wenn T es sich um einen Verweistyp handelt, stringerstellt die Laufzeit freigegebene Generische, die spezielle Instanziierungen sind Test und Callee die von allen Reftyptypen T gemeinsam verwendet werden. Damit dies funktioniert, erstellt die Laufzeit Wörterbücher, die generische Typen internen Typen zuordnen. Diese Wörterbücher sind je generischem Typ (oder je generischer Methode) spezialisiert und werden zur Laufzeit aufgerufen, um Informationen zu T und Typen abzurufen, die von T abhängig sind. Historisch gesehen konnte just-in-time kompilierten Code nur Laufzeitanfragen für das Wörterbuch der Stammmethode ausführen. Dies bedeutete, dass der JIT-Compiler nicht inline inline CalleeTestkonnte – es gab keine Möglichkeit für den inlineierten Code, Callee auf das richtige Wörterbuch zuzugreifen, obwohl beide Methoden über denselben Typ instanziiert wurden.

    .NET 9 hat diese Einschränkung aufgehoben, indem Laufzeittypsuchvorgänge bei den aufgerufenen uneingeschränkt aktiviert werden, was bedeutet, dass der JIT-Compiler jetzt inline-Methoden wie Callee in Test aufnehmen kann.

    Angenommen, wir rufen eine andere Methode auf Test<string> . In Pseudocode sieht die Inlinierung wie folgt aus:

    static bool Test<string>() => typeof(string) == typeof(int);
    

    Diese Typüberprüfung kann während der Kompilierung berechnet werden, sodass der endgültige Code wie folgt aussieht:

    static bool Test<string>() => false;
    

    Verbesserungen am Inliner des JIT-Compilers können zusammengesetzte Auswirkungen auf andere Inliningentscheidungen haben, was zu erheblichen Leistungsgewinnen führt. Die Entscheidung für inline kann z. B. das Inlineieren Callee des Anrufs Test<string> aktivieren usw. Dies erzeugte Hunderte von Benchmark-Verbesserungen , wobei mindestens 80 Benchmarks um 10% oder mehr verbessert wurden.

  • Greift auf threadlokale Statische unter Windows x64, Linux x64 und Linux Arm64 zu.

    Für static Klassenmember ist genau eine Instanz des Mitglieds in allen Instanzen der Klasse vorhanden, die das Element "teilen". Wenn der Wert eines static Elements für jeden Thread eindeutig ist, kann dieser Wert threadlokal die Leistung verbessern, da dadurch die Notwendigkeit eines Parallelitätsgrundtyps beseitigt wird, um sicher vom enthaltenden Thread auf das static Element zuzugreifen.

    Zuvor mussten Zugriffe auf threadlokale Statiken in nativen AOT-kompilierten Programmen erforderlich sein, um einen Aufruf an die Laufzeit zu senden, um die Basisadresse des threadlokalen Speichers abzurufen. Jetzt kann der Compiler diese Aufrufe inlineieren, was zu deutlich weniger Anweisungen für den Zugriff auf diese Daten führt.

PGO-Verbesserungen: Typüberprüfungen und Umwandlungen

.NET 8 hat standardmäßig die dynamische profilgeführte Optimierung (Dynamic Profile Guided Optimization, PGO) aktiviert. NET 9 erweitert die PGO-Implementierung des JIT-Compilers, um weitere Codemuster zu profilieren. Wenn die mehrstufige Kompilierung aktiviert ist, fügt der JIT-Compiler die Instrumentierung bereits in Ihr Programm ein, um sein Verhalten zu profilieren. Wenn es mit Optimierungen neu kompiliert wird, nutzt der Compiler das Profil, das er zur Laufzeit erstellt hat, um entscheidungen zu treffen, die für die aktuelle Ausführung Ihres Programms spezifisch sind. In .NET 9 verwendet der JIT-Compiler PGO-Daten, um die Leistung von Typüberprüfungen zu verbessern.

Für die Bestimmung des Typs eines Objekts ist ein Aufruf der Laufzeit erforderlich, was zu leistungseinbußen kommt. Wenn der Typ eines Objekts überprüft werden muss, gibt der JIT-Compiler diesen Aufruf aus Gründen der Korrektheit aus (Compiler können in der Regel keine Möglichkeiten ausschließen, auch wenn sie unwahrscheinlich erscheinen). Wenn PGO-Daten jedoch vermuten, dass ein Objekt ein bestimmter Typ ist, gibt der JIT-Compiler jetzt einen schnellen Pfad aus, der billig nach diesem Typ sucht und auf den langsamen Pfad der Aufrufe in die Laufzeit zurückfällt, wenn erforderlich.

Arm64-Vektorisierung in .NET-Bibliotheken

Eine neue EncodeToUtf8 Implementierung nutzt die Fähigkeit des JIT-Compilers, Lade-/Speicheranweisungen für mehrere Register auf Arm64 auszustrahlen. Mit diesem Verhalten können Programme größere Datenblöcke mit weniger Anweisungen verarbeiten. .NET-Apps in verschiedenen Domänen sollten Durchsatzverbesserungen auf Arm64-Hardware sehen, die diese Features unterstützt. Einige Benchmarks schneiden ihre Ausführungszeit um mehr als die Hälfte ab.

Arm64-Codegenerierung

Der JIT-Compiler hat bereits die Möglichkeit, seine Darstellung zusammenhängender Lasten zu transformieren, um die ldp Anweisung (zum Laden von Werten) auf Arm64 zu verwenden. .NET 9 erweitert diese Möglichkeit zum Speichern von Vorgängen.

Die str Anweisung speichert Daten aus einem einzigen Register im Arbeitsspeicher, während die stp Anweisung Daten aus einem Registerpaar speichert. Anstelle stp von str Mitteln kann die gleiche Aufgabe mit weniger Speichervorgängen erreicht werden, wodurch die Ausführungszeit verbessert wird. Das Abschneiden einer Anweisung kann wie eine kleine Verbesserung aussehen, aber wenn der Code in einer Schleife für eine nichttrivielle Anzahl von Iterationen ausgeführt wird, können sich die Leistungsgewinne schnell addieren.

Betrachten Sie beispielsweise den folgenden Codeausschnitt:

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;
    }
}

Die Werte von b.x, b.yund b.z werden im Schleifentext aktualisiert. Auf Assemblyebene kann jedes Element mit einer str Anweisung gespeichert werden; oder mit stpzwei der Speicher (b.x und b.y, oder b.yb.z, da diese Paare im Arbeitsspeicher zusammenhängend sind) kann mit einer Anweisung behandelt werden. Um die stp Anweisung zum speichern und b.x gleichzeitig zu verwendenb.y, muss der Compiler auch bestimmen, dass die Berechnungen b.x + (dt * b.vx) unabhängig voneinander sind und b.y + (dt * b.vy) vor dem Speichern b.x in und b.yausgeführt werden können.

Schnellere Ausnahmen

Die CoreCLR-Laufzeit hat einen neuen Ansatz zur Ausnahmebehandlung eingeführt, der die Leistung der Ausnahmebehandlung verbessert. Die neue Implementierung basiert auf dem Ausnahmebehandlungsmodell der NativeAOT-Laufzeit. Die Änderung entfernt die Unterstützung für die strukturierte Behandlung von Windows-Ausnahmen (SEH) und deren Emulation auf Unix. Der neue Ansatz wird in allen Umgebungen unterstützt, mit Ausnahme von Windows x86 (32-Bit).

Die neue Implementierung der Ausnahmebehandlung ist 2-4 Mal schneller, je nach Ausnahmebehandlung von Mikro-Benchmarks. Die folgenden Perf-Verbesserungen wurden im perf-Labor gemessen:

Die neue Implementierung ist standardmäßig aktiviert. Wenn Sie jedoch zurück zum Verhalten der Legacy-Ausnahmebehandlung wechseln müssen, können Sie dies auf eine der folgenden Arten tun:

  • Legen Sie den Satzes System.Runtime.LegacyExceptionHandling in der true fest.runtimeconfig.json
  • Legen Sie die Umgebungsvariable DOTNET_LegacyExceptionHandling auf 1.

Codelayout

Compiler begründen in der Regel den Steuerungsfluss eines Programms mithilfe grundlegender Blöcke, wobei jeder Block ein Codeabschnitt ist, der nur bei der ersten Anweisung eingegeben und über die letzte Anweisung beendet werden kann. Die Reihenfolge der grundlegenden Blöcke ist wichtig. Wenn ein Block mit einer Verzweigungsanweisung endet, wird der Ablauf an einen anderen Block übertragen. Ein Ziel der Blockumordnung besteht darin, die Anzahl der Verzweigungsanweisungen im generierten Code zu reduzieren, indem das Fall-Through-Verhalten maximiert wird. Wenn auf jeden Basisblock sein höchstwahrscheinlicher Nachfolger folgt, kann er in seinen Nachfolger "fallen", ohne einen Sprung zu benötigen.

Bis vor kurzem wurde die Neuanordnung des Blocks im JIT-Compiler durch die Flowgraph-Implementierung eingeschränkt. In .NET 9 wurde der Block-Neuanordnungsalgorithmus des JIT-Compilers durch einen einfacheren, globaleren Ansatz ersetzt. Die Flussdiagrammdatenstrukturen wurden umgestaltet in:

  • Entfernen Sie einige Einschränkungen im Hinblick auf die Blockierung.
  • Ingrain-Ausführungswahrscheinlichkeiten in jeder Steuerungsflussänderung zwischen Blöcken.

Darüber hinaus werden Profildaten weitergegeben und verwaltet, während das Flussdiagramm der Methode transformiert wird.

Reduzierte Adressexposition

In .NET 9 kann der JIT-Compiler die Verwendung lokaler Variablenadressen besser nachverfolgen und unnötige Adressexposition vermeiden.

Wenn die Adresse einer lokalen Variablen verwendet wird, muss der JIT-Compiler bei der Optimierung der Methode zusätzliche Vorsichtsmaßnahmen ergreifen. Angenommen, der Compiler optimiert eine Methode, die die Adresse einer lokalen Variablen in einem Aufruf an eine andere Methode übergibt. Da der Angerufene möglicherweise die Adresse für den Zugriff auf die lokale Variable verwendet, um die Korrektheit beizubehalten, verhindert der Compiler die Transformation der Variablen. Adressierte Lokale können das Optimierungspotenzial des Compilers erheblich behindern.

AVX10v1-Unterstützung

Neue APIs wurden für AVX10 hinzugefügt, ein neuer SIMD-Anweisungssatz von Intel. Sie können Ihre .NET-Anwendungen auf AVX10-aktivierter Hardware mit vektorisierten Vorgängen mithilfe der neuen Avx10v1 APIs beschleunigen.

Hardwareinterne Codegenerierung

Viele systeminterne HARDWARE-APIs erwarten, dass Benutzer konstanten Werte für bestimmte Parameter übergeben. Diese Konstanten werden direkt in die zugrunde liegende Anweisung des Systeminternen codiert, anstatt in Register geladen oder aus dem Speicher darauf zugegriffen zu werden. Wenn keine Konstante bereitgestellt wird, wird das systeminterne System durch einen Aufruf einer Fallbackimplementierung ersetzt, die funktionell gleichwertig ist, aber langsamer.

Betrachten Sie das folgenden Beispiel:

static byte Test1()
{
    Vector128<byte> v = Vector128<byte>.Zero;
    const byte size = 1;
    v = Sse2.ShiftRightLogical128BitLane(v, size);
    return Sse41.Extract(v, 0);
}

Die Verwendung des size Aufrufs Sse2.ShiftRightLogical128BitLane kann durch die Konstante 1 ersetzt werden, und unter normalen Umständen ist der JIT-Compiler bereits in der Lage, diese Ersetzungsoptimierung zu optimieren. Bei der Bestimmung, ob der beschleunigte oder Fallbackcode generiert Sse2.ShiftRightLogical128BitLanewerden soll, erkennt der Compiler jedoch, dass eine Variable anstelle einer Konstanten übergeben wird und vorzeitig gegen "Intrinsifizieren" des Aufrufs entscheidet. Ab .NET 9 erkennt der Compiler weitere Fälle wie diese und ersetzt das Variable-Argument durch seinen konstanten Wert, wodurch der beschleunigte Code generiert wird.

Konstante Faltung für Gleitkomma- und SIMD-Vorgänge

Konstante Faltung ist eine vorhandene Optimierung im JIT-Compiler. Konstantenfaltung bezieht sich auf den Austausch von Ausdrücken, die während der Kompilierungszeit zu den Konstanten berechnet werden können, zu denen sie ausgewertet werden, wodurch Berechnungen zur Laufzeit entfallen. .NET 9 fügt neue Funktionen zur Konstantenfaltung hinzu:

  • Bei Binären Gleitkommavorgängen, bei denen einer der Operanden eine Konstante ist:
    • x + NaNist jetzt gefaltet.NaN
    • x * 1.0ist jetzt gefaltet.x
    • x + -0ist jetzt gefaltet.x
  • Für systeminterne Hardware. Angenommen, es handelt sich x um folgendes Vector<T>:
    • x + Vector<T>.Zeroist jetzt gefaltet.x
    • x & Vector<T>.Zeroist jetzt gefaltet.Vector<T>.Zero
    • x & Vector<T>.AllBitsSetist jetzt gefaltet.x

Arm64 SVE-Unterstützung

.NET 9 führt experimentelle Unterstützung für die Scalable Vector Extension (SVE) ein, eine SIMD-Anweisung für ARM64-CPUs. .NET unterstützt bereits den NEON-Anweisungssatz, also auf NEON-fähiger Hardware, ihre Anwendungen können 128-Bit-Vektorregister nutzen. SVE unterstützt flexible Vektorlängen bis zu 2048 Bit und entsperrt mehr Datenverarbeitung pro Anweisung. In .NET 9 Vector<T> ist 128 Bit breit, wenn SVE ausgerichtet ist, und zukünftige Arbeit ermöglicht die Skalierung der Breite, um die Vektorregistergröße des Zielcomputers zu entsprechen. Sie können Ihre .NET-Anwendungen auf SVE-fähiger Hardware mithilfe der neuen System.Runtime.Intrinsics.Arm.Sve APIs beschleunigen.

Hinweis

Die SVE-Unterstützung in .NET 9 ist experimentell. Die folgenden System.Runtime.Intrinsics.Arm.Sve APIs sind mit ExperimentalAttributegekennzeichnet, was bedeutet, dass sie in zukünftigen Versionen geändert werden können. Darüber hinaus funktionieren Debuggerschritte und Haltepunkte durch SVE-generierten Code möglicherweise nicht ordnungsgemäß, was zu Anwendungsabstürzen oder Beschädigungen von Daten führt.

Objektstapelzuweisung für Felder

Werttypen, z int . B. und struct, werden in der Regel auf dem Stapel anstelle des Heaps zugewiesen. Um jedoch verschiedene Codemuster zu aktivieren, werden sie häufig in Objekte "boxt" angezeigt.

Betrachten Sie den folgenden Codeausschnitt:

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 wird bequem so geschrieben, dass Sie, wenn Sie andere Typen wie Zeichenfolgen oder double Werte vergleichen möchten, dieselbe Implementierung wiederverwenden können. In diesem Beispiel hat es jedoch auch den Leistungsnachteil, dass alle Werttypen erforderlich sind, die an das Feld übergeben werden.

Der generierte RunIt x64-Assemblycode lautet wie folgt:

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

Die Aufrufe CORINFO_HELP_NEWSFAST sind die Heap-Zuordnungen für die Box-Ganzzahlargumente. Beachten Sie außerdem, dass es keinen Aufruf gibt Compare; der Compiler hat beschlossen, es inline zu inlineieren RunIt. Diese Einstreichung bedeutet, dass die Kästchen niemals "escape" sind. Mit anderen Worten, während der gesamten Ausführung von Compare, es weiß x und y sind tatsächlich ganze Zahlen, und sie können sicher entboxt werden, ohne die Vergleichslogik zu beeinträchtigen.

Ab .NET 9 ordnet der 64-Bit-Compiler nicht geschachtelte Felder auf dem Stapel zu, wodurch mehrere andere Optimierungen entsperrt werden. In diesem Beispiel übergibt der Compiler jetzt die Heap-Zuordnungen, aber da er weiß x und 3 und y 4 ist, kann er auch den Textkörper Compareweglassen; der Compiler kann zur Kompilierungszeit "false" bestimmen x.Equals(y) , daher RunIt sollte immer 100 zurückgegeben werden. Hier ist die aktualisierte Assembly:

mov      eax, 100
ret