一、為什么要關注.NET異常處理的性能
隨著現代云原生、高并發、分布式場景的大量普及,異常處理(Exception Handling)早已不再只是一個冷僻的代碼路徑。在高復雜度的微服務、網絡服務、異步編程環境下,服務依賴的外部資源往往不可靠,偶發失效或小概率的“雪崩”場景已經十分常見。實際系統常常在高頻率地拋出、傳遞、捕獲異常,異常處理性能直接影響著系統的恢復速度、吞吐量,甚至是穩定性與容錯邊界。
.NET平臺在異常處理性能方面長期落后于C++、Java等同類主流平臺——業內社區多次對比公開跑分就證實了這一點,.NET 8時代雖然差距有所縮小,但在某些高并發/異步等極端場景下,異常高開銷持續困擾社區和大廠工程師。于是到了.NET 9,終于迎來了一次代際變革式的性能飛躍,拋出/捕獲異常的耗時基本追平C++,成為技術圈最關注的.NET runtime底層事件之一。
二、實測:.NET 9異常處理提速直觀對比
1. 測試代碼
最經典的異常性能測試如下——C# 和 Java的實現基本一致
C#:
class ExceptionPerformanceTest
{
public void Test()
{
var stopwatch = Stopwatch.StartNew();
ExceptionTest(100_000);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
}
private void ExceptionTest(long times)
{
for (int i = 0; i < times; i++)
{
try
{
throw new Exception();
}
catch (Exception ex)
{
// Ignore
}
}
}
}
Java:
public class ExceptionPerformanceTest {
public void Test() {
Instant start = Instant.now();
ExceptionTest(100_000);
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println(duration.toMillis());
}
private void ExceptionTest(long times) {
for (int i = 0; i < times; i++) {
try {
throw new Exception();
} catch (Exception ex) {
// Ignore
}
}
}
}
2. 早期測試結果(以.NET Core 2.2時代為例)
.NET 的異常拋出/捕獲速度相較慢得多。但到了.NET 8后期和.NET 9,基準成績已翻天覆地:
3. 新時代基準結果(.NET 8 vs .NET 9)
借助 BenchmarkDotNet 可以更科學對比:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Environments;
namespace ExceptionBenchmark
{
[Config(typeof(Config))]
[HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio, Column.Gen0, Column.Gen1)]
[MemoryDiagnoser]
public class ExceptionBenchmark
{
private const int NumberOfIterations = 1000;
[Benchmark]
public void ThrowAndCatchException()
{
for (int i = 0; i < NumberOfIterations; i++)
{
try
{
ThrowException();
}
catch
{
// Exception caught - the cost of this is what we're measuring
}
}
}
private void ThrowException()
{
throw new System.Exception("This is a test exception.");
}
private class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80).AsBaseline());
AddJob(Job.Default.WithId(".NET 9").WithRuntime(CoreRuntime.Core90));
SummaryStyle =
SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
}
}
}
}
如下圖結果,拋出+捕獲1000次異常:
- .NET 8:每次約 12μs
- .NET 9:每次減少至約 2.8μs (約76~80%提升)

