Aracılığıyla paylaş


.NET 10 çalışma zamanındaki yenilikler

Bu makalede.NET 10 için .NET çalışma zamanındaki yeni özellikler ve performans geliştirmeleri açıklanmaktadır. Önizleme 5 için güncelleştirildi.

Dizi arabirimi yöntemi sanallaştırmadan çıkarma

.NET 10 için odak alanlarından biri, popüler dil özelliklerinin soyutlama ek yükünü azaltmaktır. Bu hedefin peşinde, JIT'nin yöntem çağrılarını devirtualize etme yeteneği, dizi arabirim yöntemlerini kapsayacak şekilde genişlemiştir.

Bir dizi üzerinde döngü gerçekleştirmenin tipik yaklaşımını göz önünde bulundurun:

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

Bu kod yapısı, JIT için kolayca optimize edilebilir çünkü değerlendirilmesi gereken sanal çağrılar yoktur. Bunun yerine, JIT dizi erişimindeki sınır denetimlerini kaldırmaya ve .NET 9 eklenendöngüsü iyileştirmelerini uygulamaya odaklanabilir. Aşağıdaki örnek bazı sanal çağrılar ekler:

static int Sum(int[] array)
{
    int sum = 0;
    IEnumerable<int> temp = array;

    foreach (var num in temp)
    {
        sum += num;
    }
    return sum;
}

Temel koleksiyonun türü açıkça belirlenmiştir ve JIT'nin bu kod parçacığını önceki haline dönüştürmesi gerekir. Ancak, dizi arabirimleri "normal" arabirimlerden farklı şekilde uygulanır, böylece JIT bunları nasıl devirtualize ettiğini bilmez. Bu, foreach döngüsündeki numaralandırıcı çağrılarının sanal kaldığı ve satır içi ve yığın ayırma gibi birden çok iyileştirmeyi engellediği anlamına gelir.

.NET 10'dan başlayarak JIT, satır içi dizi arabirim yöntemlerini devirtualize edebilir. Bu, .NET 10 soyutlama planlarında açıklandığı gibi, uygulamalar arasında performans eşliği elde etmek için birçok adımın ilkidir.

Dizi numaralandırmasının soyutlamadan arındırılması

Numaralandırıcılar aracılığıyla dizi yinelemesinin soyutlama ek yükünü azaltmaya yönelik çalışmalar JIT'nin satır içi, yığın ayırma ve döngü kopyalama yeteneklerini geliştirdi. Örneğin, IEnumerable aracılığıyla dizileri listeleme yükü azalır ve koşullu kaçış analizi artık belirli senaryolarda numaralandırıcıların yığın ayırmasını sağlar.

Geliştirilmiş kod düzeni

.NET 10'daki JIT derleyicisi, daha iyi çalışma zamanı performansı için yöntem kodunu temel bloklar halinde düzenlemeye yönelik yeni bir yaklaşım sağlar. Daha önce JIT, ilk düzen olarak programın akış grafiğinin ters sıralı (RPO) geçişini ve ardından yinelemeli dönüşümleri kullanıyordu. Etkili olsa da, bu yaklaşımın dallanmayı azaltma ile sıcak kod yoğunluğunun artırılması arasındaki dengeleri modelleme konusunda sınırlamaları vardı.

.NET 10'da JIT, blok yeniden sıralama sorununu asimetrik Seyahat Eden Satıcı Sorununun bir indirgeme olarak modelleyerek optimuma yakın bir yolu bulmak için 3-opt buluşsal yöntemi uygular. Bu optimizasyon, sıcak yol yoğunluğunu artırır ve dal uzaklıklarını azaltarak çalışma süresi performansını artırır.

AVX10.2 desteği

.NET 10, x64 tabanlı işlemciler için Gelişmiş Vektör Uzantıları (AVX) 10.2 desteği sağlar. System.Runtime.Intrinsics.X86.Avx10v2 sınıfında bulunan yeni iç bileşenler, uygun donanım kullanılabilir olduğunda test edilebilir.

AVX10.2 özellikli donanım henüz kullanılamadığından, JIT'nin AVX10.2 desteği şu anda varsayılan olarak devre dışıdır.

Yığın ayırma

Yığın ayırma, GC'nin izlemesi gereken nesne sayısını azaltır ve ayrıca diğer iyileştirmelerin kilidini açar. Örneğin, bir nesne yığında tahsis edildikten sonra, JIT onu tamamen skaler değerleriyle değiştirmeyi düşünebilir. Bu nedenle yığın ayırma, başvuru türlerinin soyutlama cezasını azaltmanın anahtarıdır. .NET 10 , küçük değer türleri dizileriveküçük başvuru türleri dizileri için yığın ayırması ekler. Ayrıca yerel yapı alanları ve temsilciler için kaçış analizi içerir. (Kaçamayan nesneler yığın üzerinde ayrılabilir.)

