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

原文 | 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/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章