写点什么

.NET 9 中的异常处理性能提升分析:为什么过去慢,未来快

  • 2025-06-05
    福建
  • 本文字数:4652 字

    阅读完需:约 15 分钟

一、为什么要关注.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: 2151ms

  • Java: 175ms

.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 系统的极限性能,无论架构设计还是细节实现都欠缺优化,反映在:

  • 内部优先保证兼容性和健壮性,而不是高性能

  • 代码中凡是热路径,都让开发者“自觉避开异常”


近年来,现代服务常常:

  • 依赖于“不可靠资源” (如网络、外部 API、云存储),短暂失效随时发生

  • 借助基于async/await的异步编程,异常常常跨栈、跨线程重抛

  • 在微服务系统中,单点故障可能导致“异常风暴”,大量请求因依赖故障极短时间内批量失败

这些场景下,异常处理已极易成为性能瓶颈,应用的可用性与 SLA 依赖于异常恢复速度。


2. CoreCLR/Mono 异常实现机制的先天劣势


Windows 实现

  • 采用 Windows 的 Structured Exception Handling (SEH),异常抛出后,OS 内核统一回溯堆栈、查找/触发 catch 和 finally,且需要“双遍遍历”栈帧(第一次查 catch、第二次触发 catch/finally,源数据由 Windows 维护)

    Structured Exception Handling(结构化异常处理,简称 SEH)是微软 Windows 操作系统上一种异常处理机制,主要用于捕获和处理程序运行过程中产生的异常,如访问违规(Access Violation)、除零错误、非法指令等。在 Windows 平台上,SEH 被底层编译器和系统广泛支持。

  • 用户层主要通过回调介入,绝大多数性能消耗“锁死”在 OS 堆栈查找、回调和上下文切换中,优化空间很小



Unix/Linux 实现

  • 没有 SEH,只能自己模拟

  • 采用 C++异常,异常抛出后靠 libgcc/libunwind 的_C++机制回溯托管栈,但需“桥接”托管/本地的边界,异常对象需反复throw/catch,初始化/过滤时会有多次 C++异常嵌套传递

    libunwind 是一个开源的栈回溯库,主要用于在运行时获取和操作调用栈,从而支持异常处理、调试和崩溃分析等功能。

  • 托管运行时(如 ExecutionManager) 需要频繁做函数表和异常元数据线性遍历(链表查找),并发场景下会有大量锁竞争,极易成为瓶颈


实际 CPU 性能热点采样发现:

  • libgcc_s.so.1/_Unwind_Find_FDE 等 C++异常系统函数占用近 13%的热点

  • 托管代码层大量链表遍历/锁(ExecutionManager::FindCodeRangeWithLock 等)

  • 多线程/多异常场景下 lock 恶性竞争,栈查找速度极慢



额外开销

  • 每次抛出异常需清空/复制完整 CONTEXT 结构(Windows 上下文),单次就近 1KB 数据

  • 捕获栈信息、生成调试辅助、捕获完整 stacktrace 等都增加明显延迟


3. Async/多线程场景放大性能损耗


现代 C#的 async/await 广泛出现。每遇到 await 断点,异常需在 async 状态机多次 catch/throw 重入口,即使只有 1 层异常,实际走了多倍 catch 分支。多线程下,本地堆栈互不关联,所有栈回溯、元数据查找都需走 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 上。主要技术变更包括:

  1. 将异常展开、catch/finally 分派等环节全部搬到托管主流程

  2. native helper 只做最小的 stack frame 展开,与垃圾回收栈遍历接口复用(易于维护)

  3. 强化托管级缓存与元数据管理。关键链表遍历全部升级成缓存/高速哈希表,一举解决了多线程、深栈、频繁异常场景下的 scalability 困境

  4. 钉死所有多余的 C++ throw/catch——对 Unix/Windows 都生效

  5. 为 Async/Await 生成优化代码路径,避免多次重复抛出/捕获


工程落地与效果


  • 性能测试实测,异常处理耗时降幅约 76%~80%,多线程/高并发效果更好

  • 性能剖析热点:主要耗时已缩小到 stack walker 和关键数据结构哈希效率上,其他已近极致

  • 全平台统一,无历史特殊兼容路径、包袱


真实图片示例


  • 单线程性能提升图:



  • 多线程性能提升图:




(三)可进一步优化的场景与细节


  1. 热点分支 profile(PGO)

    异常的“常用路径”可被 profile,按 pgo 机制热路径内联/重编排逻辑

    比如 async await 状态机里常抛异常的分支 inline 获得最佳 cache 局部性

  2. Unwind Section 缓存/优化

    比如在 libunwind 的 findUnwindSections 过程中用 cache 提升多线程吞吐,已实测可提速近 7 倍

    类似样板代码见:https://gist.github.com/filipnavara/9dca9d78bf2a768a9512afe9233d4cbe

  3. 双检省栈 trace 与细粒度采集

    支持仅按需采集 stacktrace(避免捕获所有调试信息)

  4. 特殊场景快速捕获(业务异常/操作性异常)

    通过拓展托管 catch 块类型,可以极简分为业务异常与系统异常,实现“无栈捕获”,加速高频捕获型异常(如 EndOfData、ParseError 等流控制型异常)

  5. 异步异常统一延迟捕获传递

    在没有用户自定义 try 块的 async 方法中,捕获异常仅保存,真正抛出延迟到非异常主流程结束前即可。这将极大降低状态机驱动的抛出/捕获次数。


六、总结展望


.NET 9 通过彻底拥抱 NativeAOT 极简式的托管异常处理体系,把历史包袱(OS-Specific/C++ Exception Bridge/冗余链表 &锁/多次 catch-rethrow)一举清除,大幅释放了异常路径的性能潜力。这一变革支撑了.NET 在微服务、云原生、异步并发等新主流场景下的顶级运行时表现。未来,随着堆栈展开、元数据 cache 自适应等不断迭代,.NET 有望成为托管平台的异常处理性能“天花板”。


文章转载自:InCerry

原文链接:https://www.cnblogs.com/InCerry/p/-/dotnet-9-exception-pref-improve

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
.NET 9中的异常处理性能提升分析:为什么过去慢,未来快_.NET 7_电子尖叫食人鱼_InfoQ写作社区