随着现代云原生、高并发、分布式场景的大量普及,异常处理(Exception Handling)早已不再只是一个冷僻的代码路径。在高复杂度的微服务、网络服务、异步编程环境下,服务依赖的外部资源往往不可靠,偶发失效或小概率的“雪崩”场景已经十分常见。实际系统常常在高频率地抛出、传递、捕获异常,异常处理性能直接影响着系统的恢复速度、吞吐量,甚至是稳定性与容错边界。
.NET平台在异常处理性能方面长期落后于C++、Java等同类主流平台——业内社区多次对比公开跑分就证实了这一点,.NET 8时代虽然差距有所缩小,但在某些高并发/异步等极端场景下,异常高开销持续困扰社区和大厂工程师。于是到了.NET 9,终于迎来了一次代际变革式的性能飞跃,抛出/捕获异常的耗时基本追平C++,成为技术圈最关注的.NET runtime底层事件之一。
最经典的异常性能测试如下——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
}
}
}
}
.NET 的异常抛出/捕获速度相较慢得多。但到了.NET 8后期和.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次异常:
image
.NET 9的性能提升几乎让EH成本降到C++/Java同量级,成为托管平台的性能标杆之一。
传统观点认为:“异常只为异常流程准备,主业务应以if/else或TryXXX等方式避免极端异常分支”。社区和官方因此忽视了EH系统的极限性能,无论架构设计还是细节实现都欠缺优化,反映在:
近年来,现代服务常常:
async/await
**的异步编程,异常常常跨栈、跨线程重抛这些场景下,异常处理已极易成为性能瓶颈,应用的可用性与SLA依赖于异常恢复速度。
Windows实现
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实现
throw/catch
,初始化/过滤时会有多次C++异常嵌套传递libunwind 是一个开源的栈回溯库,主要用于在运行时获取和操作调用栈,从而支持异常处理、调试和崩溃分析等功能。实际CPU性能热点采样发现:
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 |
额外开销
现代C#的async/await广泛出现。每遇到await断点,异常需在async状态机多次catch/throw重入口,即使只有1层异常,实际走了多倍catch分支。多线程下,本地堆栈互不关联,所有栈回溯、元数据查找都需走OS或本地锁/链表,进一步拉低性能扩展性。
因Windows/Unix两套机制并存,大量platform abstraction和边界容错逻辑,极大增加了维护成本和bug风险。每一次异常跨界都需要特殊处理,开发运维和调优都十分困难。
以下是.NET9以前多线程和单线程异常抛出耗时,可以看到随着堆栈深度的增加,抛出异常要花费的世界越来越长。
.NET 9之所以实现了异常处理的性能“质变”,核心思路是吸收NativeAOT的极简托管实现,将主力流程自托管直接管理,核心只依赖native stack walker完成功能边界,避免一切反复嵌套或冗余环节。
在.NET 9,团队把NativeAOT的异常处理模式移植到了CoreCLR上。主要技术变更包括:
• 多线程性能提升图:
.NET 9通过彻底拥抱NativeAOT极简式的托管异常处理体系,把历史包袱(OS-Specific/C++ Exception Bridge/冗余链表&锁/多次catch-rethrow)一举清除,大幅释放了异常路径的性能潜力。这一变革支撑了.NET在微服务、云原生、异步并发等新主流场景下的顶级运行时表现。未来,随着堆栈展开、元数据cache自适应等不断迭代,.NET有望成为托管平台的异常处理性能“天花板”。