原文 | 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#63406 和 dotnet/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)