天天看點

在.Net 7性能改進-棧上替換(OSR)

作者:秋風技術

前言

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

//原文位址: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#loop-hoisting-and-cloning           

On-Stack Replacement(棧上替換),是在.NET 7中實作JIT的最酷的功能之一.但要真正了解OSR,我們首先需要了解分層編譯,是以快速回顧一下…

使用JIT編譯器的托管環境必須處理的問題之一是啟動和吞吐量之間的權衡.從曆史上看,編譯器優化的任務是生成執行更快的代碼,以便在應用程式或服務運作時實作盡可能最佳的吞吐量.但這種優化需要分析,需要時間,執行所有這些工作會導緻啟動時間增加,因為程式的所有代碼(例如,在web伺服器可以服務第一個請求之前需要運作的所有代碼)都需要編譯.是以,JIT編譯器需要做出權衡:以更長的啟動時間為代價提高吞吐量,或以降低吞吐量為代價提高啟動時間.對于某些類型的應用程式和服務,折衷是一個簡單的調用,例如,如果您的服務啟動一次,然後運作幾天,額外幾秒的啟動時間并不重要,或者如果您是一個控制台應用程式,将要進行快速計算并退出,啟動時間才是最重要的.

但是,JIT如何知道它處于哪個場景中,我們真的希望每個開發人員都知道這些設定和權衡,并相應地配置他們的每個應用程式嗎?對此的一個答案是提前編譯,它在.NET中采用了多種形式.例如,所有的核心庫都是“crossgen”,這意味着它們已經通過一個工具運作,該工具生成了前面提到的R2R格式,生成的二進制檔案包含彙編代碼,隻需要稍加調整即可實際執行;不是每個方法都可以為其生成代碼,但足以顯著減少啟動時間.當然,這種方法也有其自身的缺點,例如,JIT編譯器的一個承諾是,它可以利用目前機器/程序的知識進行最佳優化,是以,例如,R2R映像必須假設某個基線指令集(例如,哪些矢量化指令可用),而JIT可以看到哪些實際可用并使用最佳.“分層編譯”提供了另一個答案,無論是否使用這些其他提前(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倍(在.NET6中).這是因為這個主函數從未應用過優化.通過将DOTNET_TC_QuickJitForLoops設定為1,我們說“JIT,請将分層也應用于具有循環的方法”,但這種具有循環的方式隻調用一次,是以在整個過程中,它最終保持在“tier-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]           

棧上替換的思想是,方法不僅可以在調用之間替換,而且可以在“棧上”執行時替換.除了為調用計數檢測第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”)編譯的方法的初始版本(“第0層”).第二,注意裝配的這一行:

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

我們的IsAsciiDigit輔助方法是簡單的可内聯的,但它沒有内聯;相反,程式集有一個對它的調用,事實上,我們可以在下面看到為IsAsciiDigit生成的代碼(也是“MinOpts”).為什麼?因為内聯是一種優化(非常重要的優化),但在tier-0中被禁用(因為為了進行良好的内聯分析也非常昂貴).第三,我們可以看到JIT輸出到檢測該方法的代碼.這有點複雜,但我會指出相關部分.首先,我們看到:

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

0x3E8是十進制1000的十六進制值,這是在JIT生成方法的優化版本之前循環需要疊代的預設疊代次數(可通過環境變量DOTNET_TC_OnStackReplacement_InitialCounter進行配置).是以,我們看到1000存儲在這個堆棧位置.然後,在該方法的後面,我們看到:

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 helper方法.該助手負責觸發優化方法的建立(如果它還不存在),修複所有适當的跟蹤狀态,并跳轉到新方法.事實上,如果您再次檢視運作程式的控制台輸出,您将看到主方法的另一個輸出:

// 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           

是以,我們知道這既是優化的“第1層”代碼,也是該方法的“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進行比較.聽起來很像我們的IsasciidGit實作( (uint)(c-“0”)<=9 ),不是嗎?這是因為它是.幫助程式成功地内聯到現在優化的代碼中.

很好,現在在.NET7中,我們可以在很大程度上避免啟動和吞吐量之間的權衡,因為OSR使分層編譯能夠應用于所有方法,甚至是那些長期運作的方法.許多送出開始啟用此功能,包括過去幾年中的許多送出,但是所有的功能在釋出時都被禁用了.由于dotnet/runtime#62831在Arm64上實作了對OSR的支援(之前僅實作了x64支援),以及dotnet/Runtime#63406和dotnet/runtime#65609修改了如何OSR導入和epilog的處理,dotnet/runtime#65675在預設情況下啟用OSR(DOTNET_TC_QuickJitForLoops=1).

但是,分層編譯和OSR不僅僅是關于啟動(盡管它們在那裡當然非常有價值).他們還将進一步提高吞吐量.盡管分層編譯最初被設想為一種在不影響吞吐量的情況下優化啟動的方法,但它已經遠遠不止于此.JIT可以在tier-0期間了解到關于方法的各種資訊,然後可以用于tier-1.例如,執行的tier-2代碼意味着該方法通路的任何靜态都将被初始化,這意味着任何隻讀靜态不僅在執行tier-3代碼時已經初始化,而且它們的值永遠不會改變.這反過來意味着,任何原始類型的隻讀靜态(如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           

注意,我們再次看到程式的兩個輸出:Test方法的彙編代碼.首先,我們看到“Tier-0”代碼,它正在通路靜态(注意調用CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE指令).但是,我們看到了“Tier-1”代碼,其中所有的開銷都消失了,取而代之的是mov eax,1.由于必須執行“Tier-0”代碼才能分層,“Tier-2”代碼是在知道靜态隻讀bool IS64位字段的值為真的情況下生成的(1),是以,整個方法是将值1存儲到用于傳回值的eax寄存器中.

這非常有用,現在編寫元件時都考慮到了分層.考慮一下新的Regex源代碼生成器,這将在後面的文章中讨論(Roslyn源代碼生成器是幾年前引入的;就像Roslyn分析器能夠插入編譯器并基于編譯器從源代碼中學習到的所有資料提供額外的診斷一樣,Roslyn源代碼生成器能夠分析相同的資料,然後用額外的源代碼進一步增加編譯單元).Regex源生成器應用基于此的dotnet/runtime#67775技術.Regex支援設定流程範圍的逾時,該逾時将應用于沒有顯式設定逾時的Regex執行個體.這意味着,即使設定這樣一個程序範圍的逾時非常罕見,Regex源生成器仍然需要輸出與逾時相關的代碼,以備需要.它通過輸出一些helper來實作,像這樣:

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.infinittimeespan,此時s_hasTimeout将為false.由于s_hasTimeout是一個靜态的隻讀bool, JIT将能夠将其視為const,并且所有的條件,如if (Utilities.s_hasTimeout)将被視為與if (false)相等,并從彙編代碼中完全清除為死代碼.

但是,這有點老生常談了.自從.NET Core 3.0引入分層編譯以來,JIT就能夠進行這樣的優化.不過,現在在.NET 7中,在OSR中,它也可以在預設情況下對帶有循環的方法進行優化(是以啟用了類似于正規表達式的情況).然而,當OSR與另一個令人興奮的特性:動态PGO相結合時,OSR的真正魔力開始發揮作用.

因JIT部分内容太多,這裡進行拆分,PGO拆分為一篇博文.

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

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

繼續閱讀