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

鄭子銘發表於2023-02-20

原文 | Stephen Toub

翻譯 | 鄭子銘

堆疊替換 (On-Stack Replacement)

堆疊替換 (OSR) 是 .NET 7 中最酷的 JIT 功能之一。但要真正瞭解 OSR,我們首先需要了解分層編譯 (tiered compilation),所以快速回顧一下……

具有 JIT 編譯器的託管環境必須處理的問題之一是啟動和吞吐量之間的權衡。從歷史上看,最佳化編譯器的工作就是最佳化,以便在執行時實現應用程式或服務的最佳吞吐量。但是這種最佳化需要分析,需要時間,並且執行所有這些工作會導致啟動時間增加,因為啟動路徑上的所有程式碼(例如,在 Web 伺服器可以為第一個請求提供服務之前需要執行的所有程式碼)需要編譯。因此 JIT 編譯器需要做出權衡:以更長的啟動時間為代價獲得更好的吞吐量,或者以降低吞吐量為代價獲得更好的啟動時間。對於某些型別的應用程式和服務,權衡很容易,例如如果您的服務啟動一次然後執行數天,那麼啟動時間多幾秒並不重要,或者如果您是一個控制檯應用程式,它將進行快速計算並退出,啟動時間才是最重要的。但是 JIT 如何知道它處於哪種場景中,我們真的希望每個開發人員都必須瞭解這些型別的設定和權衡並相應地配置他們的每個應用程式嗎?對此的一種解決方案是提前編譯,它在 .NET 中採用了多種形式。例如,所有核心庫都是“crossgen”的,這意味著它們已經透過生成前面提到的 R2R 格式的工具執行,生成包含彙編程式碼的二進位制檔案,只需稍作調整即可實際執行;並非每個方法都可以為其生成程式碼,但足以顯著減少啟動時間。當然,這種方法有其自身的缺點,例如JIT 編譯器的承諾之一是它可以利用當前機器/程式的知識來進行最佳最佳化,例如,R2R 影像必須採用特定的基線指令集(例如,哪些向量化指令可用),而JIT 可以看到實際可用的東西並使用最好的。 “分層編譯”提供了另一種答案,無論是否使用這些其他提前 (ahead-of-time) (AOT) 編譯解決方案,它都可以使用。

分層彙編使JIT能夠擁有傳說中的蛋糕,也能吃到它。這個想法很簡單:允許 JIT 多次編譯相同的程式碼。第一次,JIT 可以使用盡可能少的最佳化(少數最佳化實際上可以使 JIT 自身的吞吐量更快,因此應用這些最佳化仍然有意義),生成相當未最佳化的彙編程式碼,但這樣做速度非常快。當它這樣做時,它可以在程式集中新增一些工具來跟蹤呼叫方法的頻率。事實證明,啟動路徑上使用的許多函式只被呼叫一次或可能只被呼叫幾次,最佳化它們比不最佳化地執行它們需要更多的時間。然後,當方法的檢測觸發某個閾值時,例如某個方法已執行 30 次,工作項將排隊重新編譯該方法,但這次 JIT 可以對其進行所有最佳化。這被親切地稱為“分層”。重新編譯完成後,該方法的呼叫站點將使用新高度最佳化的彙編程式碼的地址進行修補,以後的呼叫將採用快速路徑。因此,我們獲得了更快的啟動速度和更快的持續吞吐量。至少,這是希望。

然而,一個問題是不適合這種模式的方法。雖然許多對效能敏感的方法確實相對較快並且執行了很多很多次,但也有大量對效能敏感的方法只執行了幾次,甚至可能只執行了一次,但是需要很長時間才能執行,甚至可能是整個過程的持續時間:帶有迴圈的方法。因此,儘管可以透過將 DOTNET_TC_QuickJitForLoops 環境變數設定為 1 來啟用它,但預設情況下分層編譯並未應用於迴圈。我們可以透過使用 .NET 6 嘗試這個簡單的控制檯應用程式來檢視其效果。使用預設值設定,執行這個應用程式:

class Program
{
    static void Main()
    {
        var sw = new System.Diagnostics.Stopwatch();
        while (true)
        {
            sw.Restart();
            for (int trial = 0; trial < 10_000; trial++)
            {
                int count = 0;
                for (int i = 0; i < char.MaxValue; i++)
                    if (IsAsciiDigit((char)i))
                        count++;
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed);
        }

        static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
    }
}

