天天看點

在.Net 7中性能改進-PGO(按配置優化)

前言

本文是 Performance Improvements in .NET 7 PGO部分的翻譯.下面開始正文:

//原文位址: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/           

我在我的 Performance Improvements in .NET 6 文章中寫過關于配置檔案引導優化(PGO)的内容,但在這裡我将再次介紹它,因為它為.net 7帶來了大量的改進.

// Performance Improvements in .NET 6 
//位址: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/           

PGO已經存在很長一段時間了,存在于各種語言和編譯器中.基本思想是,在編譯應用程式時,要求編譯器向應用程式中注入工具來跟蹤各種有趣的資訊.然後讓應用運作,運作各種常見場景,使該工具““分析”應用執行時發生的情況,然後儲存結果.然後應用程式被重新編譯,将這些檢測結果回報給編譯器,并允許它優化應用程式,以準确地實作預期的使用方式.這種PGO的方法被稱為“靜态PGO”,因為所有的資訊都是在實際部署之前收集的,多年來.Net以各種形式一直在做這種事情.不過,在我看來,.在.Net中真正有趣的開發是“動态PGO”,它是在.Net 6中引入的,但預設是關閉的.

動态PGO利用了分層編譯的優勢.我注意到JIT利用0層代碼來跟蹤方法被調用了多少次,或者在循環的情況下,跟蹤循環執行了多少次.它也可以用它來做其他事情.例如,它可以準确地跟蹤将哪些具體類型用作接口分發的目标,然後在第1層專門化代碼以期望最常見的類型(這被稱為“受保護的去虛拟化”或GDV).你可以在這個小例子中看到.設定DOTNET_TieredPGO環境變量為1,然後在.Net 7上運作:

class Program
{
    static void Main()
    {
        IPrinter printer = new Printer();
        for (int i = 0; ; i++)
        {
            DoWork(printer, i);
        }
    }

    static void DoWork(IPrinter printer, int i)
    {
        printer.PrintIfTrue(i == int.MaxValue);
    }

    interface IPrinter
    {
        void PrintIfTrue(bool condition);
    }

    class Printer : IPrinter
    {
        public void PrintIfTrue(bool condition)
        {
            if (condition) Console.WriteLine("Print!");
        }
    }
}           

DoWork的tier-0代碼看起來是這樣的:

G_M000_IG01:                // offset=0000H
       55                   push     rbp
       4883EC30             sub      rsp, 48
       488D6C2430           lea      rbp, [rsp+30H]
       33C0                 xor      eax, eax
       488945F8             mov      qword ptr [rbp-08H], rax
       488945F0             mov      qword ptr [rbp-10H], rax
       48894D10             mov      gword ptr [rbp+10H], rcx
       895518               mov      dword ptr [rbp+18H], edx

G_M000_IG02:                // offset=001BH
       FF059F220F00         inc      dword ptr [(reloc 0x7ffc3f1b2ea0)]
       488B4D10             mov      rcx, gword ptr [rbp+10H]
       48894DF8             mov      gword ptr [rbp-08H], rcx
       488B4DF8             mov      rcx, gword ptr [rbp-08H]
       48BAA82E1B3FFC7F0000 mov      rdx, 0x7FFC3F1B2EA8
       E8B47EC55F           call     CORINFO_HELP_CLASSPROFILE32
       488B4DF8             mov      rcx, gword ptr [rbp-08H]
       48894DF0             mov      gword ptr [rbp-10H], rcx
       488B4DF0             mov      rcx, gword ptr [rbp-10H]
       33D2                 xor      edx, edx
       817D18FFFFFF7F       cmp      dword ptr [rbp+18H], 0x7FFFFFFF
       0F94C2               sete     dl
       49BB0800F13EFC7F0000 mov      r11, 0x7FFC3EF10008
       41FF13               call     [r11]IPrinter:PrintIfTrue(bool):this
       90                   nop

G_M000_IG03:                // offset=0062H
       4883C430             add      rsp, 48
       5D                   pop      rbp
       C3                   ret           

值得注意的是,你可以看到調用call [r11]IPrinter:PrintIfTrue(bool):this做接口分發.但是,接下來看看為第1層生成的代碼.我們仍然看到call [r11]IPrinter:PrintIfTrue(bool):this ,但我們也看到了這個:

