.NET9中異常處理性能提升分析
一、為什么要關(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)十分常見。實(shí)際系統(tǒng)常常在高頻率地拋出、傳遞、捕獲異常,異常處理性能直接影響著系統(tǒng)的恢復(fù)速度、吞吐量,甚至是穩(wěn)定性與容錯(cuò)邊界。
.NET平臺(tái)在異常處理性能方面長期落后于C++、Java等同類主流平臺(tái)——業(yè)內(nèi)社區(qū)多次對(duì)比公開跑分就證實(shí)了這一點(diǎn),.NET 8時(shí)代雖然差距有所縮小,但在某些高并發(fā)/異步等極端場(chǎng)景下,異常高開銷持續(xù)困擾社區(qū)和大廠工程師。于是到了.NET 9,終于迎來了一次代際變革式的性能飛躍,拋出/捕獲異常的耗時(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: 2151ms
- Java: 175ms
.NET 的異常拋出/捕獲速度相較慢得多。但到了.NET 8后期和.NET 9,基準(zhǔn)成績已翻天覆地:
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)的極限性能,無論架構(gòu)設(shè)計(jì)還是細(xì)節(jié)實(shí)現(xiàn)都欠缺優(yōu)化,反映在:
- 內(nèi)部優(yōu)先保證兼容性和健壯性,而不是高性能
- 代碼中凡是熱路徑,都讓開發(fā)者“自覺避開異常”
近年來,現(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)行過程中產(chǎn)生的異常,如訪問違規(guī)(Access Violation)、除零錯(cuò)誤、非法指令等。在 Windows 平臺(tái)上,SEH 被底層編譯器和系統(tǒng)廣泛支持。
用戶層主要通過回調(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)
沒有SEH,只能自己模擬
采用C++異常,異常拋出后靠libgcc/libunwind的_C++機(jī)制回溯托管棧,但需“橋接”托管/本地的邊界,異常對(duì)象需反復(fù)throw/catch
,初始化/過濾時(shí)會(huì)有多次C++異常嵌套傳遞
libunwind 是一個(gè)開源的?;厮輲?,主要用于在運(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)的世界越來越長。
四、技術(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)無其他依賴。
- 無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í)別,見后續(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)一,無歷史特殊兼容路徑、包袱
真實(shí)圖片示例
1.單線程性能提升圖:
2.多線程性能提升圖:
(三)可進(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)化
- 比如在libunwind的findUnwindSections過程中用cache提升多線程吞吐,已實(shí)測(cè)可提速近7倍
- 類似樣板代碼見:https://gist.github.com/filipnavara/9dca9d78bf2a768a9512afe9233d4cbe
雙檢省棧trace與細(xì)粒度采集
支持僅按需采集stacktrace(避免捕獲所有調(diào)試信息)
特殊場(chǎng)景快速捕獲(業(yè)務(wù)異常/操作性異常)
通過拓展托管catch塊類型,可以極簡(jiǎn)分為業(yè)務(wù)異常與系統(tǒng)異常,實(shí)現(xiàn)“無棧捕獲”,加速高頻捕獲型異常(如EndOfData、ParseError等流控制型異常)
異步異常統(tǒng)一延遲捕獲傳遞
在沒有用戶自定義try塊的async方法中,捕獲異常僅保存,真正拋出延遲到非異常主流程結(jié)束前即可。這將極大降低狀態(tài)機(jī)驅(qū)動(dòng)的拋出/捕獲次數(shù)。
六、總結(jié)展望
.NET 9通過徹底擁抱NativeAOT極簡(jiǎn)式的托管異常處理體系,把歷史包袱(OS-Specific/C++ Exception Bridge/冗余鏈表&鎖/多次catch-rethrow)一舉清除,大幅釋放了異常路徑的性能潛力。這一變革支撐了.NET在微服務(wù)、云原生、異步并發(fā)等新主流場(chǎng)景下的頂級(jí)運(yùn)行時(shí)表現(xiàn)。未來,隨著堆棧展開、元數(shù)據(jù)cache自適應(yīng)等不斷迭代,.NET有望成為托管平臺(tái)的異常處理性能“天花板”。
到此這篇關(guān)于.NET9中異常處理性能提升分析的文章就介紹到這了,更多相關(guān).NET9異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用.NET命令行編譯器編譯項(xiàng)目(如ASP.NET、C#等)
很多情況你從網(wǎng)上下載了源程序,卻苦于本機(jī)沒裝開發(fā)環(huán)境而不能編譯查看,下面我簡(jiǎn)單說一下解決辦法。2009-03-03asp.net根據(jù)計(jì)算機(jī)MAC地址限定每臺(tái)機(jī)子只能領(lǐng)取一次賬號(hào)
這里只做簡(jiǎn)單演示過程,請(qǐng)根據(jù)您的實(shí)際情況作適當(dāng)修改!另外我的博客只做自己參考查詢方便用,請(qǐng)各位大神不要沒事噴我,知道您的技術(shù)高,我是新手正在努力學(xué)習(xí)當(dāng)中,謝謝2012-06-06.Net Core內(nèi)存回收模式及性能測(cè)試對(duì)比分析
下面小編就為大家分享一篇.Net Core內(nèi)存回收模式及性能測(cè)試對(duì)比分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12ASP.NET Core中自定義路由約束的實(shí)現(xiàn)
這篇文章主要介紹了ASP.NET Core中自定義路由約束的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03ASP.NET?Core基于現(xiàn)有數(shù)據(jù)庫創(chuàng)建EF模型
這篇文章介紹了ASP.NET?Core基于現(xiàn)有數(shù)據(jù)庫創(chuàng)建EF模型的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04詳解免費(fèi)高效實(shí)用的.NET操作Excel組件NPOI(.NET組件介紹之六)
這篇文章主要介紹了詳解免費(fèi)高效實(shí)用的.NET操作Excel組件NPOI(.NET組件介紹之六),具有一定的參考價(jià)值,有興趣的可以了解一下。2016-12-12