我列印出如下數字:

00:00:00.5734352
00:00:00.5526667
00:00:00.5675267
00:00:00.5588724
00:00:00.5616028

現在,嘗試將 DOTNET_TC_QuickJitForLoops 設定為 1。當我再次執行它時,我得到如下數字:

00:00:01.2841397
00:00:01.2693485
00:00:01.2755646
00:00:01.2656678
00:00:01.2679925

換句話說,在啟用 DOTNET_TC_QuickJitForLoops 的情況下,它花費的時間是不啟用時的 2.5 倍(.NET 6 中的預設設定)。那是因為這個 main 函式永遠不會對其應用最佳化。透過將 DOTNET_TC_QuickJitForLoops 設定為 1,我們說“JIT,請將分層也應用於帶迴圈的方法”,但這種帶迴圈的方法只會被呼叫一次,因此在整個過程中它最終保持在“層” -0”,也就是未最佳化。現在,讓我們在 .NET 7 上嘗試同樣的事情。無論是否設定了環境變數,我都會再次得到這樣的數字:

00:00:00.5528889
00:00:00.5562563
00:00:00.5622086
00:00:00.5668220
00:00:00.5589112

但重要的是,這種方法仍然參與分層。事實上,我們可以透過使用前面提到的 DOTNET_JitDisasmSummary=1 環境變數來確認這一點。當我設定它並再次執行時,我在輸出中看到這些行:

   4: JIT compiled Program:Main() [Tier0, IL size=83, code size=319]
...
   6: JIT compiled Program:Main() [Tier1-OSR @0x27, IL size=83, code size=380]

強調 Main 確實被編譯了兩次。這怎麼可能?堆疊替換。

棧上替換背後的想法是一種方法不僅可以在呼叫之間替換,甚至可以在它執行時替換,當它“在堆疊上”時。除了用於呼叫計數的第 0 層程式碼外,迴圈還用於迭代計數。當迭代次數超過某個限制時,JIT 會編譯該方法的一個高度最佳化的新版本,將所有本地/註冊狀態從當前呼叫轉移到新呼叫,然後跳轉到新方法中的適當位置。我們可以透過使用前面討論的 DOTNET_JitDisasm 環境變數來實際看到這一點。將其設定為 Program:* 以檢視為 Program 類中的所有方法生成的彙編程式碼,然後再次執行應用程式。您應該看到如下輸出:

; Assembly listing for method Program:Main()
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-0 compilation
; MinOpts code
; rbp based frame
; partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       4881EC80000000       sub      rsp, 128
       488DAC2480000000     lea      rbp, [rsp+80H]
       C5D857E4             vxorps   xmm4, xmm4
       C5F97F65B0           vmovdqa  xmmword ptr [rbp-50H], xmm4
       33C0                 xor      eax, eax
       488945C0             mov      qword ptr [rbp-40H], rax

G_M000_IG02:                ;; offset=001FH
       48B9002F0B50FC7F0000 mov      rcx, 0x7FFC500B2F00
       E8721FB25F           call     CORINFO_HELP_NEWSFAST
       488945B0             mov      gword ptr [rbp-50H], rax
       488B4DB0             mov      rcx, gword ptr [rbp-50H]
       FF1544C70D00         call     [Stopwatch:.ctor():this]
       488B4DB0             mov      rcx, gword ptr [rbp-50H]
       48894DC0             mov      gword ptr [rbp-40H], rcx
       C745A8E8030000       mov      dword ptr [rbp-58H], 0x3E8

G_M000_IG03:                ;; offset=004BH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG05

G_M000_IG04:                ;; offset=0059H
       488D4DA8             lea      rcx, [rbp-58H]
       BA06000000           mov      edx, 6
       E8B985AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG05:                ;; offset=0067H
       488B4DC0             mov      rcx, gword ptr [rbp-40H]
       3909                 cmp      dword ptr [rcx], ecx
       FF1585C70D00         call     [Stopwatch:Restart():this]
       33C9                 xor      ecx, ecx
       894DBC               mov      dword ptr [rbp-44H], ecx
       33C9                 xor      ecx, ecx
       894DB8               mov      dword ptr [rbp-48H], ecx
       EB20                 jmp      SHORT G_M000_IG08

