异常的零成本模型:正常路径免费,异常路径很贵
「异常很慢」是最大的误解之一
C++ 圈子里「异常很慢、性能代码要 -fno-exceptions」是流传最广的误解之一。它部分对(异常路径确实贵),部分错(正常路径几乎免费)。真实模型叫表驱动零成本(table-driven zero-cost):
- 零成本(正常路径):不抛异常的代码路径,几乎没有额外指令。异常机制的代价不体现在「每次调用多一条检查指令」(那是 error code 的代价),而是挪到了异常真的抛出时。
- 表驱动(异常路径):抛异常时,运行时查 EH 表(Exception Handling table,编译期生成,记录「这个地址的栈帧怎么展开、有没有 catch」)来沿调用栈往上找 catch,并展开栈。这个查表 + 栈展开是微秒级的。
我们直接测这个模型。
上手跑一跑:正常免费,异常贵 3400 倍
四种路径(本机,平均每次 ns):
===== 异常成本 =====
正常路径(从不抛): 0.250 ns/op
错误码(返回值传 err): 0.247 ns/op
有 throw 能力但从不抛(零成本): 0.249 ns/op
每次都 throw+catch: 857.3 ns/op ← 异常路径很贵这张表验证了零成本模型的两半:
1. 正常路径零成本坐实。前三行几乎一样快(0.247-0.250 ns):「纯函数」、「错误码(每次多传一个 err 参数)」、「有 throw 能力但从不抛」三者的正常路径耗时没有可测差别。这就是「零成本」的精确含义:开 try/异常机制,不给正常路径加任何成本。
2. 异常路径贵 ~3400 倍。最后一行 857 ns vs 0.25 ns。抛+捕获一次异常要:分配异常对象、查 EH 表、沿栈展开(逐个析构局部对象)、找到 catch、跳转。这一套是微秒级,比正常返回贵三个数量级。
这两条合起来,直接指导异常的使用纪律:
- 异常只用于真正的异常情况(罕见、意外、需要栈展开的路径)。正常控制流千万别用异常当「特殊的返回值」,那条路径贵 3400 倍。
- 正常路径不用担心异常开销。开了
try/异常机制不会让你的正常代码变慢(零成本坐实)。「try块慢」是误解,try本身不产生运行时指令,慢的是「真的 throw」。
和错误码的取舍
异常 vs 错误码不是「哪个快」,是「哪种错误分布适合哪个」:
| 模型 | 正常路径 | 错误路径 | 适合 |
|---|---|---|---|
| 错误码 | 略贵(每次检查 err) | 和正常一样 | 错误常见(每次检查不浪费) |
| 异常 | 免费(零成本) | 很贵(微秒级) | 错误罕见(正常路径不被污染) |
推论:错误越罕见,异常越划算。如果一个 API 的「错误」其实是常态(比如 parse 经常遇到不合法输入),错误码更好;如果错误是真异常(比如 vector::at 越界、内存分配失败、网络中断),异常更好,它让正常路径代码干净(没 if (err) 满天飞),真异常时那点开销也无所谓。
C++ 标准库就是这个取舍:vector::operator[] 不检查越界(快),vector::at 检查并抛异常(正常路径仍零成本,越界是真异常)。
-fno-exceptions:什么时候关
有些场景会 -fno-exceptions 整体关掉异常:
- 嵌入式 / 游戏 / 实时:确定性要求高,栈展开时间不可控(微秒级抖动);或二进制体积受限(EH 表占空间)。
- 极致体积:EH 表和 unwind 信息占不少体积,关掉能省(顺带省一些代码生成约束)。
- 代价:失去 RAII 错误传播。异常是 C++ 跨函数传播错误的「无遗漏」机制(析构链保证),关掉后要手写错误码透传,容易漏。容器(
vector等)的某些行为也会退化(比如at改为abort)。
所以 -fno-exceptions 是有取舍的工程决策,不是「为了快」的银弹。正常 C++ 代码(后端、桌面、大多数库)应该留着异常,因为它让正常路径干净、且正常路径零成本。
实现:Itanium C++ ABI EH
异常的底层实现遵循 Itanium C++ ABI 的 EH 规范(x86-64 Linux + macOS 通用):throw 用 __cxa_throw、catch 用 personality function 查 .gcc_except_table、栈展开用 _Unwind_RaiseException。这套机制是「表驱动」的,编译期为每段能抛异常的代码生成 EH 表,运行时只在抛出时才查。深度(两阶段异常处理、handler 搜索)超出 vol6 范围,知道「表驱动零成本」这个架构事实就够指导决策了。
一句话收口:异常是表驱动零成本模型,正常路径无额外指令(实测 0.25 ns,和纯函数一样),异常路径靠 EH 表查 + 栈展开(实测 857 ns,贵 3400 倍);「try 慢」是误解,慢的是「真的 throw」,try 块本身零成本;错误越罕见异常越划算,错误是常态则用错误码;-fno-exceptions 是嵌入式/体积/确定性的取舍,不是性能银弹,代价是失去 RAII 错误传播。异常机制深度(Itanium ABI EH、两阶段处理)超出 vol6,知道零成本模型即可。
参考资源
- Itanium C++ ABI Exception Handling(itanium-cxx-abi.github.io/cxx-abi-eh.html)——EH 表、personality function、栈展开的规范
- CppCoreGuidelines Errors and Exception Handling(Stroustrup & Sutter)——异常使用纪律
- Agner Fog《Optimizing software in C++》异常小节。本地
- 本篇实测代码:
code/volumn_codes/vol6-performance/ch06/exception_cost.cpp