Küçük değer türleri dizileri

JIT, GC işaretçileri içermeyen ve üst yöntemlerinden daha uzun ömürlü olmayacağı garanti edilen küçük, sabit boyutlu değer türü dizilerini artık yığına ayırır. Aşağıdaki örnekte, JIT derleme zamanında numbers öğesinin yalnızca üç tamsayıdan oluşan ve Sum çağrısından sonra yaşamayan bir dizi olduğunu bilir, bu nedenle onu yığına ayırır.

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

Referans türlerinin küçük dizileri

.NET 10 , .NET 9 yığın ayırma geliştirmelerini başvuru türlerinin küçük dizilerine genişletir. Daha önce, referans türü dizileri, yaşam süreleri yalnızca bir yönteme dayalı olduğunda bile yığında her zaman tahsis edilirdi. Artık JIT, oluştukları bağlamdan daha uzun süre yaşamayan bu tür dizileri yığında tahsis edebilir. Aşağıdaki örnekte dizi words artık yığında ayrılmıştır.

static void Print()
{
    string[] words = {"Hello", "World!"};
    foreach (var str in words)
    {
        Console.WriteLine(str);
    }
}

Kaçış analizi

Kaçış analizi bir nesnenin üst metodundan daha uzun süre yaşayıp yaşayamayacağını belirler. Yerel olmayan değişkenlere atandığında veya JIT tarafından satır içi hale getirilmeyen işlevlere geçirildiğinde nesneler "kaçar". Bir nesne kaçamıyorsa, yığına atanabilir. .NET 10, şunlar için kaçış analizi içerir:

Yerel yapı alanları

.NET 10'dan başlayarak, JIT daha fazla yığın ayırması sağlayan ve yığın ek yükünü azaltan yapı alanları tarafından başvurulan nesneleri dikkate alır. Aşağıdaki örneği göz önünde bulundurun:

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

Normalde, JIT kaçış olmayan, küçük ve sabit boyutlu dizileri, örneğin x, yığında ayırır. y.arr'e atanması, x'in kaçmasına neden olmaz çünkü y de kaçmaz. Ancak JIT'nin önceki kaçış analizi uygulaması yapı alanı başvurularını modellemedi. .NET 9'da, Main için oluşturulan x64 derlemesi, yığın üzerinde CORINFO_HELP_NEWARR_1_VC ayırmak için bir x çağrısını içerir ve kaçacağını belirten şekilde işaretlenmiştir.

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

.NET 10'da, söz konusu yapı escape etmiyorsa JIT artık yerel yapı alanları tarafından referans verilen nesneleri escape etmiş gibi işaretlemez. Montaj şimdi şöyle görünüyor (yığın belleği ayırma yardımcı işlevinin gittiğine dikkat edin):

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

.NET 10'daki soyutlamayı kaldırma geliştirmeleri hakkında daha fazla bilgi için bkz. dotnet/runtime#108913.

Temsilciler

Kaynak kodu IL'ye derlendiğinde, her temsilci, tanımına uygun bir yöntem ve yakalanan değişkenlere karşılık gelen alanlarla bir kapanış sınıfına dönüştürülür. Çalışma zamanında, yakalanan değişkenleri örneklemek için bir kapatma nesnesi ve temsilciyi çağırmak için Func nesnesi oluşturulur. Kaçış analizi nesnenin geçerli kapsamını aşmayacağını belirlerse, JIT onu yığına ayırır.

Aşağıdaki Main yöntemi göz önünde bulundurun:

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

Daha önce, JIT Main için aşağıdaki kısaltılmış x64 derlemesini üretti. Döngüye girmeden önce, arr, func ve func olarak adlandırılan, Program+<>c__DisplayClass0_0 için kapanış sınıfı, çağrılarında belirtildiği üzere yığında ayrılır.

       ; 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

Artık func, Main kapsamı dışında hiçbir zaman başvurulmadığı için yığında da ayrılır.

       ; 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

Dikkat edin, bir tane kalan CORINFO_HELP_NEW* çağrı var, bu da kapanış için bellek yığını tahsisatıdır. Çalışma zamanı takımı, gelecekteki bir sürümde fonksiyon kapatmalarının yığındaki tahsisini desteklemek için kaçış analizi (escape analysis) metodunu genişletmeyi planlıyor.

Inlining geliştirmeleri

.NET 10'da çeşitli inlining geliştirmeleri yapılmıştır.

JIT, artık önceki satır içi işlemler nedeniyle devirtualizasyon için uygun hale gelen yöntemleri satır içine alabilir. Bu geliştirme, JIT'nin daha fazla iyileştirme fırsatı, örneğin daha fazla "inline" işlemi ve sanallaştırmayı ortadan kaldırma gibi, ortaya çıkarmasını sağlar.

Özellikle try-finally bloklarına sahip olan bazı istisna işleme semantiği yöntemleri de satır içi yerleştirilebilir.