.NET 9的性能提升幾乎讓EH成本降到C++/Java同量級,成為托管平臺的性能標桿之一。
三、.NET早期異常處理為何如此之慢?
1. 策略層面的歷史誤區
傳統觀點認為:“異常只為異常流程準備,主業務應以if/else或TryXXX等方式避免極端異常分支”。社區和官方因此忽視了EH系統的極限性能,無論架構設計還是細節實現都欠缺優化,反映在:
- 內部優先保證兼容性和健壯性,而不是高性能
- 代碼中凡是熱路徑,都讓開發者“自覺避開異?!?/li>
近年來,現代服務常常:
- 依賴于“不可靠資源” (如網絡、外部API、云存儲),短暫失效隨時發生
- 借助基于
async/await
的異步編程,異常常常跨棧、跨線程重拋 - 在微服務系統中,單點故障可能導致“異常風暴”,大量請求因依賴故障極短時間內批量失敗
這些場景下,異常處理已極易成為性能瓶頸,應用的可用性與SLA依賴于異?;謴退俣?。
2. CoreCLR/Mono 異常實現機制的先天劣勢
Windows實現
Name | Exc % | Exc | Inc % | Inc |
---|
ntdll!RtlpxLookupFunctionTable | 11.4 | 4,525 | 11.4 | 4,525 |
ntdll!RtlpUnwindPrologue | 11.2 | 4,441 | 11.2 | 4,441 |
ntdll!RtlLookupFunctionEntry | 7.2 | 2,857 | 28.4 | 11,271 |
ntdll!RtlpxVirtualUnwind | 6.5 | 2,579 | 17.7 | 7,020 |
ntdll!RtlpLookupDynamicFunctionEntry | 3.6 | 1,425 | 9.8 | 3,889 |
coreclr!EEJitManager::JitCodeToMethodInfo | 2.9 | 1,167 | 2.9 | 1,167 |
ntdll!RtlVirtualUnwind | 2.9 | 1,137 | 17.9 | 7,099 |
ntoskrnl!EtwpWriteUserEvent | 2.5 | 990 | 4.3 | 1,708 |
coreclr!ExceptionTracker::ProcessManagedCallFrame | 2.4 | 941 | 18.7 | 7,405 |
coreclr!ProcessCLRException | 2.4 | 938 | 93.3 | 36,969 |
ntdll!LdrpDispatchUserCallTarget | 2.2 | 871 | 2.2 | 871 |
coreclr!ExecutionManager::FindCodeRangeWithLock | 2.2 | 868 | 2.2 | 868 |
coreclr!memset | 2.0 | 793 | 2.0 | 793 |
coreclr!ExceptionTracker::ProcessOSExceptionNotification | 1.9 | 742 | 31.9 | 12,622 |
coreclr!SString::Replace | 1.8 | 720 | 1.8 | 720 |
ntoskrnl!EtwpReserveTraceBuffer | 1.8 | 718 | 1.8 | 718 |
coreclr!FillRegDisplay | 1.8 | 709 | 1.8 | 709 |
ntdll!NtTraceEvent | 1.7 | 673 | 7.1 | 2,803 |
Unix/Linux實現
實際CPU性能熱點采樣發現:
- libgcc_s.so.1/_Unwind_Find_FDE等C++異常系統函數占用近13%的熱點
- 托管代碼層大量鏈表遍歷/鎖(ExecutionManager::FindCodeRangeWithLock等)
- 多線程/多異常場景下lock惡性競爭,棧查找速度極慢
Overhead | Shared Object | Symbol |
---|
+ 8,29% | libgcc_s.so.1 | [.] _Unwind_Find_FDE |
+ 2,51% | libc.so.6 | [.] __memmove_sse2_unaligned_erms |
+ 2,14% | ld-linux-x86-64.so.2 | [.] _dl_find_object |
+ 1,94% | libstdc++.so.6.0.30 | [.] __gxx_personality_v0 |
+ 1,85% | libgcc_s.so.1 | [.] 0x00000000000157eb |
+ 1,77% | libc.so.6 | [.] __memset_sse2_unaligned_erms |
+ 1,36% | ld-linux-x86-64.so.2 | [.] __tls_get_addr |
+ 1,28% | libcoreclr.so | [.] ExceptionTracker::ProcessManagedCallFrame |
+ 1,26% | libcoreclr.so | [.] apply_reg_state |
+ 1,12% | libcoreclr.so | [.] OOPStackUnwinderAMD64::UnwindPrologue |
+ 1,08% | libgcc_s.so.1 | [.] 0x0000000000016990 |
+ 1,08% | libcoreclr.so | [.] ExceptionTracker::ProcessOSExceptionNotification |
額外開銷
- 每次拋出異常需清空/復制完整CONTEXT結構(Windows上下文),單次就近1KB數據
- 捕獲棧信息、生成調試輔助、捕獲完整stacktrace等都增加明顯延遲
3. Async/多線程場景放大性能損耗
現代C#的async/await廣泛出現。每遇到await斷點,異常需在async狀態機多次catch/throw重入口,即使只有1層異常,實際走了多倍catch分支。多線程下,本地堆?;ゲ魂P聯,所有棧回溯、元數據查找都需走OS或本地鎖/鏈表,進一步拉低性能擴展性。
4. 跨平臺和歷史兼容包袱
因Windows/Unix兩套機制并存,大量platform abstraction和邊界容錯邏輯,極大增加了維護成本和bug風險。每一次異??缃缍夹枰厥馓幚?,開發運維和調優都十分困難。
以下是.NET9以前多線程和單線程異常拋出耗時,可以看到隨著堆棧深度的增加,拋出異常要花費的世界越來越長。


