.NET9中异常处理性能提升分析

ASP.NET教程 2025-08-24

目录

  • 一、为什么要关注.NET异常处理的性能
  • 二、实测:.NET 9异常处理提速直观对比
    • 1. 测试代码
    • 2. 早期测试结果
    • 3. 新时代基准结果(.NET 8 vs .NET 9)
  • 三、.NET早期异常处理为何如此之慢?
    • 1. 策略层面的历史误区
    • 2. CoreCLR/Mono 异常实现机制的先天劣势
    • 3. Async/多线程场景放大性能损耗
    • 4. 跨平台和历史兼容包袱
  • 四、技术极客视角:.NET 9彻底变革的细节原理
    • (一)NativeAOT异常处理架构剖析
    • (二).NET 9实现与补全 - 同步NativeAOT设计到CoreCLR
    • (三)可进一步优化的场景与细节
  • 六、总结展望

    一、为什么要关注.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:每次约 12s
    • .NET 9:每次减少至约 2.8s (约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堆栈查找、回调和上下文切换中,优化空间很小

    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实现

    没有SEH,只能自己模拟

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

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

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

    实际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分支。多线程下,本地堆栈互不关联,所有栈回溯、元数据查找都需走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和关键数据结构哈希效率上,其他已近极致
    • 全平台统一,无历史特殊兼容路径、包袱

    真实图片示例

    1.单线程性能提升图:

    2.多线程性能提升图:

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

    热点分支profile(PGO)

    • 异常的常用路径可被profile,按pgo机制热路径内联/重编排逻辑
    • 比如async await状态机里常抛异常的分支inline获得最佳cache局部性

    Unwind Section缓存/优化

    • 比如在libunwind的findUnwindSections过程中用cache提升多线程吞吐,已实测可提速近7倍
    • 类似样板代码见:https://gist.git*h**ub.com/filipnavara/9dca9d78bf2a768a9512afe9233d4cbe

    双检省栈trace与细粒度采集

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

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

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

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

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

    六、总结展望

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

    到此这篇关于.NET9中异常处理性能提升分析的文章就介绍到这了,更多相关.NET9异常处理内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

    您可能感兴趣的文章:
    • .NET中的异常和异常处理用法分析
    • 详谈.NET的异常处理
    • 关于.NET异常处理的思考总结
    • 你应该知道的.NET错误与异常处理机制
    • 在 ASP.NET Core 中为 gRPC 服务添加全局异常处理
    • ASP.NETCore全局异常处理