Delen via


Wat is er nieuw in de .NET 10-runtime

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 Sumniet 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.