Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In dit artikel worden nieuwe functies en prestatieverbeteringen in de .NET-runtime voor .NET 10 beschreven. Deze wordt bijgewerkt voor preview 5.
Devirtualisatie van array-interfacemethode
Een van de focusgebieden voor .NET 10 is om de abstractieoverhead van populaire taalfuncties te verminderen. Om dit doel te bereiken, is het vermogen van JIT om methodeaanroepen te devirtualiseren uitgebreid om matrixinterfacemethoden te behandelen.
Overweeg de gebruikelijke benadering van het itereren over een array.
static int Sum(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
Deze codevorm is gemakkelijk voor de JIT om te optimaliseren, voornamelijk omdat er geen virtuele aanroepen zijn om rekening mee te houden. In plaats daarvan kan JIT zich richten op het verwijderen van grenzencontroles op de matrixtoegang en het toepassen van de lusoptimalisaties die zijn toegevoegd in .NET 9. In het volgende voorbeeld worden enkele virtuele aanroepen toegevoegd:
static int Sum(int[] array)
{
int sum = 0;
IEnumerable<int> temp = array;
foreach (var num in temp)
{
sum += num;
}
return sum;
}
Het type onderliggende verzameling is duidelijk en het JIT moet dit fragment kunnen omzetten in het eerste codefragment. Matrixinterfaces worden echter anders geïmplementeerd dan normale interfaces, zodat de JIT niet weet hoe ze moeten worden gedevirtualiseerd. Dit betekent dat de aanroepen van de enumerator in de foreach
lus virtueel blijven, waardoor meerdere optimalisaties, zoals inlining en stacktoewijzing, worden geblokkeerd.
Vanaf .NET 10 kan JIT devirtualiseren en inline-matrixinterfacemethoden gebruiken. Dit is de eerste van de vele stappen om prestatiepariteit te bereiken tussen de implementaties, zoals beschreven in de .NET 10-abstractieplannen.
Matrixumeration de-abstractie
Inspanningen om de abstractie-overhead van array-iteratie via enumerators te verminderen, hebben de inlining-, stacktoewijzings- en loopkloningsmogelijkheden van de JIT verbeterd. De overhead van het inventariseren van matrices via IEnumerable
wordt bijvoorbeeld verminderd en voorwaardelijke escape-analyse maakt nu stacktoewijzing van enumerators mogelijk in bepaalde scenario's.
Verbeterde code-indeling
De JIT-compiler in .NET 10 introduceert een nieuwe benadering voor het ordenen van methodecode in basisblokken voor betere runtimeprestaties. Voorheen gebruikte de JIT een reverse postorder (RPO) traversie van het stroomdiagram van het programma als een initiële indeling, gevolgd door iteratieve transformaties. Hoewel deze aanpak effectief was, had deze benadering beperkingen bij het modelleren van de afwegingen tussen het verminderen van vertakkingen en het verhogen van de hot code-dichtheid.
In .NET 10 modelleert de JIT het blok herschikkingsprobleem als een reductie van het asymmetrische Travelling Salesman Probleem en implementeert het de 3-opt-heuristiek om een bijna-optimale doorkruising te vinden. Deze optimalisatie verbetert de hot path-dichtheid en vermindert vertakkingsafstanden, wat resulteert in betere runtimeprestaties.
AVX10.2-ondersteuning
.NET 10 introduceert ondersteuning voor de AVX (Advanced Vector Extensions) 10.2 voor x64-processors. De nieuwe intrinsieke kenmerken die beschikbaar zijn in de System.Runtime.Intrinsics.X86.Avx10v2 klasse kunnen worden getest zodra geschikte hardware beschikbaar is.
Omdat AVX10.2-hardware nog niet beschikbaar is, is de ondersteuning van JIT voor AVX10.2 momenteel standaard uitgeschakeld.
Stacktoewijzing
Stacktoewijzing vermindert het aantal objecten dat de GC moet bijhouden en ontgrendelt ook andere optimalisaties. Nadat een object bijvoorbeeld in de stack is toegewezen, kan de JIT overwegen het object volledig te vervangen door de scalaire waarden. Daarom is stacktoewijzing essentieel om de abstractiestraf van referentietypen te verminderen. .NET 10 voegt stacktoewijzing toe voor kleine matrices van waardetypenenkleine matrices met referentietypen. Het bevat ook escape-analyse voor lokale structuurvelden en delegaten. (Objecten die niet kunnen ontsnappen, kunnen op de stapel worden gealloceerd.)
Kleine matrices met waardetypen
De JIT wijst nu kleine, vaste matrices met waardetypen toe die geen GC-aanwijzers bevatten wanneer ze gegarandeerd hun bovenliggende methode niet kunnen overleven. In het volgende voorbeeld weet de JIT op het moment van compileren dat numbers
een matrix is van slechts drie gehele getallen die een aanroep Sum
niet overleeft en die daarom aan de stack toewijst.
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 matrices van referentietypen
.NET 10 breidt de verbeteringen voor de toewijzing van .NET 9-stacks uit tot kleine matrices van referentietypen. Voorheen werden reeksen van referentietypen altijd toegewezen aan de heap, zelfs wanneer hun levensduur beperkt was tot één methode. Nu kan de JIT dergelijke arrays stack-alloceren wanneer wordt bepaald dat ze hun aanmaakcontext niet overleven. In het volgende voorbeeld wordt de matrix words
nu toegewezen aan de stack.
static void Print()
{
string[] words = {"Hello", "World!"};
foreach (var str in words)
{
Console.WriteLine(str);
}
}
Escape-analyse
Escape-analyse bepaalt of een object langer kan bestaan dan zijn oudermethode. Objecten ontsnappen wanneer ze worden toegewezen aan niet-lokale variabelen of doorgegeven aan functies die niet door de JIT worden geïntegreerd. Als een object niet kan ontsnappen, kan het worden toegewezen aan de stack. .NET 10 bevat escape-analyse voor:
Lokale struct-velden
Vanaf .NET 10 beschouwt de JIT objecten waarnaar wordt verwezen door structvelden, waardoor meer stacktoewijzingen mogelijk zijn en de heap-overhead wordt verminderd. Bekijk het volgende voorbeeld:
public class Program
{
struct GCStruct
{
public int[] arr;
}
public static void Main()
{
int[] x = new int[10];
GCStruct y = new GCStruct() { arr = x };
return y.arr[0];
}
}
Normaal gesproken gebruikt de JIT stack-allocatie om kleine arrays met een vaste grootte toe te wijzen die de stack niet verlaten, zoals x
. De toewijzing aan y.arr
zorgt er niet voor dat x
ontsnapt, omdat y
ook niet ontsnapt. De eerdere implementatie van de escape-analyse van JIT heeft echter geen structveldreferenties gemodelleerd. In .NET 9 bevat de x64-assembly die is gegenereerd voor Main
een aanroep naar CORINFO_HELP_NEWARR_1_VC
om x
op de heap toe te wijzen, waarmee wordt aangegeven dat deze is gemarkeerd als ontsnappend.
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 merkt de JIT geen objecten meer aan waarop wordt verwezen door lokale struct-velden als ontkomend, zolang de struct in kwestie niet zelf ontkomt. De assembly ziet er nu als volgt uit (u ziet dat de aanroep van de heap-toewijzingshulp is verdwenen):
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
Zie dotnet/runtime#108913 voor meer informatie over de abstractieverbeteringen in .NET 10.
Gedelegeerden
Wanneer de broncode wordt gecompileerd naar IL, wordt elke delegate omgezet in een closure class met een methode die overeenkomt met de definitie van de delegate en velden die overeenkomen met alle vastgelegde variabelen. Tijdens runtime wordt een sluitingsobject gemaakt om de vastgelegde variabelen te instantiëren, samen met een Func
object om de gemachtigde aan te roepen. Als de escape-analyse bepaalt dat het Func
object het huidige bereik niet kan overleven, wijst de JIT het toe aan de stack.
Houd rekening met de volgende Main
methode:
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;
}
Voorheen produceert de JIT de volgende afgekorte x64-assembly voor Main
. Voordat u de lus invoert, worden arr
, func
, en de sluitingsklasse voor func
, genoemd Program+<>c__DisplayClass0_0
, allemaal toegewezen aan de heap, zoals aangegeven door de CORINFO_HELP_NEW*
aanroepen.
; 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
Nu, omdat func
nooit buiten het bereik van Main
wordt verwezen, wordt deze ook toegewezen aan de stack:
; 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
Merk op dat er slechts één CORINFO_HELP_NEW*
aanroep is, namelijk de heaptoewijzing voor de closure. Het runtimeteam is van plan om escape-analyse uit te breiden ter ondersteuning van stacktoewijzing van sluitingen in een toekomstige release.
Verbeteringen in delining
Er zijn verschillende inliningsverbeteringen aangebracht in .NET 10.
De JIT kan nu methoden inlinen die vanwege eerdere inlining in aanmerking komen voor devirtualisatie. Door deze verbetering kan JIT meer optimalisatiemogelijkheden ontdekken, zoals verdere inlining en devirtualisatie.
Sommige methoden met semantiek voor het afhandelen van uitzonderingen, met name methoden met try-finally
blokken, kunnen ook inline worden geplaatst.
Om beter te profiteren van het vermogen van de JIT-compiler om sommige arrays in de stack toe te wijzen, zijn de heuristieken van de inliner aangepast om de effectiviteit te vergroten van kandidaten die mogelijk kleine, vaste arrays retourneren.
Retourtypen
Tijdens het inlijnen werkt de JIT nu het type tijdelijke variabelen bij waarin retourwaarden worden opgeslagen. Als alle retoursites in een aangeroepen functie hetzelfde type opleveren, wordt deze exacte type-informatie gebruikt om volgende aanroepen te devirtualiseren. Deze verbetering vormt een aanvulling op de verbeteringen in late devirtualisatie en de-abstractie van array-opsomming.
Profielgegevens
.NET 10 verbetert het inliningsbeleid van JIT om beter te profiteren van profielgegevens. De inliner van JIT beschouwt door middel van talloze heuristieken geen methoden die een bepaalde grootte overschrijden om te voorkomen dat de aanroepende methode opgeblazen wordt. Wanneer de beller profielgegevens bevat die suggereren dat een inliningskandidaat vaak wordt uitgevoerd, verhoogt de inliner de groottetolerantie voor de kandidaat.
Stel dat de JIT sommige functies inline maakt, Callee
zonder profielgegevens in een aanroeper Caller
met profielgegevens. Deze discrepantie kan optreden als de aanroep te klein is om instrumentatie waard te zijn, of als deze te vaak inline is om voldoende aantal oproepen te hebben. Als Callee
zijn eigen inliningskandidaten heeft, heeft de JIT ze eerder niet overwogen vanwege de standaardlimiet op grootte, omdat Callee
geen profielgegevens had. Nu realiseert de JIT dat Caller
profielgegevens heeft en versoepelt de groottebeperking (maar, om rekening te houden met verlies van precisie, niet in dezelfde mate als wanneer Callee
profielgegevens zou hebben).
Wanneer de JIT besluit dat een aanroeplocatie niet winstgevend is voor inlinen, wordt de methode gemarkeerd met NoInlining
om toekomstige pogingen tot inlinen te vermijden. Veel inline heuristieken zijn echter gevoelig voor profielgegevens. De JIT kan bijvoorbeeld besluiten dat een methode te groot is om de moeite waard te zijn voor inlining bij afwezigheid van profielgegevens. Maar wanneer de beller voldoende dynamisch is, kan de JIT bereid zijn om de groottebeperking te versoepelen en de oproep inline te plaatsen. In .NET 10 markeert het JIT niet langer onprofiterbare inlinees NoInlining
om pessimisering van oproepsites met profielgegevens te voorkomen.
Verbeteringen van het NativeAOT-type preinitializer
NativeAOT's type preinitializer ondersteunt nu alle varianten van de conv.*
en neg
opcodes. Deze verbetering maakt preinitialisatie mogelijk van methoden die cast- of negatiebewerkingen omvatten, waardoor de runtimeprestaties verder worden geoptimaliseerd.
Verbeteringen aan arm64-schrijfbarrières
.NET's garbagecollector (GC) is generationeel, wat betekent dat het live-objecten scheidt op basis van leeftijd om de verzameling efficiënter te maken. De GC verzamelt jongere generaties vaker onder de veronderstelling dat langlevende objecten op elk gewenst moment minder waarschijnlijk ondekenbaar (of 'dood') zijn. Stel dat een oud object verwijst naar een jong object; de GC moet weten dat het jonge object niet kan worden verzameld. Als u echter oudere objecten wilt scannen om een jong object te verzamelen, worden de prestatieverbeteringen van een generatie GC verslagen.
Om dit probleem op te lossen, voegt de JIT schrijfbarrières in voordat objectverwijzingsupdates worden bijgewerkt om de GC op de hoogte te houden. Op x64 kan de runtime dynamisch schakelen tussen implementaties van schrijfbarrières om de schrijfsnelheden en de efficiëntie van verzamelingen te verdelen, afhankelijk van de configuratie van de GC. In .NET 10 is deze functionaliteit ook beschikbaar op Arm64. Met name de nieuwe standaard implementatie van schrijfbarrières op Arm64 verwerkt GC-regio's nauwkeuriger, wat de prestaties van de verzameling verbetert tegen een lichte kosten voor schrijfbarrièredoorvoer. Benchmarks tonen aan dat de onderbrekingsverbeteringen van de GC zijn toegenomen van 8% naar meer dan 20% met de nieuwe standaardinstellingen van de GC.