
在高并发下,C# 异常处理的性能开销不是“有点大”,而是**单次异常抛出 ≈ 几百纳秒到数毫秒级延迟,且随调用栈深度指数上升**——这在每秒处理上万请求的系统里,足以让吞吐量断崖式下跌。
为什么一次 throw 就这么贵?
CLR 不是简单跳转,而是要:
- 遍历整个调用栈,查找匹配的
catch块(哪怕最终没找到) - 为每个栈帧生成完整堆栈跟踪(
StackTrace字符串、方法元数据、IL 偏移等) - 触发 JIT 临时编译异常处理相关代码路径(冷路径)
- 若未被立即捕获,还会多次重入异常分发逻辑(尤其跨 async 边界时)
实测:在 16 层调用栈 + Exception 类型下,throw new Exception() 平均耗时约 1.2–2.8 ms(.NET 6/7,Release 模式,中等负载 CPU)。这不是 CPU 计算开销,而是**不可缓存、不可预测、强阻塞的系统级开销**。
try/catch 本身几乎免费,但「进 catch 块」代价极高
很多人误以为 try 块有开销,其实不然:try 只是元数据标记,零运行时成本。真正昂贵的是——
-
catch (Exception ex)块被执行(意味着异常已展开完毕) - 更糟的是:
catch块里再throw或throw ex—— 这会重建堆栈,开销翻倍 - 在循环里写
try/catch(比如逐条解析 JSON),等于把毫秒级延迟放大 N 倍
int.TryParse
-
when表达式在异常分发早期执行,不触发堆栈构建 - 表达式里禁止副作用(如修改状态、打日志),否则行为不可控
- 不能用于
catch (Exception)后无类型限定的场景(编译报错)
高频异常 = 隐形雪崩,监控比优化更优先
生产环境最危险的不是慢,而是“偶发慢”——比如数据库连接池耗尽时,每 1000 次请求抛一次 SqlException,QPS 看似正常,GC 耗时却悄悄涨了 300%。
必须做两件事:
- 用
dotnet-counters监控System.Runtime/Exceptions Per Second,阈值建议设为 > 10/s 即告警 - 在全局异常处理器(如 ASP.NET Core 的
UseExceptionHandler)里记录Exception.ToString()前,先检查exception.GetType().Name是否属于业务预期异常(如ValidationException),避免日志刷爆磁盘
真正难的不是写出高性能代码,而是意识到:**“这段逻辑本不该走异常路径”——那才是性能问题的根因。**











