【譯】.NET 7 中的效能改進(三)

2023-02-22 06:01:22

原文 | Stephen Toub

翻譯 | 鄭子銘

PGO

我在我的 .NET 6 效能改進一文中寫了關於組態檔引導優化 (profile-guided optimization) (PGO) 的文章,但我將在此處再次介紹它,因為它已經看到了 .NET 7 的大量改進。

PGO 已經存在了很長時間,有多種語言和編譯器。基本思想是你編譯你的應用程式,要求編譯器將檢測注入應用程式以跟蹤各種有趣的資訊。然後你讓你的應用程式通過它的步伐,執行各種常見的場景,使該儀器「描述」應用程式執行時發生的事情,然後儲存結果。然後重新編譯應用程式,將這些檢測結果反饋給編譯器,並允許它根據預期的使用方式優化應用程式。這種 PGO 方法被稱為「靜態 PGO」,因為所有資訊都是在實際部署之前收集的,這是 .NET 多年來一直以各種形式進行的事情。不過,從我的角度來看,.NET 中真正有趣的開發是「動態 PGO」,它是在 .NET 6 中引入的,但預設情況下是關閉的。

動態 PGO 利用分層編譯。我注意到 JIT 檢測第 0 層程式碼以跟蹤方法被呼叫的次數,或者在迴圈的情況下,迴圈執行了多少次。它也可以將它用於其他事情。例如,它可以準確跟蹤哪些具體型別被用作介面分派的目標,然後在第 1 層專門化程式碼以期望最常見的型別(這稱為「保護去虛擬化 (guarded devirtualization)」或 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 的第 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

而最值得注意的是,你可以看到呼叫[r11]IPrinter:PrintIfTrue(bool):這個做介面排程。但是,再看一下為第一層生成的程式碼。我們仍然看到呼叫[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的呼叫)。因此,普通情況(本例中唯一的情況)是超級有效的,代價是一個單一的比較和分支。

這一切都存在於.NET 6中,那麼為什麼我們現在要談論它?有幾件事得到了改善。首先,由於dotnet/runtime#61453這樣的改進,PGO現在可以與OSR一起工作。這是一個大問題,因為這意味著做這種介面排程的熱的長期執行的方法(這相當普遍)可以得到這些型別的去虛擬化/精簡優化。第二,雖然PGO目前不是預設啟用的,但我們已經讓它更容易開啟了。在dotnet/runtime#71438dotnet/sdk#26350之間,現在可以簡單地將true放入你的.csproj中。 csproj,它的效果和你在每次呼叫應用程式之前設定DOTNET_TieredPGO=1一樣,啟用動態PGO(注意,它不會禁止使用R2R影象,所以如果你希望整個核心庫也採用動態PGO,你還需要設定DOTNET_ReadyToRun=0)。然而,第三,是動態PGO已經學會了如何檢測和優化額外的東西。

PGO已經知道如何對虛擬排程進行檢測。現在在.NET 7中,在很大程度上要感謝dotnet/runtime#68703,它也可以為委託做這件事(至少是對實體方法的委託)。考慮一下這個簡單的控制檯應用程式。

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

注意其中呼叫[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中得到了相同的效能吞吐量。

方法 執行時間 平均值 比率
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倍!

方法 執行時間 平均值 比率
DelegatePGO .NET 6.0 1,427.7 ns 1.00
DelegatePGO .NET 7.0 539.0 ns 0.38

dotnet/runtime#70377是動態PGO的另一個有價值的改進,它使PGO能夠很好地發揮迴圈克隆和不變數提升的作用。為了更好地理解這一點,簡要地說說這些是什麼。迴圈克隆 (Loop cloning) 是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不知道傳入的陣列是否有足夠的長度,以至於在迴圈中對陣列[i]的所有存取都在邊界內,因此它需要為每次存取注入邊界檢查。雖然簡單地在前面進行長度檢查,並在長度不夠的情況下提前丟擲一個異常是很好的,但這樣做也會改變行為(設想該方法在進行時向陣列中寫入資料,或者以其他方式改變一些共用狀態)。相反,JIT採用了 "迴圈克隆"。它從本質上重寫了這個測試方法,使之更像這樣。

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部分正在進行空值檢查和長度檢查,如果任何一項失敗,則跳轉到G_M000_IG05塊。如果兩者都成功了,它就會執行迴圈(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

這就是 "迴圈克隆"。那麼,"不變數提升 (invariant hoisting) "呢?提升是指把某個東西從迴圈中拉到迴圈之前,而不變數是不會改變的東西。因此,不變數提升是指把某個東西從迴圈中拉到迴圈之前,以避免在迴圈的每個迭代中重新計算一個不會改變的答案。實際上,前面的例子已經展示了不變數提升,即邊界檢查被移到了迴圈之前,而不是在迴圈中,但一個更具體的例子是這樣的。

[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

這裡我們再次看到陣列被測試為空(test rcx, rcx),陣列的長度被檢查(mov r8d, dword ptr [rcx+08H] then 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 then cmp qword ptr [rsi], rcx then jne SHORT G_M000_IG05),否則就跳到G_M000_IG03,這是一個緊密的快速路徑迴圈,內聯BlankPrinter.Print,看不到任何型別檢查。

有趣的是,這樣的改進也會帶來自己的挑戰。PGO導致了型別檢查數量的大幅增加,因為專門針對某一特定型別的呼叫站點需要與該型別進行比較。然而,普通的子表示式消除 (common subexpression elimination)(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
       cmp       [rdx],rax
       jne       short M00_L01
       test      r8,r8
       je        short M00_L01
       mov       rax,offset MT_System.String
       cmp       [r8],rax
       jne       short M00_L01
       test      r9,r9
       je        short M00_L01
       mov       rax,offset MT_System.String
       cmp       [r9],rax
       jne       short M00_L01
       mov       rax,[rsp+28]
       test      rax,rax
       je        short M00_L00
       mov       rdx,offset MT_System.String
       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#對字串有四個測試,而組合程式碼中的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
       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

原文連結

Performance Improvements in .NET 7

知識共享許可協議

本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協定進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

如有任何疑問,請與我聯絡 ([email protected])