Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
In diesem Artikel werden neue Features und Leistungsverbesserungen in der .NET-Laufzeit für .NET 10 beschrieben.
JIT-Compilerverbesserungen
Der JIT-Compiler in .NET 10 enthält erhebliche Verbesserungen, die die Leistung durch bessere Codegenerierungs- und Optimierungsstrategien verbessern.
Verbesserte Codegenerierung für Strukturargumente
. Der JIT-Compiler von NET kann eine Optimierung namens physischer Heraufstufung durchführen, bei der die Member einer Struktur in Registern und nicht im Stapel platziert werden, wodurch Speicherzugriffe beseitigt werden. Diese Optimierung ist besonders nützlich, wenn eine Struktur an eine Methode übergeben wird, und die Aufrufkonvention erfordert, dass die Strukturmitglieder in Registern übergeben werden.
.NET 10 verbessert die interne Darstellung des JIT-Compilers, um Werte zu verarbeiten, die ein Register gemeinsam nutzen. Früher, wenn Strukturmitglieder in ein einzelnes Register gepackt werden mussten, speicherte der JIT erst die Werte im Arbeitsspeicher und lud sie dann in ein Register. Jetzt kann der JIT-Compiler die beförderten Mitglieder von Strukturargumenten direkt in gemeinsame Register setzen, wodurch unnötige Speicheroperationen eliminiert werden.
Betrachten Sie das folgenden Beispiel:
struct Point
{
public long X;
public long Y;
public Point(long x, long y)
{
X = x;
Y = y;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Consume(Point p)
{
Console.WriteLine(p.X + p.Y);
}
private static void Main()
{
Point p = new Point(10, 20);
Consume(p);
}
Auf x64 werden die Mitglieder von Point in separaten Registern an Consume übergeben, und da die physische Heraufstufung für die lokale p eingetreten ist, wird zunächst nichts auf dem Stapel zugewiesen.
Program:Main() (FullOpts):
mov edi, 10
mov esi, 20
tail.jmp [Program:Consume(Program+Point)]
Angenommen, der Typ der Mitglieder von Point wurde von int zu long geändert. Da eine Breite von int vier Byte beträgt und Register acht Byte breit auf x64 sind, müssen die Mitglieder von Point in einem Register übergeben werden. Zuvor würde der JIT-Compiler zuerst die Werte im Arbeitsspeicher speichern und dann den 1-Byte-Block in ein Register laden. Mit den Verbesserungen in .NET 10 kann der JIT-Compiler jetzt die beförderten Mitglieder von Strukturargumenten direkt in gemeinsame Register platzieren.
Program:Main() (FullOpts):
mov rdi, 0x140000000A
tail.jmp [Program:Consume(Program+Point)]
Dadurch wird die Notwendigkeit des Zwischenspeichers beseitigt, was zu effizienteren Assemblycode führt.
Verbesserte Schleifeninvertierung
Der JIT-Compiler kann die Bedingung einer while-Schleife aufheben und den Body der Schleife in eine do-while-Schleife transformieren, wodurch die endgültige Form entsteht:
if (loopCondition)
{
do
{
// loop body
} while (loopCondition);
}
Diese Transformation wird als Schleifeninversion bezeichnet. Durch Das Verschieben der Bedingung an den unteren Rand der Schleife entfernt das JIT die Notwendigkeit, zum oberen Rand der Schleife zu verzweigen, um die Bedingung zu testen und das Codelayout zu verbessern. Zahlreiche Optimierungen (wie das Klonen von Schleifen, das Abrollen von Schleifen und die Optimierung von Induktionsvariablen) hängen ebenfalls von der Schleifeninversion ab, um diese Form zur Unterstützung der Analyse zu erzeugen.
.NET 10 optimiert die Schleifeninversion, indem es von einer Implementierung auf Basis lexikalischer Analyse zu einer Implementierung wechselt, die graphbasierte Schleifenerkennung verwendet. Diese Änderung führt zu einer verbesserten Präzision, da alle natürlichen Schleifen (Schleifen mit einem einzigen Einstiegspunkt) berücksichtigt werden und falsche Positivergebnisse, die zuvor berücksichtigt wurden, ignoriert werden. Dies führt zu einem höheren Optimierungspotenzial für .NET-Programme mit for und while Anweisungen.
Devirtualisierung der Array-Schnittstellenmethode
Einer der Schwerpunktbereiche für .NET 10 besteht darin, den Abstraktionsaufwand beliebter Sprachfeatures zu reduzieren. Um dieses Ziel zu erreichen, wurde die Fähigkeit des JIT, Methodenaufrufe zu devirtualisieren, erweitert, um Arrayschnittstellenmethoden abzudecken.
Betrachten Sie die typische Vorgehensweise beim Durchlaufen eines Arrays:
static int Sum(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
Diese Code-Struktur ist für das JIT einfach zu optimieren, hauptsächlich weil es keine virtuellen Methodenaufrufe gibt. Stattdessen kann sich das JIT auf das Entfernen von Begrenzungsprüfungen auf den Arrayzugriff und das Anwenden der Schleifenoptimierungen konzentrieren, die in .NET 9 hinzugefügt wurden. Im folgenden Beispiel werden einige virtuelle Aufrufe hinzugefügt:
static int Sum(int[] array)
{
int sum = 0;
IEnumerable<int> temp = array;
foreach (var num in temp)
{
sum += num;
}
return sum;
}
Der Typ der zugrunde liegenden Auflistung ist klar, und der JIT sollte dieses Code-Snippet in das erste umwandeln können. Arrayschnittstellen werden jedoch anders als "normale" Schnittstellen implementiert, sodass der JIT nicht weiß, wie man sie entvirtualisiert. Dies bedeutet, dass die Enumeratoraufrufe in der foreach Schleife virtuell bleiben und mehrere Optimierungen blockieren, z. B. Inlining und Stapelzuweisung.
Ab .NET 10 kann der JIT Array-Schnittstellenmethoden devirtualisieren und inline setzen. Dies ist die erste von vielen Schritten, um die Leistungsparität zwischen den Implementierungen zu erreichen, wie in den .NET 10-Destraktionsplänen beschrieben.
Enumeration der Abstraktion bei der Array-Aufzählung
Die Bemühungen, den Abstraktionsaufwand der Arrayiteration über Enumerationen zu reduzieren, haben die Inlining-, Stapelzuordnungs- und Schleifen-Klonfähigkeiten des JIT verbessert. So wird beispielsweise der Mehraufwand für das Aufzählen von Arrays über IEnumerable reduziert, und die bedingte Escapeanalyse ermöglicht jetzt die Stapelzuweisung von Enumerationen in bestimmten Szenarien.
Verbessertes Codelayout
Der JIT-Compiler in .NET 10 führt einen neuen Ansatz zum Organisieren von Methodencode in grundlegende Blöcke ein, um eine bessere Laufzeitleistung zu erzielen. Früher verwendete das JIT eine Reverse-Postorder-(RPO)-Traversal des Programmflussgraphen als anfängliches Layout, gefolgt von iterativen Transformationen. Während dieser Ansatz effektiv war, hatte er Einschränkungen bei der Modellierung der Abwägungen zwischen der Verringerung der Verzweigung und der Erhöhung der Heißcodedichte.
In .NET 10 modelliert das JIT das Block-Neuanordnungsproblem als Reduzierung des asymmetrischen Problems "Travelling Salesman" und implementiert die 3-Opt-Heuristik, um ein nahezu optimales Traversal zu finden. Diese Optimierung verbessert die Hot Path-Dichte und reduziert Verzweigungen, was zu einer besseren Laufzeitleistung führt.
Verbesserungen beim Inlining
In .NET 10 wurden verschiedene Verbesserungen beim Inlining vorgenommen.
Das JIT kann nun inline Methoden anwenden, die aufgrund der vorherigen Inlinierung zur Entvirtualisierung berechtigt werden. Diese Verbesserung ermöglicht es dem JIT, weitere Optimierungsmöglichkeiten zu entdecken, z. B. weitere Inlinierung und Entvirtualisierung.
Einige Methoden mit Ausnahmebehandlungssemantik, insbesondere mit try-finally Blöcken, können ebenfalls inlineiert werden.
Um die Fähigkeit des JIT, einige Arrays auf den Stack zu legen, besser zu nutzen, wurden die Heuristiken des Inliners angepasst, um die Rentabilität von Kandidaten zu erhöhen, die möglicherweise kleine Arrays mit fester Größe zurückgeben.
Rückgabetypen
Während der Inlineierung aktualisiert das JIT nun den Typ temporärer Variablen, die Rückgabewerte enthalten. Wenn alle Rückgabestellen in einer aufgerufenen Funktion denselben Typ liefern, werden diese präzisen Typinformationen verwendet, um nachfolgende Aufrufe zu entvirtualisieren. Diese Erweiterung ergänzt die Verbesserungen bei der späten Devirtualisierung und der Deabstraktion der Array-Enumeration.
Profilerstellungsdaten
.NET 10 verbessert die JiT-Inlining-Richtlinie, um die Profildaten besser zu nutzen. Neben zahlreichen anderen Heuristiken berücksichtigt der JIT-Inliner keine Methoden, die eine bestimmte Größe überschreiten, um eine Aufblähung der aufrufenden Methode zu vermeiden. Wenn der Aufrufer über Profildaten verfügt, die darauf hindeuten, dass ein Inlining-Kandidat häufig ausgeführt wird, erhöht der Inliner seine Größentoleranz für den Kandidaten.
Nehmen wir an, der JIT inlined einen Calllee Callee ohne Profildaten in einen Caller Caller mit Profildaten. Diese Diskrepanz kann auftreten, wenn der Caller zu klein ist, um eine Instrumentierung zu lohnen, oder wenn er zu oft inline ausgeführt wird, um eine ausreichende Anzahl von Aufrufen zu haben. Wenn Callee seine eigenen Inlining-Kandidaten hat, hat das JIT diese bisher nicht mit seiner Standardgrößenbeschränkung berücksichtigt, weil Callee keine Profildaten hat. Jetzt erkennt das JIT, dass Caller Profildaten hat und lockert seine Größenbeschränkung (aber, um den Präzisionsverlust zu berücksichtigen, nicht in dem Maße, wie wenn Callee Profildaten hätte).
Ähnlich verhält es sich, wenn der JIT entscheidet, dass eine Call-Site für das Inlining nicht profitabel ist, markiert er die Methode mit NoInlining, damit zukünftige Inlining-Versuche sie nicht mehr berücksichtigen. Viele Inline-Heuristiken sind jedoch empfindlich gegenüber Profildaten. Zum Beispiel könnte das JIT entscheiden, dass eine Methode zu groß ist, um inlinefähig zu sein, wenn keine Profildaten vorhanden sind. Wenn der Aufrufer aber ausreichend heiß ist, ist das JIT möglicherweise bereit, seine Größenbeschränkung zu lockern und den Aufruf zu inlinen. In .NET 10 kennzeichnet das JIT unrentable Inline-Aufrufe nicht mehr mit NoInlining, um eine Pessimierung von Aufrufstellen mit Profildaten zu vermeiden.
AVX10.2-Unterstützung
.NET 10 bietet Unterstützung für advanced Vector Extensions (AVX) 10.2 für x64-basierte Prozessoren. Die neuen Intrinsics, die in der System.Runtime.Intrinsics.X86.Avx10v2 Klasse verfügbar sind, können getestet werden, sobald entsprechend leistungsfähige Hardware verfügbar ist.
Da AVX10.2-fähige Hardware noch nicht verfügbar ist, ist die Unterstützung des JIT für AVX10.2 derzeit standardmäßig deaktiviert.
Stapelbelegung
Die Stapelzuordnung reduziert die Anzahl der Objekte, die die GC nachverfolgen muss, und es entsperrt auch andere Optimierungen. Beispielsweise kann das JIT nach der Stapelzuweisung eines Objekts erwägen, es vollständig durch seine Skalarwerte zu ersetzen. Aus diesem Grund ist die Stapelzuordnung entscheidend, um die Abstraktionsstrafe von Referenztypen zu verringern. .NET 10 fügt Stapelzuweisungen für kleine Arrays von Werttypenundkleine Arrays von Referenztypen hinzu. Es umfasst auch Escape-Analyse für lokale Struct-Felder und Delegierungen. (Objekte, die nicht maskiert werden können, können auf dem Stack zugewiesen werden).
Kleine Arrays von Werttypen
Der JIT weist nun kleine Arrays von Werttypen mit fester Größe auf dem Stack zu, die keine GC-Zeiger enthalten, wenn garantiert werden kann, dass sie ihre übergeordnete Methode nicht überleben. Im folgenden Beispiel weiß der JIT bei der Kompilierung, dass numbers ein Array mit nur drei Ganzzahlen ist, das einen Aufruf von Sum nicht überlebt, und weist es daher auf dem Stack zu.
static void Sum()
{
int[] numbers = {1, 2, 3};
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
Console.WriteLine(sum);
}
Kleine Arrays von Referenztypen
.NET 10 erweitert die .NET 9-Stapelzuweisungsverbesserungen auf kleine Arrays von Referenztypen. Bisher wurden Arrays von Referenztypen immer im Heap zugewiesen, auch wenn deren Geltungsbereich auf eine einzelne Methode beschränkt war. Jetzt kann das JIT solche Arrays stapelweise zuweisen, wenn es feststellt, dass sie nicht länger als ihr Erstellungskontext bestehen bleiben. Im folgenden Beispiel wird das Array words nun im Stack zugewiesen.
static void Print()
{
string[] words = {"Hello", "World!"};
foreach (var str in words)
{
Console.WriteLine(str);
}
}
Analyse maskieren
Die Escapeanalyse bestimmt, ob ein Objekt seine übergeordnete Methode überleben kann. Objekte „maskieren“, wenn sie nicht-lokalen Variablen zugewiesen oder an Funktionen übergeben werden, die vom JIT nicht inlinefähig sind. Wenn ein Objekt nicht entkommen kann, kann es im Stack zugeordnet werden. .NET 10 enthält Escapeanalyse für:
Lokale Struct-Felder
Ab .NET 10 berücksichtigt das JIT Objekte, auf die durch Strukturfelder verwiesen wird, wodurch mehr Stapelzuweisungen möglich sind und der Heap-Aufwand reduziert wird. Betrachten Sie das folgenden Beispiel:
public class Program
{
struct GCStruct
{
public int[] arr;
}
public static int Main()
{
int[] x = new int[10];
GCStruct y = new GCStruct() { arr = x };
return y.arr[0];
}
}
Normalerweise weist der JIT Stack kleine Arrays fester Größe zu, die nicht maskiert werden, wie z. B. x. Die Zuweisung an y.arr führt nicht dazu, dass x entweicht, weil auch y nicht entweicht. Die vorherige Escapeanalyseimplementierung des JIT hat jedoch Strukturfeldreferenzen nicht modelliert. In .NET 9 enthält die für Main generierte x64-Assembly einen Aufruf an CORINFO_HELP_NEWARR_1_VC, um x auf dem Heap zu allozieren, was darauf hinweist, dass sie als maskierend markiert wurde:
Program:Main():int (FullOpts):
push rax
mov rdi, 0x719E28028A98 ; int[]
mov esi, 10
call CORINFO_HELP_NEWARR_1_VC
mov eax, dword ptr [rax+0x10]
add rsp, 8
ret
In .NET 10 markiert der JIT keine Objekte mehr als entkommend, auf die von lokalen Strukturfeldern verwiesen wird, solange die betreffende Struktur nicht entkommt. Die Assembly sieht nun wie folgt aus (beachten Sie, dass der Aufruf zur Heap-Allokation verschwunden ist):
Program:Main():int (FullOpts):
sub rsp, 56
vxorps xmm8, xmm8, xmm8
vmovdqu ymmword ptr [rsp], ymm8
vmovdqa xmmword ptr [rsp+0x20], xmm8
xor eax, eax
mov qword ptr [rsp+0x30], rax
mov rax, 0x7F9FC16F8CC8 ; int[]
mov qword ptr [rsp], rax
lea rax, [rsp]
mov dword ptr [rax+0x08], 10
lea rax, [rsp]
mov eax, dword ptr [rax+0x10]
add rsp, 56
ret
Weitere Informationen zu De-Abstraktion-Verbesserungen von .NET 10 finden Sie unter dotnet/runtime#108913.
Delegierte
Wenn Quellcode in IL kompiliert wird, wird jeder Delegat in eine Abschlussklasse mit einer Methode transformiert, die der Definition und den Feldern des Delegaten entspricht, die allen erfassten Variablen entsprechen. Zur Laufzeit wird ein Abschlussobjekt erstellt, um die erfassten Variablen zusammen mit einem Func Objekt zum Aufrufen des Delegaten zu instanziieren. Wenn die Escapeanalyse bestimmt, dass das Func Objekt seinen aktuellen Bereich nicht überdauert, ordnet das JIT es dem Stack zu.
Sehen Sie sich die folgende Main-Methode an:
public static int Main()
{
int local = 1;
int[] arr = new int[100];
var func = (int x) => x + local;
int sum = 0;
foreach (int num in arr)
{
sum += func(num);
}
return sum;
}
Zuvor produziert das JIT die folgende abgekürzte x64-Assembly für Main. Vor dem Eintritt in die Schleife werden arr, func und die Closure-Klasse für func, die Program+<>c__DisplayClass0_0 genannt wird, alle auf dem Heap alloziert, wie durch die CORINFO_HELP_NEW*-Aufrufe angezeigt.
; prolog omitted for brevity
mov rdi, 0x7DD0AE362E28 ; Program+<>c__DisplayClass0_0
call CORINFO_HELP_NEWSFAST
mov rbx, rax
mov dword ptr [rbx+0x08], 1
mov rdi, 0x7DD0AE268A98 ; int[]
mov esi, 100
call CORINFO_HELP_NEWARR_1_VC
mov r15, rax
mov rdi, 0x7DD0AE4A9C58 ; System.Func`2[int,int]
call CORINFO_HELP_NEWSFAST
mov r14, rax
lea rdi, bword ptr [r14+0x08]
mov rsi, rbx
call CORINFO_HELP_ASSIGN_REF
mov rsi, 0x7DD0AE461140 ; code for Program+<>c__DisplayClass0_0:<Main>b__0(int):int:this
mov qword ptr [r14+0x18], rsi
xor ebx, ebx
add r15, 16
mov r13d, 100
G_M24375_IG03: ;; offset=0x0075
mov esi, dword ptr [r15]
mov rdi, gword ptr [r14+0x08]
call [r14+0x18]System.Func`2[int,int]:Invoke(int):int:this
add ebx, eax
add r15, 4
dec r13d
jne SHORT G_M24375_IG03
; epilog omitted for brevity
Da func nie außerhalb des Bereichs von Main referenziert wird, wird es auch auf dem Stack allokiert:
; prolog omitted for brevity
mov rdi, 0x7B52F7837958 ; Program+<>c__DisplayClass0_0
call CORINFO_HELP_NEWSFAST
mov rbx, rax
mov dword ptr [rbx+0x08], 1
mov rsi, 0x7B52F7718CC8 ; int[]
mov qword ptr [rbp-0x1C0], rsi
lea rsi, [rbp-0x1C0]
mov dword ptr [rsi+0x08], 100
lea r15, [rbp-0x1C0]
xor r14d, r14d
add r15, 16
mov r13d, 100
G_M24375_IG03: ;; offset=0x0099
mov esi, dword ptr [r15]
mov rdi, rbx
mov rax, 0x7B52F7901638 ; address of definition for "func"
call rax
add r14d, eax
add r15, 4
dec r13d
jne SHORT G_M24375_IG03
; epilog omitted for brevity
Beachten Sie, dass es einen verbleibenden CORINFO_HELP_NEW*-Aufruf gibt, der die Heap-Zuweisung für die Closure darstellt. Das Runtime-Team plant, die Escape-Analyse zu erweitern, um die Stack-Zuweisung von Closures in einer zukünftigen Version zu unterstützen.
Verbesserungen bei der Vorinitialisierung von NativeAOT-Typen
NativeAOTs Typpräinitialisierer unterstützt jetzt alle Varianten der conv.*- und neg- Opcodes. Diese Erweiterung ermöglicht die Präinitialisierung von Methoden, die Umwandlungs- oder Negationsvorgänge enthalten und die Laufzeitleistung weiter optimieren.
Verbesserungen bei Arm64-Schreibbarrieren
Der Garbage Collector (GC) von .NET ist generationenbasiert, das bedeutet, dass er Liveobjekte nach ihrem Alter trennt, um die Leistung der Sammlung zu verbessern. Der GC generiert häufiger jüngere Generationen unter der Annahme, dass langlebige Objekte weniger wahrscheinlich zu einem bestimmten Zeitpunkt nicht referenziert (oder „tot“) sind. Angenommen, ein altes Objekt verweist auf ein junges Objekt; die GC muss wissen, dass es das junge Objekt nicht sammeln kann. Durch die Notwendigkeit, ältere Objekte zu scannen, um ein junges Objekt zu sammeln, werden jedoch die Leistungsgewinne einer generationalen GC verloren.
Um dieses Problem zu lösen, fügt das JIT Schreibbarrieren ein, bevor Objektverweisaktualisierungen aktualisiert werden, um die GC auf dem Laufenden zu halten. Auf x64 kann die Laufzeit, je nach Konfiguration des GC, dynamisch zwischen Schreibbarrierenimplementierungen wechseln, um Schreibgeschwindigkeit und Effizienz der Sammlung auszubalancieren. In .NET 10 ist diese Funktionalität auch auf Arm64 verfügbar. Insbesondere die neue Standardimplementierung für Schreibbarrieren auf Arm64 verarbeitet GC-Regionen genauer, wodurch die Sammlungsleistung bei geringen Kosten beim Schreibbarrieredurchsatz verbessert wird. Benchmarks zeigen eine Verbesserung der GC-Pausen von 8 % auf über 20 % mit den neuen GC-Standardwerten.