19. 异常与性能:异常处理的开销、RTTI 的开销、如何禁用异常
异常处理这个话题,在 C++ 社区里争议一直很大。有人觉得它是现代 C++ 的必备特性,有人则避之不及。我个人属于中间派——异常很好用,但你要清楚它的代价。
今天我们就来聊聊异常到底慢在哪,RTTI 又是什么鬼,以及怎么在项目中禁用它们。
异常处理的开销
很多人以为异常就是「抛出去,接住」这么简单。其实不然。异常的开销分两块:正常路径和异常路径。
正常路径的开销
如果代码里没有抛出异常,是不是就没开销?不是。
编译器为了支持异常,会在每个可能抛出异常的函数入口和出口插入额外的代码。这些代码负责维护一个「异常处理栈」——说白了就是记录当前函数调用到了哪一层,如果抛异常了该往哪跳。
我曾在项目中做过一个测试:一个纯数值计算的循环,开启异常支持后,性能下降了 5%~8%。你想想看,循环里根本没抛异常,但编译器就是得给你留后路。
关键点:异常的正常路径开销,主要来自栈展开信息的维护。这部分开销通常不大(5%~15%),但在热循环中会被放大。
异常路径的开销
真正慢的是抛出异常的那一刻。我给大家拆解一下:
- 构造异常对象——需要动态内存分配(new 一个 exception 对象)
- 栈展开——遍历调用栈,找到匹配的 catch 块
- 析构局部对象——沿途调用所有局部对象的析构函数
- 异常对象清理——最后释放异常对象的内存
这一套下来,少说几百个 CPU 周期。如果异常发生在性能敏感路径上,那基本就是灾难。
警告:千万不要在热循环里用异常做控制流。比如用异常来跳出循环、或者用异常表示「没找到数据」——这种用法我见过太多次了,每次都是性能杀手。
RTTI 的开销
RTTI(运行时类型识别)是另一个容易被忽视的性能点。它主要靠 typeid 和 dynamic_cast 两个操作。
typeid 的开销
typeid 本身开销不大。它只是从虚函数表里取一个指针,指向一个 std::type_info 对象。这个操作基本是 O(1) 的。
但要注意:typeid 返回的字符串比较(比如 typeid(*ptr).name())是字符串比较,不是指针比较。如果你在循环里频繁做这个操作,那就有问题了。
dynamic_cast 的开销
这才是大头。dynamic_cast 需要遍历继承链,检查类型是否匹配。对于单继承,开销还好;对于多继承或菱形继承,那可就麻烦了。
我记得有一次,一个同事在消息分发系统里大量使用 dynamic_cast 来做类型转换。每个消息处理都要 cast 好几次。后来我换成 static_cast + 手动类型枚举,性能直接翻了一倍。
| 操作 | 相对开销 | 说明 |
|---|---|---|
| typeid | 低 | 基本是虚表指针访问 |
| dynamic_cast(单继承) | 中 | 遍历继承链,但链不长 |
| dynamic_cast(多继承) | 高 | 需要多次偏移计算和类型检查 |
| dynamic_cast(失败时) | 更高 | 需要遍历所有可能路径才能确认失败 |
小技巧:如果你必须用 RTTI,尽量用 typeid 做一次判断,然后用 static_cast 转换。别反复 dynamic_cast。
如何禁用异常
很多高性能项目(比如游戏引擎、嵌入式系统)会直接禁用异常。做法很简单:
编译器层面
在编译选项中加上:
// GCC/Clang
-fno-exceptions
// MSVC
/EHsc-
这样编译器就不会生成异常处理相关的代码了。正常路径的开销直接归零。
标准库层面
禁用异常后,标准库的某些功能会受影响。比如 std::vector::at() 会抛出 std::out_of_range,现在它不能抛了——行为变成未定义。
所以你需要:
- 用
operator[]代替at() - 用
std::optional或错误码代替异常 - 自己实现简单的断言机制
注意:禁用异常后,new 操作失败也不会抛 std::bad_alloc,而是返回 nullptr。如果你用了 nothrow 版本,记得检查返回值。
代码层面的替代方案
我习惯用这种模式来替代异常:
// 错误码方式
enum class ErrorCode {
Success,
NotFound,
InvalidArg,
OutOfMemory
};
// 或者用 optional
std::optional<Result> doSomething() {
if (/* 失败 */) {
return std::nullopt;
}
return Result{...};
}
说实话,这种模式在性能上比异常好得多。没有栈展开,没有动态内存分配,就是简单的值返回和判断。
知识体系总览
下面这张图总结了异常和 RTTI 的性能影响,以及禁用异常的路径:
总结一下:异常和 RTTI 不是不能用,但你要知道它们的代价。在性能敏感的场景下,我建议禁用异常,用错误码或 optional 替代。RTTI 尽量少用,尤其是 dynamic_cast——能用虚函数解决的就别用 cast。
嗯,今天就聊到这。记住一句话:没有银弹。每个特性都有它的适用场景,关键是你要清楚它背后的成本。
公众号:蓝海资料掘金营,微信deep3321