G_M000_IG02:                ;; offset=0020H
       48B9982D1B3FFC7F0000 mov      rcx, 0x7FFC3F1B2D98
       48390F               cmp      qword ptr [rdi], rcx
       7521                 jne      SHORT G_M000_IG05
       81FEFFFFFF7F         cmp      esi, 0x7FFFFFFF
       7404                 je       SHORT G_M000_IG04

G_M000_IG03:                ;; offset=0037H
       FFC6                 inc      esi
       EBE5                 jmp      SHORT G_M000_IG02

G_M000_IG04:                ;; offset=003BH
       48B9D820801A24020000 mov      rcx, 0x2241A8020D8
       488B09               mov      rcx, gword ptr [rcx]
       FF1572CD0D00         call     [Console:WriteLine(String)]
       EBE7                 jmp      SHORT G_M000_IG03           

第一個代碼塊檢查IPrinter的具體類型(存儲在rdi中),并将其與Printer(0x7FFC3F1B2D98)的已知類型進行比較.如果它們不同,它就跳轉到在未優化版本中執行的相同接口分派.但如果它們是相同的,則直接跳轉到Printer.PrintIfTrue(你可以在這個方法中看到對Console:WriteLine的調用)内聯版本.是以,通常的情況(本例中唯一的情況)是以單個比較和分支為代價的超級高效.

在.NET6中已經有了,那麼為什麼我們現在要讨論它呢?有幾個方面有所改善.首先,由于dotnet/runtime#61453等改進,PGO現在與OSR一起工作.這是一件大事,因為這意味着執行這種接口分發(這是相當常見的)的長時間運作的熱方法可以獲得這種類型的去虛拟化/内聯優化.第二,雖然PGO目前沒有預設啟用,但我們已經讓它更容易啟用.在dotnet/runtime#71438和dotnet/sdk#26350之間,現在可以簡單地把 <TieredPGO>true</TieredPGO> 在你的項目工程檔案(*.csproj)中,它會有相同的效果,如果你設定DOTNET_TieredPGO=1之前的應用程式調用,啟用動态PGO(注意,它不禁用R2R鏡像,是以如果你想要整個核心庫也采用動态PGO,你還需要設定DOTNET_ReadyToRun=0).第三,動态PGO已經學會了如何測量和優化附加的東西.

using System.Runtime.CompilerServices;

class Program
{
    static int[] s_values = Enumerable.Range(0, 1_000).ToArray();

    static void Main()
    {
        for (int i = 0; i < 1_000_000; i++)
            Sum(s_values, i => i * 42);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static int Sum(int[] values, Func<int, int> func)
    {
        int sum = 0;
        foreach (int value in values)
            sum += func(value);
        return sum;
    }
}           

如果未啟用PGO,則生成彙編代碼如下:

// Assembly listing for method Program:Sum(ref,Func`2):int
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// optimized code
// rsp based frame
// partially interruptible
// No PGO data

G_M000_IG01:                ;; offset=0000H
       4156               push     r14
       57                   push     rdi
       56                   push     rsi
       55                   push     rbp
       53                   push     rbx
       4883EC20       sub      rsp, 32
       488BF2           mov      rsi, rdx

G_M000_IG02:                ;; offset=000DH
       33FF                 xor      edi, edi
       488BD9            mov      rbx, rcx
       33ED                xor      ebp, ebp
       448B7308         mov      r14d, dword ptr [rbx+08H]
       4585F6             test     r14d, r14d
       7E16                 jle      SHORT G_M000_IG04

G_M000_IG03:                ;; offset=001DH
       8BD5                mov      edx, ebp
       8B549310         mov      edx, dword ptr [rbx+4*rdx+10H]
       488B4E08         mov      rcx, gword ptr [rsi+08H]
       FF5618             call     [rsi+18H]Func`2:Invoke(int):int:this
       03F8                 add      edi, eax
       FFC5                 inc      ebp
       443BF5             cmp      r14d, ebp
       7FEA                 jg       SHORT G_M000_IG03

G_M000_IG04:                // offset=0033H
       8BC7              mov      eax, edi

G_M000_IG05:                // offset=0035H
       4883C420       add      rsp, 32
       5B                   pop      rbx
       5D                   pop      rbp
       5E                   pop      rsi
       5F                   pop      rdi
       415E                 pop      r14
       C3                   ret

// Total bytes of code 64           

注意 call [rsi+18H]Func ' 2:Invoke(int):int:this 在那裡調用委托.現在啟用PGO:

// Assembly listing for method Program:Sum(ref,Func`2):int
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// optimized code
// optimized using profile data
// rsp based frame
// fully interruptible
// with Dynamic PGO: edge weights are valid, and fgCalledCount is 5628
// 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                // offset=0000H
       4157               push     r15
       4156               push     r14
       57                   push     rdi
       56                   push     rsi
       55                   push     rbp
       53                   push     rbx
       4883EC28       sub      rsp, 40
       488BF2            mov      rsi, rdx