JIT'nin bazı dizileri yığınlama özelliğinden daha iyi yararlanmak için inliner'ın buluşsal yöntemleri, küçük, sabit boyutlu diziler döndürebilecek adayların kârlılığını artıracak şekilde ayarlanmıştır.

Dönüş türleri

Satır içi oluşturma sırasında JIT artık dönüş değerlerini tutan geçici değişkenlerin türünü güncelleştirir. Bir çağrıdaki tüm dönüş siteleri aynı türe sahipse, sonraki çağrıları devirtualize etmek için bu kesin tür bilgileri kullanılır. Bu iyileştirme, geç sanal kaldırma ve dizi sayım soyutlamasının iyileştirmelerini tamamlar.

Profil verileri

.NET 10, profil verilerinden daha iyi yararlanmak için JIT'nin iç ilkesini geliştirir. Çok sayıda buluşsal yöntem arasında JIT inliner, çağıranın yöntemini şişirmekten kaçınmak için belirli bir boyuttaki yöntemleri dikkate almaz. Çağıranın, bir inlining adayının sıklıkla yürütüldüğüne işaret eden profil verileri olduğunda, inliner adayın boyut toleransını artırır.

JIT'in profil verileri olmayan bazı çağrıları Callee profil verileri olan bazı arayanların Caller içine satır içi olarak yerleştirdiğini varsayalım. Bu tutarsızlık, çağrılanın izlemeye değmeyecek kadar küçük olması veya yeterli çağrı sayısına sahip olmak için çok sık satır içi hale getirilmesi durumunda ortaya çıkabilir. Kendi inlining adayları varsa Callee, JIT daha önce Callee profil verilerinin bulunmaması nedeniyle bunları varsayılan boyut sınırını dikkate alarak değerlendirmemişti. Şimdi JIT, Caller profil verilerine sahip olduğunu fark edecek ve boyut kısıtlamasını gevşetecek (ancak, kesinlik kaybını hesaba katmak için, Callee profil verilerine sahip olsa olduğu kadar değil).

Benzer şekilde, JIT bir çağrı konumunun iç içe yerleştirme için karlı olmadığını belirlediğinde, gelecekteki inlining girişimlerini göz önünde bulundurmaktan kurtarmak için yöntemi NoInlining ile işaretler. Bununla birlikte, birçok iç içe geçirme sezgisi profil verilerine duyarlıdır. Örneğin, JIT profil verilerinin yokluğunda bir yöntemin satır içi genişlemeye değmeyecek kadar büyük olduğuna karar verebilir. Ancak arayan yeterince sıcak olduğunda, JIT boyut kısıtlamasını ve aramayı satır içi olarak gevşetmeye istekli olabilir. .NET 10'da, Anında Derleyici (JIT), profil verileriyle çağrı noktalarının performansını olumsuz etkilememek için artık verimsiz inline'ları NoInlining ile işaretlemez.

NativeAOT türü ön yükleyici geliştirmeleri

NativeAOT'un tür ön başlatıcısı artık conv.* ve neg opcode'larının tüm değişkenlerini destekliyor. Bu iyileştirme, dönüştürme veya negasyon işlemlerini içeren yöntemlerin ön yüklemesini ve çalışma zamanı performansını daha da optimize etmeyi sağlar.

Arm64 yazma engeli geliştirmeleri

. NET'in çöp toplayıcısı (GC) nesilseldir, yani canlı nesneleri toplama performansını geliştirmek için yaşa göre ayırır. GC, uzun ömürlü nesnelerin herhangi bir zamanda başvurulmama (veya "ölü") olma olasılığının daha düşük olduğu varsayımı altında genç nesilleri daha sık toplar. Ancak, eski bir nesnenin genç bir nesneye başvurmaya başladığını varsayalım; GC'nin genç nesneyi toplayamayacağını bilmesi gerekir. Ancak genç bir nesneyi toplamak için eski nesneleri taramak gerektiğinde, bu durum nesilsel GC'nin performans kazançlarını ortadan kaldırır.

Bu sorunu çözmek için JIT, GC'yi bilgilendirmek için nesne başvurusu güncelleştirmelerinden önce yazma engelleri ekler. x64'te çalışma zamanı, GC yapılandırmasına bağlı olarak yazma hızlarını ve koleksiyon verimliliğini dengelemek için yazma engeli uygulamaları arasında dinamik olarak geçiş yapabilir. .NET 10'da bu işlev Arm64'te de kullanılabilir. Özellikle Arm64'teki yeni varsayılan yazma engeli uygulaması GC bölgelerini daha hassas bir şekilde işler ve bu da toplama performansını yazma engeli aktarım hızına düşük bir maliyetle artırır. Benchmark testleri, yeni GC varsayılanlarıyla GC duraklatma iyileştirmelerinin 8%'dan 20%'in üzerine çıktığını gösteriyor.