G_M000_IG06:                ;; offset=007FH
       8B4DB8               mov      ecx, dword ptr [rbp-48H]
       0FB7C9               movzx    rcx, cx
       FF152DD40B00         call     [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]
       85C0                 test     eax, eax
       7408                 je       SHORT G_M000_IG07
       8B4DBC               mov      ecx, dword ptr [rbp-44H]
       FFC1                 inc      ecx
       894DBC               mov      dword ptr [rbp-44H], ecx

G_M000_IG07:                ;; offset=0097H
       8B4DB8               mov      ecx, dword ptr [rbp-48H]
       FFC1                 inc      ecx
       894DB8               mov      dword ptr [rbp-48H], ecx

G_M000_IG08:                ;; offset=009FH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG10

G_M000_IG09:                ;; offset=00ADH
       488D4DA8             lea      rcx, [rbp-58H]
       BA23000000           mov      edx, 35
       E86585AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG10:                ;; offset=00BBH
       817DB800CA9A3B       cmp      dword ptr [rbp-48H], 0x3B9ACA00
       7CBB                 jl       SHORT G_M000_IG06
       488B4DC0             mov      rcx, gword ptr [rbp-40H]
       3909                 cmp      dword ptr [rcx], ecx
       FF1570C70D00         call     [Stopwatch:get_ElapsedMilliseconds():long:this]
       488BC8               mov      rcx, rax
       FF1507D00D00         call     [Console:WriteLine(long)]
       E96DFFFFFF           jmp      G_M000_IG03

; Total bytes of code 222

; Assembly listing for method Program:<Main>g__IsAsciiDigit|0_0(ushort):bool
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-0 compilation
; MinOpts code
; rbp based frame
; partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       488BEC               mov      rbp, rsp
       894D10               mov      dword ptr [rbp+10H], ecx

G_M000_IG02:                ;; offset=0007H
       8B4510               mov      eax, dword ptr [rbp+10H]
       0FB7C0               movzx    rax, ax
       83C0D0               add      eax, -48
       83F809               cmp      eax, 9
       0F96C0               setbe    al
       0FB6C0               movzx    rax, al

G_M000_IG03:                ;; offset=0019H
       5D                   pop      rbp
       C3                   ret

這裡需要注意一些相關的事情。首先,頂部的註釋強調了這段程式碼是如何編譯的:

; Tier-0 compilation
; MinOpts code

因此,我們知道這是使用最小最佳化(“MinOpts”)編譯的方法的初始版本(“Tier-0”)。其次,注意彙編的這一行:

FF152DD40B00         call     [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]

我們的 IsAsciiDigit 輔助方法很容易內聯,但它沒有被內聯;相反,程式集呼叫了它,實際上我們可以在下面看到為 IsAsciiDigit 生成的程式碼(也稱為“MinOpts”)。為什麼?因為內聯是一種最佳化(一個非常重要的最佳化),它作為第 0 層的一部分被禁用(因為做好內聯的分析也非常昂貴)。第三,我們可以看到 JIT 輸出的程式碼來檢測這個方法。這有點複雜,但我會指出相關部分。首先,我們看到:

C745A8E8030000       mov      dword ptr [rbp-58H], 0x3E8

0x3E8 是十進位制 1,000 的十六進位制值,這是在 JIT 生成方法的最佳化版本之前迴圈需要迭代的預設迭代次數(這可以透過 DOTNET_TC_OnStackReplacement_InitialCounter 環境變數進行配置)。所以我們看到 1,000 被儲存到這個堆疊位置。然後稍後在方法中我們看到這個:

G_M000_IG03:                ;; offset=004BH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG05

G_M000_IG04:                ;; offset=0059H
       488D4DA8             lea      rcx, [rbp-58H]
       BA06000000           mov      edx, 6
       E8B985AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG05:                ;; offset=0067H

