一、為什么要關(guān)注.NET異常處理的性能
隨著現(xiàn)代云原生、高并發(fā)、分布式場(chǎng)景的大量普及,異常處理(Exception Handling)早已不再只是一個(gè)冷僻的代碼路徑。在高復(fù)雜度的微服務(wù)、網(wǎng)絡(luò)服務(wù)、異步編程環(huán)境下,服務(wù)依賴的外部資源往往不可靠,偶發(fā)失效或小概率的“雪崩”場(chǎng)景已經(jīng)十分常見(jiàn)。實(shí)際系統(tǒng)常常在高頻率地拋出、傳遞、捕獲異常,異常處理性能直接影響著系統(tǒng)的恢復(fù)速度、吞吐量,甚至是穩(wěn)定性與容錯(cuò)邊界。
.NET平臺(tái)在異常處理性能方面長(zhǎng)期落后于C++、Java等同類主流平臺(tái)——業(yè)內(nèi)社區(qū)多次對(duì)比公開跑分就證實(shí)了這一點(diǎn),.NET 8時(shí)代雖然差距有所縮小,但在某些高并發(fā)/異步等極端場(chǎng)景下,異常高開銷持續(xù)困擾社區(qū)和大廠工程師。于是到了.NET 9,終于迎來(lái)了一次代際變革式的性能飛躍,拋出/捕獲異常的耗時(shí)基本追平C++,成為技術(shù)圈最關(guān)注的.NET runtime底層事件之一。
二、實(shí)測(cè):.NET 9異常處理提速直觀對(duì)比
1. 測(cè)試代碼
最經(jīng)典的異常性能測(cè)試如下——C# 和 Java的實(shí)現(xiàn)基本一致
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. 早期測(cè)試結(jié)果(以.NET Core 2.2時(shí)代為例)
.NET 的異常拋出/捕獲速度相較慢得多。但到了.NET 8后期和.NET 9,基準(zhǔn)成績(jī)已翻天覆地:
3. 新時(shí)代基準(zhǔn)結(jié)果(.NET 8 vs .NET 9)
借助 BenchmarkDotNet 可以更科學(xué)對(duì)比:
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);
}
}
}
}
如下圖結(jié)果,拋出+捕獲1000次異常:
- .NET 8:每次約 12μs
- .NET 9:每次減少至約 2.8μs (約76~80%提升)

