真实虚函数开销需测多态场景下的流水线、缓存与优化连锁影响,须关闭内联、用volatile防优化、混杂派生类类型;perf应关注分支预测失败、指令页分散、CPI下降;对比需设虚函数组、静态分派组、单类型基线三组。

怎么测才暴露真实虚函数开销
只测单次空虚函数调用毫无意义——现代 CPU 下那点 1–3 周期开销,远小于一次 L1 缓存未命中(~4 周期)或分支预测失败(~15 周期)。真正要测的,是它在典型多态场景下对流水线、缓存和编译器优化的连锁破坏。
必须满足三个条件,测量结果才有参考价值:
- 关闭编译器内联:
-fno-inline(GCC/Clang)或/Ob0(MSVC),否则compute()可能被直接展开,虚调用根本没发生 - 用
volatile阻止死代码消除:volatile int sink = 0;,然后在循环里写sink += ptr->compute(i);,否则整个循环可能被优化成常量 - 对象类型必须混杂:不能全用
Derived*,得用std::vector<:unique_ptr>></:unique_ptr>或原始指针数组,且内容包含至少两种派生类,逼出 vtable 切换和分支预测压力
perf 能抓到哪些关键信号
Linux 下用 perf record -e cycles,instructions,br_misp_retired.all_branches,dtlb_load_misses.miss_causes_a_walk 运行你的测试循环,再 perf report 看热点。重点关注:
-
br_misp_retired.all_branches显著偏高 → 说明间接跳转(call [rax + 0x10])让 CPU 分支预测器频繁失准,这是虚函数最典型的性能疤痕 - 指令地址分散在多个不同代码页 →
perf script输出里看到compute的符号跳来跳去,说明虚函数体没聚集,iTLB 和 L1i 缓存效率下降 - cycles/instruction 比基线低(比如降到 0.8 以下)→ 流水线停顿严重,大概率是 vptr/vtable 查表引发的 cache miss 连锁反应
对比基线怎么设才公平
别拿“去掉 virtual”当唯一对照组——那只是消除了语法,没反映设计替代方案的真实成本。应该跑三组:
立即学习“C++免费学习笔记(深入)”;
-
虚函数组:原样
Base*数组 +virtual compute() -
静态分派组:改用
std::variant<derived1 derived2></derived1>+std::visit,或者 CRTP 模板,保持运行时类型混合但无虚表 -
单类型基线:把数组强制 cast 成
Derived1*直接调用,用来量化“纯间接跳转+cache 影响”的下限
三者跑出来的时间差,才能告诉你:到底是虚函数本身慢,还是你的多态模式(类型混杂+小函数体+高频调用)天然就不适合动态分发。
容易被忽略的陷阱:LTO 和跨 TU 优化
即使你写了 final 和 override,如果虚函数调用发生在另一个 .cpp 文件里,而定义在当前文件,GCC/Clang 默认不会 devirtualize——因为链接时看不到具体类型。这时候:
- 要么打开
-flto(Link Time Optimization),让编译器全局分析 - 要么把关键虚函数实现移到头文件,配合
inline(注意:仅适用于小函数,且会增大二进制体积) - 要么用
[[gnu::hot]]或[[msvc::optimize("speed")]]提示编译器重点优化该调用点,有时能触发局部去虚拟化
不检查是否真发生了 devirtualization,就去调优虚函数,等于在修一辆没发动的车——所有测量都建立在错误前提上。



