生成的程式碼將該計數器載入到 ecx 暫存器中,遞減它,將其儲存回去,然後檢視計數器是否降為 0。如果沒有,程式碼跳到 G_M000_IG05,這是實際程式碼的標籤迴圈的其餘部分。但是,如果計數器確實降為 0,JIT 會繼續將相關狀態儲存到 rcx 和 edx 暫存器中,然後呼叫 CORINFO_HELP_PATCHPOINT 輔助方法。該助手負責觸發最佳化方法的建立(如果尚不存在)、修復所有適當的跟蹤狀態並跳轉到新方法。事實上,如果您再次檢視執行該程式的控制檯輸出,您會看到 Main 方法的另一個輸出:

; Assembly listing for method Program:Main()
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; OSR variant for entry point 0x23
; optimized code
; rsp based frame
; fully interruptible
; No PGO data
; 1 inlinees with PGO data; 8 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0000H
       4883EC58             sub      rsp, 88
       4889BC24D8000000     mov      qword ptr [rsp+D8H], rdi
       4889B424D0000000     mov      qword ptr [rsp+D0H], rsi
       48899C24C8000000     mov      qword ptr [rsp+C8H], rbx
       C5F877               vzeroupper
       33C0                 xor      eax, eax
       4889442428           mov      qword ptr [rsp+28H], rax
       4889442420           mov      qword ptr [rsp+20H], rax
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       8BBC249C000000       mov      edi, dword ptr [rsp+9CH]
       8BB42498000000       mov      esi, dword ptr [rsp+98H]

G_M000_IG02:                ;; offset=0041H
       EB45                 jmp      SHORT G_M000_IG05
                            align    [0 bytes for IG06]

G_M000_IG03:                ;; offset=0043H
       33C9                 xor      ecx, ecx
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       48894B08             mov      qword ptr [rbx+08H], rcx
       488D4C2428           lea      rcx, [rsp+28H]
       48B87066E68AFD7F0000 mov      rax, 0x7FFD8AE66670

G_M000_IG04:                ;; offset=0060H
       FFD0                 call     rax ; Kernel32:QueryPerformanceCounter(long):int
       488B442428           mov      rax, qword ptr [rsp+28H]
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       48894310             mov      qword ptr [rbx+10H], rax
       C6431801             mov      byte  ptr [rbx+18H], 1
       33FF                 xor      edi, edi
       33F6                 xor      esi, esi
       833D92A1E55F00       cmp      dword ptr [(reloc 0x7ffcafe1ae34)], 0
       0F85CA000000         jne      G_M000_IG13

G_M000_IG05:                ;; offset=0088H
       81FE00CA9A3B         cmp      esi, 0x3B9ACA00
       7D17                 jge      SHORT G_M000_IG09

G_M000_IG06:                ;; offset=0090H
       0FB7CE               movzx    rcx, si
       83C1D0               add      ecx, -48
       83F909               cmp      ecx, 9
       7702                 ja       SHORT G_M000_IG08

G_M000_IG07:                ;; offset=009BH
       FFC7                 inc      edi

G_M000_IG08:                ;; offset=009DH
       FFC6                 inc      esi
       81FE00CA9A3B         cmp      esi, 0x3B9ACA00
       7CE9                 jl       SHORT G_M000_IG06

G_M000_IG09:                ;; offset=00A7H
       488B6B08             mov      rbp, qword ptr [rbx+08H]
       48899C24A0000000     mov      gword ptr [rsp+A0H], rbx
       807B1800             cmp      byte  ptr [rbx+18H], 0
       7436                 je       SHORT G_M000_IG12

G_M000_IG10:                ;; offset=00B9H
       488D4C2420           lea      rcx, [rsp+20H]
       48B87066E68AFD7F0000 mov      rax, 0x7FFD8AE66670

G_M000_IG11:                ;; offset=00C8H
       FFD0                 call     rax ; Kernel32:QueryPerformanceCounter(long):int
       488B4C2420           mov      rcx, qword ptr [rsp+20H]
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       482B4B10             sub      rcx, qword ptr [rbx+10H]
       4803E9               add      rbp, rcx
       833D2FA1E55F00       cmp      dword ptr [(reloc 0x7ffcafe1ae34)], 0
       48899C24A0000000     mov      gword ptr [rsp+A0H], rbx
       756D                 jne      SHORT G_M000_IG14

