後端工坊 2026 年 9 月 16 日

2026-09-16 — .NET 11 Runtime Async:同步完成配置歸零,掛起路徑減半

primary=https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/ primary=https://github.com/dotnet/runtime/pull/121273 primary=https://github.com/dotnet/runtime/pull/127439 primary=https://github.com/dotnet/runtime/pull/127488 primary=https://github.com/dotnet/runtime/pull/122167 primary=https://github.com/dotnet/runtime/pull/122946

.NET 11 Runtime Async:同步完成配置歸零,掛起路徑減半

Microsoft DevBlogs(.NET Blog)· 2026-09-15

.NET 11 把 async 方法的執行單位,從編譯器展開的狀態機物件換成執行期直接管理的堆疊續體(continuation),同步完成的路徑配置直接歸零。這項機制稱為 Runtime Async,由 Distinguished Engineer Stephen Toub 發表於 .NET Blog,目前以專案旗標選擇性開啟,官方預告 .NET 12 起會預設開啟。

背景

C# 的 async/await 已經用了超過十年,讓非同步程式碼寫起來跟同步程式碼一樣直覺。但編譯器背後其實是把整個方法展開成一個實作 IAsyncStateMachine 的類別:區域變數搬進欄位,每個 await 點變成跳躍表的一個分支,遇到還沒完成的操作時再把整個物件包進 Task 丟回呼叫端。

問題在於即使操作當下就同步完成——這在真實服務裡是常態,例如快取命中、記憶體內查詢——呼叫端仍要付出建立狀態機物件、裝箱、後續 GC 的成本。方法巢狀愈深、await 層數愈多,這筆固定成本就被重複攤提愈多次。

核心改動:Runtime Async

Runtime Async 讓執行期直接管理續體,不再依賴編譯器生成的狀態機類別。啟用方式是在專案檔加一行旗標,不需要新的 C# 語法,也不必開 LangVersion=preview

<Features>$(Features);runtime-async=on</Features>

官方用兩層呼叫鏈的微觀基準測試,對照傳統狀態機(Classic)與 Runtime Async(Runtime)的差異:

情境ClassicRuntime Async比例
同步完成21.221 ns,144 B6.151 ns,0 B0.29x 時間
掛起後恢復254.139 ns,248 B116.927 ns,168 B0.46x 時間、0.68x 配置
例外堆疊深度 10 層19.469 μs,15.13 KB5.885 μs,2.13 KB0.30x 時間、0.14x 配置
例外堆疊深度 30 層51.302 μs,84.2 KB10.721 μs,5.71 KB0.21x 時間、0.07x 配置

連編譯後的機器碼都變小:同一段程式碼在 Runtime Async 下產生的機器碼從 10,752 位元組降到 5,632 位元組,減少 52%。

JIT 去抽象化:邊界檢查與裝箱優化

除了 async 重寫,.NET 11 的 JIT 也拿掉了更多因抽象層留下的邊界檢查與裝箱。例如下面這段用 list pattern 比對字串前綴的程式碼:

private static int Classify(ReadOnlySpan<char> name) =>
    name switch
    {
        [] => 0,
        [':'] => 1,
        [':', not ':', ..] => 10 + name[0] + name[1],
        _ => 3
    };

.NET 10 產生的組合語言裡還留著呼叫 CORINFO_HELP_RNGCHKFAIL 的邊界檢查失敗處理,.NET 11 直接把它拿掉(dotnet/runtime#121273)。連續存取陣列固定索引的加總也把重複邊界檢查合併成一次,耗時從 2.958 ns 降到 1.828 ns,比例 0.62x(dotnet/runtime#127439);讀取 span 尾端 4 個位元組的輔助函式,機器碼從 73 位元組壓到 28 位元組(dotnet/runtime#127488)。可為 null 的實質型別裝箱路徑也有類似優化,常見情境下配置直接消失,耗時降到 0.45x(dotnet/runtime#122167)。foreach 用到的列舉器(enumerator)過去在某些情境下也得先裝箱成介面型別才能走迭代,.NET 11 讓編譯器把這類列舉器直接配置在堆疊上,消除一次 32 位元組的堆積配置(dotnet/runtime#122946)。泛型虛擬方法(GVM)呼叫過去為了處理型別參數,也常常需要額外的裝箱與間接呼叫,這次同樣拿掉了固定的 24 位元組配置。

影響範圍

Runtime Async 是選擇性功能,要在 .NET 11 專案上手動加旗標才會生效——沒開旗標的專案行為不變,仍走原本的編譯器狀態機。高吞吐、深層巢狀 await 呼叫的服務(例如 API gateway、RPC 中介層)升級後值得評估開啟,尤其同步完成比例高的路徑,GC 壓力下降最直接。

啟用前建議先確認第三方函式庫、AOT 編譯或原始碼產生器是否依賴目前的狀態機物件結構(例如用反射檢查 IAsyncStateMachine),因為底層執行模型換了一套。邊界檢查與裝箱的優化則不需要任何動作,只要用 .NET 11 SDK 重新編譯,符合模式的程式碼會自動拿掉多餘檢查,效益大小取決於程式碼是否大量使用 Span<T>、list pattern、可為 null 的實質型別。

原始來源:Performance Improvements in .NET 11dotnet/runtime PR #121273


End of article
0
Would love your thoughts, please comment.x
()
x