.NET9中异常处理性能提升分析
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、为什么要关注.NET异常处理的性能</a></li><li><a href="#_label1">二、实测:.NET 9异常处理提速直观对比</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">1. 测试代码</a></li><li><a href="#_lab2_1_1">2. 早期测试结果</a></li><li><a href="#_lab2_1_2">3. 新时代基准结果(.NET 8 vs .NET 9)</a></li></ul><li><a href="#_label2">三、.NET早期异常处理为何如此之慢?</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_3">1. 策略层面的历史误区</a></li><li><a href="#_lab2_2_4">2. CoreCLR/Mono 异常实现机制的先天劣势</a></li><li><a href="#_lab2_2_5">3. Async/多线程场景放大性能损耗</a></li><li><a href="#_lab2_2_6">4. 跨平台和历史兼容包袱</a></li></ul><li><a href="#_label3">四、技术极客视角:.NET 9彻底变革的细节原理</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_7">(一)NativeAOT异常处理架构剖析</a></li><li><a href="#_lab2_3_8">(二).NET 9实现与补全 —— 同步NativeAOT设计到CoreCLR</a></li><li><a href="#_lab2_3_9">(三)可进一步优化的场景与细节</a></li></ul><li><a href="#_label4">六、总结展望</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、为什么要关注.NET异常处理的性能</h2><p>随着现代云原生、高并发、分布式场景的大量普及,异常处理(Exception Handling)早已不再只是一个冷僻的代码路径。在高复杂度的微服务、网络服务、异步编程环境下,服务依赖的外部资源往往不可靠,偶发失效或小概率的“雪崩”场景已经十分常见。实际系统常常在高频率地抛出、传递、捕获异常,异常处理性能直接影响着系统的恢复速度、吞吐量,甚至是稳定性与容错边界。</p>
<p>.NET平台在异常处理性能方面长期落后于C++、Java等同类主流平台——业内社区多次对比公开跑分就证实了这一点,.NET 8时代虽然差距有所缩小,但在某些高并发/异步等极端场景下,异常高开销持续困扰社区和大厂工程师。于是到了.NET 9,终于迎来了一次代际变革式的性能飞跃,抛出/捕获异常的耗时基本追平C++,成为技术圈最关注的.NET runtime底层事件之一。</p>
<p class="maodian"><a name="_label1"></a></p><h2>二、实测:.NET 9异常处理提速直观对比</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>1. 测试代码</h3>
<p>最经典的异常性能测试如下——C# 和 Java的实现基本一致</p>
<p>C#:</p>
<div class="jb51code"><pre class="brush:csharp;">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
}
}
}
}
</pre></div>
<p><strong>Java:</strong></p>
<div class="jb51code"><pre class="brush: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
}
}
}
}
</pre></div>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>2. 早期测试结果</h3>
<p><strong>以.NET Core 2.2时代为例</strong></p>
<ul><li>.NET: 2151ms</li><li>Java: 175ms</li></ul>
<p>.NET 的异常抛出/捕获速度相较慢得多。但到了.NET 8后期和.NET 9,基准成绩已翻天覆地:</p>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>3. 新时代基准结果(.NET 8 vs .NET 9)</h3>
<p>借助 BenchmarkDotNet 可以更科学对比:</p>
<div class="jb51code"><pre class="brush:csharp;">using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Environments;
namespace ExceptionBenchmark
{
public class ExceptionBenchmark
{
private const int NumberOfIterations = 1000;
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);
}
}
}
}
</pre></div>
<p>如下图结果,抛出+捕获1000次异常:</p>
<ul><li>.NET 8:每次约 12μs</li><li>.NET 9:每次减少至约 2.8μs (约76~80%提升)</li></ul>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025060510160320.png" /></p>
<p>.NET 9的性能提升几乎让EH成本降到C++/Java同量级,成为托管平台的性能标杆之一。</p>
<p class="maodian"><a name="_label2"></a></p><h2>三、.NET早期异常处理为何如此之慢?</h2>
<p class="maodian"><a name="_lab2_2_3"></a></p><h3>1. 策略层面的历史误区</h3>
<p>传统观点认为:“异常只为异常流程准备,主业务应以if/else或TryXXX等方式避免极端异常分支”。社区和官方因此忽视了EH系统的极限性能,无论架构设计还是细节实现都欠缺优化,反映在:</p>
<ul><li>内部优先保证兼容性和健壮性,而不是高性能</li><li>代码中凡是热路径,都让开发者“自觉避开异常”</li></ul>
<p>近年来,现代服务常常:</p>
<ul><li><strong>依赖于“不可靠资源”</strong> (如网络、外部API、云存储),短暂失效随时发生</li><li><strong>借助基于<strong><strong>async/await</strong></strong>的异步编程,异常常常跨栈、跨线程重抛</strong></li><li><strong>在微服务系统中,单点故障可能导致“异常风暴”,大量请求因依赖故障极短时间内批量失败</strong></li></ul>
<p>这些场景下,异常处理已极易成为性能瓶颈,应用的可用性与SLA依赖于异常恢复速度。</p>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>2. CoreCLR/Mono 异常实现机制的先天劣势</h3>
<p><strong>Windows实现</strong></p>
<p>采用Windows的Structured Exception Handling (SEH),异常抛出后,OS内核统一回溯堆栈、查找/触发catch和finally,且需要“双遍遍历”栈帧(第一次查catch、第二次触发catch/finally,源数据由Windows维护)</p>
<p>Structured Exception Handling(结构化异常处理,简称 SEH)是微软 Windows 操作系统上一种异常处理机制,主要用于捕获和处理程序运行过程中产生的异常,如访问违规(Access Violation)、除零错误、非法指令等。在 Windows 平台上,SEH 被底层编译器和系统广泛支持。</p>
<p>用户层主要通过回调介入,绝大多数性能消耗“锁死”在OS堆栈查找、回调和上下文切换中,优化空间很小</p>
<table><tbody><tr><th>Name</th><th>Exc %</th><th>Exc</th><th>Inc %</th><th>Inc</th></tr><tr><td>ntdll!RtlpxLookupFunctionTable</td><td>11.4</td><td>4,525</td><td>11.4</td><td>4,525</td></tr><tr><td>ntdll!RtlpUnwindPrologue</td><td>11.2</td><td>4,441</td><td>11.2</td><td>4,441</td></tr><tr><td>ntdll!RtlLookupFunctionEntry</td><td>7.2</td><td>2,857</td><td>28.4</td><td>11,271</td></tr><tr><td>ntdll!RtlpxVirtualUnwind</td><td>6.5</td><td>2,579</td><td>17.7</td><td>7,020</td></tr><tr><td>ntdll!RtlpLookupDynamicFunctionEntry</td><td>3.6</td><td>1,425</td><td>9.8</td><td>3,889</td></tr><tr><td>coreclr!EEJitManager::JitCodeToMethodInfo</td><td>2.9</td><td>1,167</td><td>2.9</td><td>1,167</td></tr><tr><td>ntdll!RtlVirtualUnwind</td><td>2.9</td><td>1,137</td><td>17.9</td><td>7,099</td></tr><tr><td>ntoskrnl!EtwpWriteUserEvent</td><td>2.5</td><td>990</td><td>4.3</td><td>1,708</td></tr><tr><td>coreclr!ExceptionTracker::ProcessManagedCallFrame</td><td>2.4</td><td>941</td><td>18.7</td><td>7,405</td></tr><tr><td>coreclr!ProcessCLRException</td><td>2.4</td><td>938</td><td>93.3</td><td>36,969</td></tr><tr><td>ntdll!LdrpDispatchUserCallTarget</td><td>2.2</td><td>871</td><td>2.2</td><td>871</td></tr><tr><td>coreclr!ExecutionManager::FindCodeRangeWithLock</td><td>2.2</td><td>868</td><td>2.2</td><td>868</td></tr><tr><td>coreclr!memset</td><td>2.0</td><td>793</td><td>2.0</td><td>793</td></tr><tr><td>coreclr!ExceptionTracker::ProcessOSExceptionNotification</td><td>1.9</td><td>742</td><td>31.9</td><td>12,622</td></tr><tr><td>coreclr!SString::Replace</td><td>1.8</td><td>720</td><td>1.8</td><td>720</td></tr><tr><td>ntoskrnl!EtwpReserveTraceBuffer</td><td>1.8</td><td>718</td><td>1.8</td><td>718</td></tr><tr><td>coreclr!FillRegDisplay</td><td>1.8</td><td>709</td><td>1.8</td><td>709</td></tr><tr><td>ntdll!NtTraceEvent</td><td>1.7</td><td>673</td><td>7.1</td><td>2,803</td></tr></tbody></table>
<p><strong>Unix/Linux实现</strong></p>
<p>没有SEH,只能自己模拟</p>
<p>采用C++异常,异常抛出后靠libgcc/libunwind的_C++机制回溯托管栈,但需“桥接”托管/本地的边界,异常对象需反复<code>throw/catch</code>,初始化/过滤时会有多次C++异常嵌套传递</p>
<p>libunwind 是一个开源的栈回溯库,主要用于在运行时获取和操作调用栈,从而支持异常处理、调试和崩溃分析等功能。</p>
<p>托管运行时(如ExecutionManager) 需要频繁做函数表和异常元数据线性遍历(链表查找),并发场景下会有大量锁竞争,极易成为瓶颈</p>
<p>实际CPU性能热点采样发现:</p>
<ul><li>libgcc_s.so.1/_Unwind_Find_FDE等C++异常系统函数占用近13%的热点</li><li>托管代码层大量链表遍历/锁(ExecutionManager::FindCodeRangeWithLock等)</li><li>多线程/多异常场景下lock恶性竞争,栈查找速度极慢</li></ul>
<table><tbody><tr><th>Overhead</th><th>Shared Object</th><th>Symbol</th></tr><tr><td>+ 8,29%</td><td>libgcc_s.so.1</td><td>[.] _Unwind_Find_FDE</td></tr><tr><td>+ 2,51%</td><td>libc.so.6</td><td>[.] __memmove_sse2_unaligned_erms</td></tr><tr><td>+ 2,14%</td><td>ld-linux-x86-64.so.2</td><td>[.] _dl_find_object</td></tr><tr><td>+ 1,94%</td><td>libstdc++.so.6.0.30</td><td>[.] __gxx_personality_v0</td></tr><tr><td>+ 1,85%</td><td>libgcc_s.so.1</td><td>[.] 0x00000000000157eb</td></tr><tr><td>+ 1,77%</td><td>libc.so.6</td><td>[.] __memset_sse2_unaligned_erms</td></tr><tr><td>+ 1,36%</td><td>ld-linux-x86-64.so.2</td><td>[.] __tls_get_addr</td></tr><tr><td>+ 1,28%</td><td>libcoreclr.so</td><td>[.] ExceptionTracker::ProcessManagedCallFrame</td></tr><tr><td>+ 1,26%</td><td>libcoreclr.so</td><td>[.] apply_reg_state</td></tr><tr><td>+ 1,12%</td><td>libcoreclr.so</td><td>[.] OOPStackUnwinderAMD64::UnwindPrologue</td></tr><tr><td>+ 1,08%</td><td>libgcc_s.so.1</td><td>[.] 0x0000000000016990</td></tr><tr><td>+ 1,08%</td><td>libcoreclr.so</td><td>[.] ExceptionTracker::ProcessOSExceptionNotification</td></tr></tbody></table>
<p><strong>额外开销</strong></p>
<ul><li>每次抛出异常需清空/复制完整CONTEXT结构(Windows上下文),单次就近1KB数据</li><li>捕获栈信息、生成调试辅助、捕获完整stacktrace等都增加明显延迟</li></ul>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3. Async/多线程场景放大性能损耗</h3>
<p>现代C#的async/await广泛出现。每遇到await断点,异常需在async状态机多次catch/throw重入口,即使只有1层异常,实际走了多倍catch分支。多线程下,本地堆栈互不关联,所有栈回溯、元数据查找都需走OS或本地锁/链表,进一步拉低性能扩展性。</p>
<p class="maodian"><a name="_lab2_2_6"></a></p><h3>4. 跨平台和历史兼容包袱</h3>
<p>因Windows/Unix两套机制并存,大量platform abstraction和边界容错逻辑,极大增加了维护成本和bug风险。每一次异常跨界都需要特殊处理,开发运维和调优都十分困难。</p>
<p>以下是.NET9以前多线程和单线程异常抛出耗时,可以看到随着堆栈深度的增加,抛出异常要花费的世界越来越长。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025060510160321.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025060510160322.png" /></p>
<p class="maodian"><a name="_label3"></a></p><h2>四、技术极客视角:.NET 9彻底变革的细节原理</h2>
<p>.NET 9之所以实现了异常处理的性能“质变”,核心思路是<strong>吸收NativeAOT的极简托管实现,将主力流程自托管直接管理,核心只依赖native stack walker完成功能边界,避免一切反复嵌套或冗余环节</strong>。</p>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>(一)NativeAOT异常处理架构剖析</h3>
<p>1. 设计变革</p>
<ul><li><strong>完全托管驱动主流程</strong>异常的捕获、catch分派、finally查找、异常对象/类型的元数据查找等主环节,全部写成托管代码(C#逻辑)。</li><li><strong>native code仅负责栈帧展开(stack walking)</strong>需要时才调用本地API(libunwind/Windows API)由native/cross平台实现stack frame的move next/遍历,极简无其他依赖。</li><li><strong>无C++异常桥接</strong>,这样省去了_os-unwind、double catch-rethrow等所有历史冗余。</li><li><strong>功能单纯、易于调优和定制</strong>,不到300行关键路径代码。</li></ul>
<p>2. 优势分析</p>
<ul><li>代码极简,热路径关键点完全可控</li><li>不存在异步场景下的“状态机分支回溯”性能急剧下滑</li><li>托管逻辑易于内联、缓存</li><li>Native代码只做最小功能、极易换实现/裁剪</li><li>性能调优点固定且标志性突出(大部分耗时都在stack walker/元数据cache里)</li><li>兼容可扩展,后续想做特殊异常/自定义类型极为简便</li></ul>
<p>3. 技术细节</p>
<ul><li>异常对象的stacktrace/元数据在托管代码按需附加</li><li>若已知异常只在本地代码路径,完全可绕开“不需要的”full stacktrace/callstack/diagnostic等场景</li><li>可以整合cache优化,如将每个托管JIT帧的元数据查找结果放本地线程缓存(甚至开启pgo热点分支识别,见后续)。</li></ul>
<p class="maodian"><a name="_lab2_3_8"></a></p><h3>(二).NET 9实现与补全 —— 同步NativeAOT设计到CoreCLR</h3>
<p>在.NET 9,团队把NativeAOT的异常处理模式移植到了CoreCLR上。主要技术变更包括:</p>
<ul><li><strong>将异常展开、catch/finally分派等环节全部搬到托管主流程</strong></li><li><strong>native helper只做最小的stack frame展开,与垃圾回收栈遍历接口复用(易于维护)</strong></li><li><strong>强化托管级缓存与元数据管理</strong>。关键链表遍历全部升级成缓存/高速哈希表,一举解决了多线程、深栈、频繁异常场景下的scalability困境</li><li><strong>钉死所有多余的C++ throw/catch</strong>——对Unix/Windows都生效</li><li><strong>为Async/Await生成优化代码路径</strong>,避免多次重复抛出/捕获</li></ul>
<p><strong>工程落地与效果</strong></p>
<ul><li>性能测试实测,异常处理耗时降幅约76%~80%,多线程/高并发效果更好</li><li>性能剖析热点:主要耗时已缩小到stack walker和关键数据结构哈希效率上,其他已近极致</li><li>全平台统一,无历史特殊兼容路径、包袱</li></ul>
<p><strong>真实图片示例</strong></p>
<p><strong>1.单线程性能提升图:</strong></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025060510160323.png" /></p>
<p><strong>2.多线程性能提升图:</strong></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202506/2025060510160324.png" /></p>
<p class="maodian"><a name="_lab2_3_9"></a></p><h3>(三)可进一步优化的场景与细节</h3>
<p><strong>热点分支profile(PGO)</strong></p>
<ul><li>异常的“常用路径”可被profile,按pgo机制热路径内联/重编排逻辑</li><li>比如async await状态机里常抛异常的分支inline获得最佳cache局部性</li></ul>
<p><strong>Unwind Section缓存/优化</strong></p>
<ul><li>比如在libunwind的findUnwindSections过程中用cache提升多线程吞吐,已实测可提速近7倍</li><li>类似样板代码见:<a href="https://gist.github.com/filipnavara/9dca9d78bf2a768a9512afe9233d4cbe" rel="external nofollow"target="_blank">https://gist.github.com/filipnavara/9dca9d78bf2a768a9512afe9233d4cbe</a></li></ul>
<p><strong>双检省栈trace与细粒度采集</strong></p>
<p>支持仅按需采集stacktrace(避免捕获所有调试信息)</p>
<p><strong>特殊场景快速捕获(业务异常/操作性异常)</strong></p>
<p>通过拓展托管catch块类型,可以极简分为业务异常与系统异常,实现“无栈捕获”,加速高频捕获型异常(如EndOfData、ParseError等流控制型异常)</p>
<p><strong>异步异常统一延迟捕获传递</strong></p>
<p>在没有用户自定义try块的async方法中,捕获异常仅保存,真正抛出延迟到非异常主流程结束前即可。这将极大降低状态机驱动的抛出/捕获次数。</p>
<p class="maodian"><a name="_label4"></a></p><h2>六、总结展望</h2>
<p>.NET 9通过彻底拥抱NativeAOT极简式的托管异常处理体系,把历史包袱(OS-Specific/C++ Exception Bridge/冗余链表&锁/多次catch-rethrow)一举清除,大幅释放了异常路径的性能潜力。这一变革支撑了.NET在微服务、云原生、异步并发等新主流场景下的顶级运行时表现。未来,随着堆栈展开、元数据cache自适应等不断迭代,.NET有望成为托管平台的异常处理性能“天花板”。</p>
頁:
[1]