G_M000_IG12:                ;; offset=00EFH
       C5F857C0             vxorps   xmm0, xmm0
       C4E1FB2AC5           vcvtsi2sd  xmm0, rbp
       C5FB11442430         vmovsd   qword ptr [rsp+30H], xmm0
       48B9F04BF24FFC7F0000 mov      rcx, 0x7FFC4FF24BF0
       BAE7070000           mov      edx, 0x7E7
       E82E1FB25F           call     CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
       C5FB10442430         vmovsd   xmm0, qword ptr [rsp+30H]
       C5FB5905E049F6FF     vmulsd   xmm0, xmm0, qword ptr [(reloc 0x7ffc4ff25720)]
       C4E1FB2CD0           vcvttsd2si  rdx, xmm0
       48B94B598638D6C56D34 mov      rcx, 0x346DC5D63886594B
       488BC1               mov      rax, rcx
       48F7EA               imul     rdx:rax, rdx
       488BCA               mov      rcx, rdx
       48C1E93F             shr      rcx, 63
       48C1FA0B             sar      rdx, 11
       4803CA               add      rcx, rdx
       FF1567CE0D00         call     [Console:WriteLine(long)]
       E9F5FEFFFF           jmp      G_M000_IG03

G_M000_IG13:                ;; offset=014EH
       E8DDCBAC5F           call     CORINFO_HELP_POLL_GC
       E930FFFFFF           jmp      G_M000_IG05

G_M000_IG14:                ;; offset=0158H
       E8D3CBAC5F           call     CORINFO_HELP_POLL_GC
       EB90                 jmp      SHORT G_M000_IG12

; Total bytes of code 351

在這裡,我們再次注意到一些有趣的事情。首先,在標題中我們看到了這個:

; Tier-1 compilation
; OSR variant for entry point 0x23
; optimized code

所以我們知道這既是最佳化的“一級”程式碼,也是該方法的“OSR 變體”。其次,請注意不再呼叫 IsAsciiDigit 幫助程式。相反,在該呼叫的位置,我們看到了這一點:

G_M000_IG06:                ;; offset=0090H
       0FB7CE               movzx    rcx, si
       83C1D0               add      ecx, -48
       83F909               cmp      ecx, 9
       7702                 ja       SHORT G_M000_IG08

這是將一個值載入到 rcx 中,從中減去 48(48 是“0”字元的十進位制 ASCII 值)並將結果值與 9 進行比較。聽起來很像我們的 IsAsciiDigit 實現 ((uint)(c - ' 0') <= 9),不是嗎?那是因為它是。幫助程式已成功內聯到這個現在最佳化的程式碼中。

太好了,現在在 .NET 7 中,我們可以在很大程度上避免啟動和吞吐量之間的權衡,因為 OSR 支援分層編譯以應用於所有方法,即使是那些長時間執行的方法。許多 PR 都致力於實現這一點,包括過去幾年的許多 PR,但所有功能在釋出時都被禁用了。感謝 dotnet/runtime#62831 等改進,它在 Arm64 上實現了對 OSR 的支援(以前只實現了 x64 支援),以及 dotnet/runtime#63406dotnet/runtime#65609 修改了 OSR 匯入和 epilogs 的處理方式,dotnet/runtime #65675 預設啟用 OSR(並因此啟用 DOTNET_TC_QuickJitForLoops)。

但是,分層編譯和 OSR 不僅僅與啟動有關(儘管它們在那裡當然非常有價值)。它們還涉及進一步提高吞吐量。儘管分層編譯最初被設想為一種在不損害吞吐量的情況下最佳化啟動的方法,但它已經變得遠不止於此。 JIT 可以在第 0 層期間瞭解有關方法的各種資訊,然後將其用於第 1 層。例如,執行第 0 層程式碼這一事實意味著該方法訪問的任何靜態都將被初始化,這意味著任何只讀靜態不僅會在第 1 層程式碼執行時被初始化,而且它們的價值觀永遠不會改變。這反過來意味著原始型別(例如 bool、int 等)的任何只讀靜態都可以像常量一樣對待,而不是靜態只讀欄位,並且在第 1 層編譯期間,JIT 可以最佳化它們,就像它最佳化一個常量。例如,在將 DOTNET_JitDisasm 設定為 Program:Test 後嘗試執行這個簡單的程式:

using System.Runtime.CompilerServices;