四、技術極客視角:.NET 9徹底變革的細節原理
.NET 9之所以實現了異常處理的性能“質變”,核心思路是吸收NativeAOT的極簡托管實現,將主力流程自托管直接管理,核心只依賴native stack walker完成功能邊界,避免一切反復嵌套或冗余環節。
(一)NativeAOT異常處理架構剖析
1. 設計變革
- 完全托管驅動主流程
異常的捕獲、catch分派、finally查找、異常對象/類型的元數據查找等主環節,全部寫成托管代碼(C#邏輯)。 - native code僅負責棧幀展開(stack walking)
需要時才調用本地API(libunwind/Windows API)由native/cross平臺實現stack frame的move next/遍歷,極簡無其他依賴。 - 無C++異常橋接,這樣省去了_os-unwind、double catch-rethrow等所有歷史冗余。
- 功能單純、易于調優和定制,不到300行關鍵路徑代碼。
2. 優勢分析
- 代碼極簡,熱路徑關鍵點完全可控
- 不存在異步場景下的“狀態機分支回溯”性能急劇下滑
- 托管邏輯易于內聯、緩存
- Native代碼只做最小功能、極易換實現/裁剪
- 性能調優點固定且標志性突出(大部分耗時都在stack walker/元數據cache里)
- 兼容可擴展,后續想做特殊異常/自定義類型極為簡便
3. 技術細節
- 異常對象的stacktrace/元數據在托管代碼按需附加
- 若已知異常只在本地代碼路徑,完全可繞開“不需要的”full stacktrace/callstack/diagnostic等場景
- 可以整合cache優化,如將每個托管JIT幀的元數據查找結果放本地線程緩存(甚至開啟pgo熱點分支識別,見后續)。
(二).NET 9實現與補全 —— 同步NativeAOT設計到CoreCLR
在.NET 9,團隊把NativeAOT的異常處理模式移植到了CoreCLR上。主要技術變更包括:
- 將異常展開、catch/finally分派等環節全部搬到托管主流程
- native helper只做最小的stack frame展開,與垃圾回收棧遍歷接口復用(易于維護)
- 強化托管級緩存與元數據管理。關鍵鏈表遍歷全部升級成緩存/高速哈希表,一舉解決了多線程、深棧、頻繁異常場景下的scalability困境
- 釘死所有多余的C++ throw/catch——對Unix/Windows都生效
- 為Async/Await生成優化代碼路徑,避免多次重復拋出/捕獲
工程落地與效果
- 性能測試實測,異常處理耗時降幅約76%~80%,多線程/高并發效果更好
- 性能剖析熱點:主要耗時已縮小到stack walker和關鍵數據結構哈希效率上,其他已近極致
- 全平臺統一,無歷史特殊兼容路徑、包袱
真實圖片示例


(三)可進一步優化的場景與細節
熱點分支profile(PGO)
- 異常的“常用路徑”可被profile,按pgo機制熱路徑內聯/重編排邏輯
- 比如async await狀態機里常拋異常的分支inline獲得最佳cache局部性
Unwind Section緩存/優化
雙檢省棧trace與細粒度采集
- 支持僅按需采集stacktrace(避免捕獲所有調試信息)
特殊場景快速捕獲(業務異常/操作性異常)
- 通過拓展托管catch塊類型,可以極簡分為業務異常與系統異常,實現“無棧捕獲”,加速高頻捕獲型異常(如EndOfData、ParseError等流控制型異常)
異步異常統一延遲捕獲傳遞
- 在沒有用戶自定義try塊的async方法中,捕獲異常僅保存,真正拋出延遲到非異常主流程結束前即可。這將極大降低狀態機驅動的拋出/捕獲次數。
六、總結展望
.NET 9通過徹底擁抱NativeAOT極簡式的托管異常處理體系,把歷史包袱(OS-Specific/C++ Exception Bridge/冗余鏈表&鎖/多次catch-rethrow)一舉清除,大幅釋放了異常路徑的性能潛力。這一變革支撐了.NET在微服務、云原生、異步并發等新主流場景下的頂級運行時表現。未來,隨著堆棧展開、元數據cache自適應等不斷迭代,.NET有望成為托管平臺的異常處理性能“天花板”。
轉自https://www.cnblogs.com/InCerry/p/-/dotnet-9-exception-pref-improve
該文章在 2025/6/6 10:18:35 編輯過