.NET 9的性能提升幾乎讓EH成本降到C++/Java同量級(jí),成為托管平臺(tái)的性能標(biāo)桿之一。
三、.NET早期異常處理為何如此之慢?
1. 策略層面的歷史誤區(qū)
傳統(tǒng)觀點(diǎn)認(rèn)為:“異常只為異常流程準(zhǔn)備,主業(yè)務(wù)應(yīng)以if/else或TryXXX等方式避免極端異常分支”。社區(qū)和官方因此忽視了EH系統(tǒng)的極限性能,無(wú)論架構(gòu)設(shè)計(jì)還是細(xì)節(jié)實(shí)現(xiàn)都欠缺優(yōu)化,反映在:
- 內(nèi)部?jī)?yōu)先保證兼容性和健壯性,而不是高性能
- 代碼中凡是熱路徑,都讓開發(fā)者“自覺(jué)避開異?!?/li>
近年來(lái),現(xiàn)代服務(wù)常常:
- 依賴于“不可靠資源” (如網(wǎng)絡(luò)、外部API、云存儲(chǔ)),短暫失效隨時(shí)發(fā)生
- 借助基于
async/await
的異步編程,異常常??鐥?、跨線程重拋 - 在微服務(wù)系統(tǒng)中,單點(diǎn)故障可能導(dǎo)致“異常風(fēng)暴”,大量請(qǐng)求因依賴故障極短時(shí)間內(nèi)批量失敗
這些場(chǎng)景下,異常處理已極易成為性能瓶頸,應(yīng)用的可用性與SLA依賴于異?;謴?fù)速度。
2. CoreCLR/Mono 異常實(shí)現(xiàn)機(jī)制的先天劣勢(shì)
Windows實(shí)現(xiàn)
采用Windows的Structured Exception Handling (SEH),異常拋出后,OS內(nèi)核統(tǒng)一回溯堆棧、查找/觸發(fā)catch和finally,且需要“雙遍遍歷”棧幀(第一次查catch、第二次觸發(fā)catch/finally,源數(shù)據(jù)由Windows維護(hù))
Structured Exception Handling(結(jié)構(gòu)化異常處理,簡(jiǎn)稱 SEH)是微軟 Windows 操作系統(tǒng)上一種異常處理機(jī)制,主要用于捕獲和處理程序運(yùn)行過(guò)程中產(chǎn)生的異常,如訪問(wèn)違規(guī)(Access Violation)、除零錯(cuò)誤、非法指令等。在 Windows 平臺(tái)上,SEH 被底層編譯器和系統(tǒng)廣泛支持。
用戶層主要通過(guò)回調(diào)介入,絕大多數(shù)性能消耗“鎖死”在OS堆棧查找、回調(diào)和上下文切換中,優(yōu)化空間很小
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實(shí)現(xiàn)
沒(méi)有SEH,只能自己模擬
采用C++異常,異常拋出后靠libgcc/libunwind的_C++機(jī)制回溯托管棧,但需“橋接”托管/本地的邊界,異常對(duì)象需反復(fù)throw/catch
,初始化/過(guò)濾時(shí)會(huì)有多次C++異常嵌套傳遞
libunwind 是一個(gè)開源的棧回溯庫(kù),主要用于在運(yùn)行時(shí)獲取和操作調(diào)用棧,從而支持異常處理、調(diào)試和崩潰分析等功能。
托管運(yùn)行時(shí)(如ExecutionManager) 需要頻繁做函數(shù)表和異常元數(shù)據(jù)線性遍歷(鏈表查找),并發(fā)場(chǎng)景下會(huì)有大量鎖競(jìng)爭(zhēng),極易成為瓶頸
實(shí)際CPU性能熱點(diǎn)采樣發(fā)現(xiàn):
- libgcc_s.so.1/_Unwind_Find_FDE等C++異常系統(tǒng)函數(shù)占用近13%的熱點(diǎn)
- 托管代碼層大量鏈表遍歷/鎖(ExecutionManager::FindCodeRangeWithLock等)
- 多線程/多異常場(chǎng)景下lock惡性競(jìng)爭(zhēng),棧查找速度極慢
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 |
額外開銷
- 每次拋出異常需清空/復(fù)制完整CONTEXT結(jié)構(gòu)(Windows上下文),單次就近1KB數(shù)據(jù)
- 捕獲棧信息、生成調(diào)試輔助、捕獲完整stacktrace等都增加明顯延遲
3. Async/多線程場(chǎng)景放大性能損耗
現(xiàn)代C#的async/await廣泛出現(xiàn)。每遇到await斷點(diǎn),異常需在async狀態(tài)機(jī)多次catch/throw重入口,即使只有1層異常,實(shí)際走了多倍catch分支。多線程下,本地堆?;ゲ魂P(guān)聯(lián),所有?;厮荨⒃獢?shù)據(jù)查找都需走OS或本地鎖/鏈表,進(jìn)一步拉低性能擴(kuò)展性。
4. 跨平臺(tái)和歷史兼容包袱
因Windows/Unix兩套機(jī)制并存,大量platform abstraction和邊界容錯(cuò)邏輯,極大增加了維護(hù)成本和bug風(fēng)險(xiǎn)。每一次異??缃缍夹枰厥馓幚?,開發(fā)運(yùn)維和調(diào)優(yōu)都十分困難。
以下是.NET9以前多線程和單線程異常拋出耗時(shí),可以看到隨著堆棧深度的增加,拋出異常要花費(fèi)的世界越來(lái)越長(zhǎng)。


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


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