class Program
{
    static readonly bool Is64Bit = Environment.Is64BitProcess;

    static int Main()
    {
        int count = 0;
        for (int i = 0; i < 1_000_000_000; i++)
            if (Test())
                count++;
        return count;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static bool Test() => Is64Bit;
}

當我這樣做時,我得到以下輸出:

; Assembly listing for method Program:Test():bool
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-0 compilation
; MinOpts code
; rbp based frame
; partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       4883EC20             sub      rsp, 32
       488D6C2420           lea      rbp, [rsp+20H]

G_M000_IG02:                ;; offset=000AH
       48B9B8639A3FFC7F0000 mov      rcx, 0x7FFC3F9A63B8
       BA01000000           mov      edx, 1
       E8C220B25F           call     CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
       0FB60545580C00       movzx    rax, byte  ptr [(reloc 0x7ffc3f9a63ea)]

G_M000_IG03:                ;; offset=0025H
       4883C420             add      rsp, 32
       5D                   pop      rbp
       C3                   ret

; Total bytes of code 43

; Assembly listing for method Program:Test():bool
; 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

G_M000_IG02:                ;; offset=0000H
       B801000000           mov      eax, 1

G_M000_IG03:                ;; offset=0005H
       C3                   ret

; Total bytes of code 6

請注意,我們再次看到 Program:Test 的兩個輸出。首先,我們看到“第 0 層”程式碼,它正在訪問靜態(注意呼叫 CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE 指令)。但隨後我們看到“Tier-1”程式碼,其中所有開銷都消失了,取而代之的是 mov eax, 1。由於必須執行“Tier-0”程式碼才能使其分層, “Tier-1”程式碼是在知道 static readonly bool Is64Bit 欄位的值為 true (1) 的情況下生成的,因此該方法的全部內容是將值 1 儲存到用於返回值的 eax 暫存器中。

這非常有用,以至於現在在編寫元件時都考慮到了分層。考慮一下新的 Regex 原始碼生成器,這將在本文後面討論(Roslyn 原始碼生成器是幾年前推出的;就像 Roslyn 分析器如何能夠插入編譯器並根據編譯器的所有資料進行額外的診斷一樣從原始碼中學習,Roslyn 原始碼生成器能夠分析相同的資料,然後使用額外的源進一步擴充編譯單元)。正規表示式源生成器在 dotnet/runtime#67775 中應用了基於此的技術。 Regex 支援設定一個程式範圍的超時,該超時應用於未明確設定超時的 Regex 例項。這意味著,即使設定這種程式範圍的超時非常罕見,Regex 原始碼生成器仍然需要輸出與超時相關的程式碼,以備不時之需。它透過輸出一些像這樣的助手來做到這一點:

static class Utilities
{
    internal static readonly TimeSpan s_defaultTimeout = AppContext.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") is TimeSpan timeout ? timeout : Timeout.InfiniteTimeSpan;
    internal static readonly bool s_hasTimeout = s_defaultTimeout != Timeout.InfiniteTimeSpan;
}

然後它在這樣的呼叫站點使用它:

if (Utilities.s_hasTimeout)
{
    base.CheckTimeout();
}

在第 0 層中,這些檢查仍將在彙編程式碼中發出,但在吞吐量很重要的第 1 層中,如果尚未設定相關的 AppContext 開關,則 s_defaultTimeout 將為 Timeout.InfiniteTimeSpan,此時 s_hasTimeout 將為錯誤的。並且由於 s_hasTimeout 是一個靜態只讀布林值,JIT 將能夠將其視為一個常量,並且所有條件如 if (Utilities.s_hasTimeout) 將被視為等於 if (false) 並從彙編程式碼中完全消除為死程式碼。

但是,這有點舊聞了。自從 .NET Core 3.0 中引入分層編譯以來,JIT 已經能夠進行這樣的最佳化。不過,現在在 .NET 7 中,有了 OSR,它也可以預設為帶迴圈的方法這樣做(從而啟用像正規表示式這樣的情況)。然而,OSR 的真正魔力在與另一個令人興奮的功能結合使用時才會發揮作用:動態 PGO。

原文連結

Performance Improvements in .NET 7

知識共享許可協議

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

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

如有任何疑問,請與我聯絡 (MingsonZheng@outlook.com)

相關文章