G_M000_IG02:               // offset=000FH
       33FF                 xor      edi, edi
       488BD9            mov      rbx, rcx
       33ED                 xor      ebp, ebp
       448B7308         mov      r14d, dword ptr [rbx+08H]
       4585F6             test     r14d, r14d
       7E27                 jle      SHORT G_M000_IG05

G_M000_IG03:               // offset=001FH
       8BC5                 mov      eax, ebp
       8B548310          mov      edx, dword ptr [rbx+4*rax+10H]
       4C8B4618           mov      r8, qword ptr [rsi+18H]
       48B8A0C2CF3CFC7F0000 mov      rax, 0x7FFC3CCFC2A0
       4C3BC0             cmp      r8, rax
       751D                 jne      SHORT G_M000_IG07
       446BFA2A         imul     r15d, edx, 42

G_M000_IG04:               // offset=003CH
       4103FF             add      edi, r15d
       FFC5                 inc      ebp
       443BF5             cmp      r14d, ebp
       7FD9                 jg       SHORT G_M000_IG03

G_M000_IG05:               // offset=0046H
       8BC7                 mov      eax, edi

G_M000_IG06:                // offset=0048H
       4883C428             add      rsp, 40
       5B                   pop      rbx
       5D                   pop      rbp
       5E                   pop      rsi
       5F                   pop      rdi
       415E               pop      r14
       415F               pop      r15
       C3                   ret

G_M000_IG07:                ;; offset=0055H
       488B4E08             mov      rcx, gword ptr [rsi+08H]
       41FFD0               call     r8
       448BF8               mov      r15d, eax
       EBDB                 jmp      SHORT G_M000_IG04           

我選擇了i=> i * 42中的42常數,以便于在程式集中看到它,果然,它就在那裡:

G_M000_IG03:                // offset=001FH
       8BC5                 mov      eax, ebp
       8B548310          mov      edx, dword ptr [rbx+4*rax+10H]
       4C8B4618          mov      r8, qword ptr [rsi+18H]
       48B8A0C2CF3CFC7F0000 mov      rax, 0x7FFC3CCFC2A0
       4C3BC0             cmp      r8, rax
       751D                 jne      SHORT G_M000_IG07
       446BFA2A         imul     r15d, edx, 42           

這是将目标位址從委托加載到r8,并将預期目标的位址加載到 rax.如果它們相同,它就簡單地執行内聯操作(imul r15d, edx, 42),否則就跳轉到G_M000_IG07,後者調用r8中的函數.如果我們将其作為基準運作,效果會很明顯:

static int[] s_values = Enumerable.Range(0, 1_000).ToArray();

[Benchmark]
public int DelegatePGO() => Sum(s_values, i => i * 42);

static int Sum(int[] values, Func<int, int>? func)
{
    int sum = 0;
    foreach (int value in values)
    {
        sum += func(value);
    }
    return sum;
}           

禁用PGO後,我們在.Net 6和.Net 7上獲得了相同的性能吞吐量:

Method Runtime Mean Ratio
DelegatePGO .NET 6.0 1.665 us 1.00
DelegatePGO .NET 7.0 1.659 us 1.00

但是當我們啟用動态PGO(DOTNET_TieredPGO=1)後,情況就不同了, .Net 6會快14%,而.Net 7會快3倍!

Method Runtime Mean Ratio
DelegatePGO .NET 6.0 1,427.7 ns 1.00
DelegatePGO .NET 7.0 539.0 ns 0.38

dotnet/runtime#70377是動态PGO的另一個有價值的改進,它使PGO能夠很好地進行循環克隆和不變量提升.為了更好地了解這一點,我們稍微離題一下這些是什麼.循環克隆是JIT用于避免循環快速路徑中的各種開銷的一種機制.考慮下面這個例子中的Test方法:

using System.Runtime.CompilerServices;

class Program
{
    static void Main()
    {
        int[] array = new int[10_000_000];
        for (int i = 0; i < 1_000_000; i++)
        {
            Test(array);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static bool Test(int[] array)
    {
        for (int i = 0; i < 0x12345; i++)
        {
            if (array[i] == 42)
            {
                return true;
            }
        }

        return false;
    }
}           

JIT不知道傳入的數組是否足夠長,使得循環中所有對array[i]的通路都在邊界内,是以它需要為每次通路注入邊界檢查.雖然在前面簡單地進行長度檢查并在長度不夠時提前抛出異常是一件好事,但這樣做也會改變行為(想象一下這個方法正在寫入數組,或者改變一些共享狀态).相反,JIT使用“循環克隆”.它本質上重寫了Test方法,使其更像這樣:

if (array is not null && array.Length >= 0x12345)
{
    for (int i = 0; i < 0x12345; i++)
    {
        if (array[i] == 42) // no bounds checks emitted for this access :-)
        {
            return true;
        }
    }
}
else
{
    for (int i = 0; i < 0x12345; i++)
    {
        if (array[i] == 42) // bounds checks emitted for this access :-(
        {
            return true;
        }
    }
}
return false;           

這樣,以犧牲一些代碼重複為代價,我們得到了沒有邊界檢查的快速循環,并且隻支付了慢路徑上的邊界檢查.你可以在生成的程式集中看到這個(如果你還不知道DOTNET_JitDisasm是我在.Net 7中最喜歡的特性之一):

// Assembly listing for method Program:Test(ref):bool
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// optimized code
// rsp based frame
// fully interruptible
// No PGO data

G_M000_IG01:                // offset=0000H
       4883EC28             sub      rsp, 40

G_M000_IG02:                // offset=0004H
       33C0                 xor      eax, eax
       4885C9             test     rcx, rcx
       7429                 je       SHORT G_M000_IG05
       81790845230100       cmp      dword ptr [rcx+08H], 0x12345
       7C20                 jl       SHORT G_M000_IG05
       0F1F40000F1F840000000000 align    [12 bytes for IG03]

G_M000_IG03:                // offset=0020H
       8BD0                 mov      edx, eax
       837C91102A     cmp      dword ptr [rcx+4*rdx+10H], 42
       7429                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100    cmp      eax, 0x12345
       7CEE                 jl       SHORT G_M000_IG03

G_M000_IG04:                // offset=0032H
       EB17                 jmp      SHORT G_M000_IG06

G_M000_IG05:                // offset=0034H
       3B4108             cmp      eax, dword ptr [rcx+08H]
       7323                 jae      SHORT G_M000_IG10
       8BD0                mov      edx, eax
       837C91102A    cmp      dword ptr [rcx+4*rdx+10H], 42
       7410                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100    cmp      eax, 0x12345
       7CE9                 jl       SHORT G_M000_IG05

G_M000_IG06:                // offset=004BH
       33C0                 xor      eax, eax

G_M000_IG07:                // offset=004DH
       4883C428       add      rsp, 40
       C3                   ret

G_M000_IG08:                // offset=0052H
       B801000000        mov      eax, 1

G_M000_IG09:                // offset=0057H
       4883C428       add      rsp, 40
       C3                   ret

G_M000_IG10:                // offset=005CH
       E81FA0C15F   call     CORINFO_HELP_RNGCHKFAIL
       CC                   int3

// Total bytes of code 98           

G_M000_IG02塊執行null檢查和長度檢查,如果失敗,則跳轉到G_M000_IG05塊.如果兩者都成功了,它就會執行循環(block G_M000_IG03),不進行邊界檢查:

G_M000_IG03:              // offset=0020H
       8BD0                mov      edx, eax
       837C91102A    cmp      dword ptr [rcx+4*rdx+10H], 42
       7429                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100     cmp      eax, 0x12345
       7CEE                 jl       SHORT G_M000_IG03           

邊界檢查隻出現在慢路徑塊中:

G_M000_IG05:                // offset=0034H
       3B4108             cmp      eax, dword ptr [rcx+08H]
       7323                 jae      SHORT G_M000_IG10
       8BD0                mov      edx, eax
       837C91102A    cmp      dword ptr [rcx+4*rdx+10H], 42
       7410                 je       SHORT G_M000_IG08
       FFC0                 inc      eax
       3D45230100    cmp      eax, 0x12345
       7CE9                 jl       SHORT G_M000_IG05           

這是“循環克隆”.那什麼是"不變提升”呢? 提升是把某個東西從循環中拉出來放到循環之前,不變量是不變的東西.是以,不變提升是在循環之前從循環中拉出一些東西,以避免在每次循環疊代中重新計算一個不變的答案.實際上,前面的例子已經展示了不變量提升,因為邊界檢查被移動到循環之前,而不是在循環中,但一個更具體的例子應該是這樣的:

[MethodImpl(MethodImplOptions.NoInlining)]
private static bool Test(int[] array)
{
    for (int i = 0; i < 0x12345; i++)
    {
        if (array[i] == array.Length - 42)
        {
            return true;
        }
    }

    return false;
}           

注意數組的值為array.Length - 42在每次循環疊代中不會改變,是以它對循環疊代是“不變的”,可以被取出,生成的代碼會這樣做:

G_M000_IG02:                // offset=0004H
       33D2                 xor      edx, edx
       4885C9              test     rcx, rcx
       742A                 je       SHORT G_M000_IG05
       448B4108         mov      r8d, dword ptr [rcx+08H]
       4181F845230100       cmp      r8d, 0x12345
       7C1D                 jl       SHORT G_M000_IG05
       4183C0D6         add      r8d, -42
       0F1F4000          align    [4 bytes for IG03]

G_M000_IG03:                // offset=0020H
       8BC2                mov      eax, edx
       4439448110     cmp      dword ptr [rcx+4*rax+10H], r8d
       7433                 je       SHORT G_M000_IG08
       FFC2                 inc      edx
       81FA45230100 cmp      edx, 0x12345
       7CED                 jl       SHORT G_M000_IG03           

在這裡,我們再次檢查數組是否為null( test rcx, rcx )和數組的長度被檢查( mov r8d, dword ptr [rcx+08H] 然後cmp r8d, 0x12345),但然後與數組的長度在 r8d ,然後我們看到這個數組長度減去42( add r8d, -42 ),這是在我們繼續進入快速路徑循環在G_M000_IG03塊之前.這樣就可以将那組額外的操作排除在循環之外,進而避免了每次疊代重新計算值的開銷.

那麼這如何應用于動态PGO呢?請記住,對于PGO能夠做到避免接口/虛拟分發,它通過進行類型檢查來檢視所使用的類型是否為最常見的類型;如果是,則使用直接調用該類型的方法的快速路徑(在這樣做時,該調用可能會内聯),如果不是,則傳回到正常的接口/虛拟分發.該檢查對循環是不變的.是以,當一個方法被分層并啟用PGO時,類型檢查現在可以從循環中升起,這使得處理常見情況的成本更低.考慮一下我們原來例子的變化:

using System.Runtime.CompilerServices;

class Program
{
    static void Main()
    {
        IPrinter printer = new BlankPrinter();
        while (true)
        {
            DoWork(printer);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void DoWork(IPrinter printer)
    {
        for (int j = 0; j < 123; j++)
        {
            printer.Print(j);
        }
    }

    interface IPrinter
    {
        void Print(int i);
    }

    class BlankPrinter : IPrinter
    {
        public void Print(int i)
        {
            Console.Write("");
        }
    }
}           

當我們檢視在啟用動态PGO的情況下為其生成優化的彙編代碼,我們看到:

// Assembly listing for method Program:DoWork(IPrinter)
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// optimized code
// optimized using profile data
// rsp based frame
// partially interruptible
// with Dynamic PGO: edge weights are invalid, and fgCalledCount is 12187
// 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:              // offset=0000H
       57                   push     rdi
       56                   push     rsi
       4883EC28             sub      rsp, 40
       488BF1               mov      rsi, rcx

G_M000_IG02:                // offset=0009H
       33FF                 xor      edi, edi
       4885F6             test     rsi, rsi
       742B                 je       SHORT G_M000_IG05
       48B9982DD43CFC7F0000 mov      rcx, 0x7FFC3CD42D98
       48390E               cmp      qword ptr [rsi], rcx
       751C                 jne      SHORT G_M000_IG05

G_M000_IG03:                // offset=001FH
       48B9282040F948020000 mov      rcx, 0x248F9402028
       488B09               mov      rcx, gword ptr [rcx]
       FF1526A80D00         call     [Console:Write(String)]
       FFC7                 inc      edi
       83FF7B               cmp      edi, 123
       7CE6                 jl       SHORT G_M000_IG03

G_M000_IG04:                // offset=0039H
       EB29                 jmp      SHORT G_M000_IG07

G_M000_IG05:                // offset=003BH
       48B9982DD43CFC7F0000 mov      rcx, 0x7FFC3CD42D98
       48390E               cmp      qword ptr [rsi], rcx
       7521                 jne      SHORT G_M000_IG08
       48B9282040F948020000 mov      rcx, 0x248F9402028
       488B09               mov      rcx, gword ptr [rcx]
       FF15FBA70D00         call     [Console:Write(String)]

G_M000_IG06:               // offset=005DH
       FFC7                 inc      edi
       83FF7B               cmp      edi, 123
       7CD7                 jl       SHORT G_M000_IG05

G_M000_IG07:                // offset=0064H
       4883C428             add      rsp, 40
       5E                   pop      rsi
       5F                   pop      rdi
       C3                   ret

G_M000_IG08:              // offset=006BH
       488BCE               mov      rcx, rsi
       8BD7                 mov      edx, edi
       49BB1000AA3CFC7F0000 mov      r11, 0x7FFC3CAA0010
       41FF13               call     [r11]IPrinter:Print(int):this
       EBDE                 jmp      SHORT G_M000_IG06

// Total bytes of code 127           

我們可以在G_M000_IG02塊中看到,它正在對IPrinter執行個體進行類型檢查,如果檢查失敗就跳轉到G_M000_IG05( mov rcx, 0x7FFC3CD42D98 然後( cmp qword ptr [rsi], rcx 然後 jne SHORT G_M000_IG05 ),否則就跳轉到G_M000_IG03,這是一個緊密的快速路徑循環,内聯 BlankPrinter.Print 後沒有進行類型檢查!

有趣的是,這樣的改進也會帶來挑戰.PGO導緻類型檢查數量的顯著增加,因為專門化給定類型的調用站點需要與該類型進行比較.然而,常見的子表達式消除(CSE)在曆史上并不适用于這種類型句柄(CSE是一種編譯器優化,通過一次計算結果然後存儲它以供後續使用,而不是每次重新計算它,進而消除重複表達式).dotnet/runtime#70580通過為這些常量句柄啟用CSE修複了這個問題.例如,考慮以下方法:

[Benchmark]
[Arguments("", "", "", "")]
public bool AllAreStrings(object o1, object o2, object o3, object o4) =>
    o1 is string && o2 is string && o3 is string && o4 is string;           

在.Net 6中JIT生成了以下彙編代碼:

// Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)
       test      rdx,rdx
       je        short M00_L01
       mov    rax,offset MT_System.String ;;第1次加載
       cmp    [rdx],rax
       jne       short M00_L01
       test      r8,r8
       je         short M00_L01
       mov     rax,offset MT_System.String  ;;第2次加載
       cmp     [r8],rax
       jne       short M00_L01
       test      r9,r9
       je         short M00_L01
       mov     rax,offset MT_System.String  ;;第3次加載
       cmp     [r9],rax
       jne       short M00_L01
       mov     rax,[rsp+28]
       test      rax,rax
       je         short M00_L00
       mov     rdx,offset MT_System.String  ;;第4次加載
       cmp     [rax],rdx
       je         short M00_L00
       xor       eax,eax
M00_L00:
       test      rax,rax
       setne   al
       movzx  eax,al
       ret
M00_L01:
       xor       eax,eax
       ret
// Total bytes of code 100           

注意,C#有四個string(字元串)test(邏輯與運算,彙編代碼有四個加載mov rax,offset MT_System.String.現在在.Net 7中進行一次加載:

Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)
       test      rdx,rdx
       je         short M00_L01
       mov     rax,offset MT_System.String  ;;隻有1次加載
       cmp     [rdx],rax
       jne       short M00_L01
       test      r8,r8
       je         short M00_L01
       cmp     [r8],rax
       jne       short M00_L01
       test      r9,r9
       je         short M00_L01
       cmp     [r9],rax
       jne       short M00_L01
       mov     rdx,[rsp+28]
       test      rdx,rdx
       je         short M00_L00
       cmp     [rdx],rax
       je         short M00_L00
       xor       edx,edx
M00_L00:
       xor       eax,eax
       test      rdx,rdx
       setne    al
       ret
M00_L01:
       xor       eax,eax
       ret
// Total bytes of code 69           

因JIT部分内容太多,這裡進行拆分,PGO就到了,Bounds Check Elimination(消除邊界檢查)拆分為一篇博文.

個人能力有限,如果您發現有什麼不對,請私信我

如果您覺得對您有用的話,可以點個贊或者加個關注,歡迎大家一起進行技術交流

繼續閱讀