Virtual——潜在的性能杀手

virtual 关键字是 C++ 运行时多态的重要特征,在我的日常学习中感受并不深。但是在优化代码性能时,组长却极力强调去除 virtual,并指出它会带来不小的性能负担。

由于我对 virtual 的理解并不深入,当时只是照做。不过优化之后确实有了一些性能提升,因此决定尝试认真了解一下 virtual 的机制,并记录下这个过程。

需要说明的是,目前我对 virtual 的认识还没达到“深入理解”的程度,内容也可能会随着我对 C++ 更深入的学习不断补充和修正(但愿我会想起来更新)。

在查阅资料过程中,我总结了一些比较有意思的看法:

  1. virtual 的性能问题主要来自于阻碍编译器 inline 优化;
  2. 在没有明确瓶颈时,没必要过早优化;
  3. 很多网上的 benchmark 数据可能过时,不妨自己验证;
  4. 为了“去 virtual”而打破封装未必值得,还是要兼顾设计合理性。

性能测试

使用 Google Benchmark 分别测试了小对象与大对象在调用普通函数与虚函数时的性能表现。

测试项 N=1,000 (ns) N=10,000 (ns) N=100,000 (ns)
Small_Normal 489 4,971 50,812
Small_Virtual 1,485 14,697 149,316
Large_Normal 511 7,693 108,530
Large_Virtual 1,327 17,204 230,358

分析总结:

  1. 虚函数比普通函数慢
    在小对象上,虚函数调用比普通调用慢约 3 倍;大对象上大约慢 2.5 倍。
    原因是虚函数通过 vtable 调用,增加了一次间接寻址,破坏了 CPU 指令预测和内联优化。

  2. 对象大小影响普通调用更大
    小对象的普通函数更快(5μs vs. 7.7μs),但虚函数差异不大,说明大对象的复制和访问成本对普通函数影响更大,而虚函数已经有固定的额外开销。

  3. 耗时基本线性增长
    不论是普通函数还是虚函数,调用时间都随 N 增大而线性增长,说明整体调用成本稳定,无明显隐藏开销。

  4. 大对象下虚函数绝对耗时更高
    在 N=100000 的场景下,大对象虚函数耗时超过 230ms,而小对象约 149ms,说明大对象的缓存和内存访问开销仍然显著影响调用效率。

开销来源

1. 间接调用

虚函数不能通过静态地址直接调用,而是通过 vptr(虚指针)查表找到目标函数地址后跳转:

1
obj->virtualFunction(); // 实际调用过程:obj->vptr->vtable[N]

代价:

  • 增加一次指针间接寻址;
  • 阻碍编译器 inline 优化;
  • 对分支预测不友好;
  • 指令缓存和流水线效率下降。

2. 无法内联

  • 普通函数如果可 inline,可在调用处直接展开,避免函数栈帧开销;
  • 虚函数调用在运行时决定目标,编译器无法内联展开。

3. 增加对象结构复杂度

  • 每个含虚函数的对象含一个 vptr 指针;
  • 每个类编译时生成一个 vtable
  • 多重继承、虚继承会有多个 vptr,增加开销与管理复杂度。

4. 编译器优化受限

虚函数调用对编译器的分析造成障碍:

  • 不能进行跨函数优化(IPO);
  • 死代码移除、代码去重受限;
  • 很难做“去虚拟化(de-virtualization)”优化。

一些取舍与思考

方面 正面(优势) 负面(代价)
灵活性 支持多态、可扩展设计 类型体系复杂,调试和维护成本上升
性能 —— 间接调用、编译器优化难度加大
内存 —— 每个对象额外含一个 vptr,类含 vtable
可维护性 接口清晰、设计优雅 若滥用抽象,反而使结构难以理解

使用建议:何时该用 virtual,何时不该用?

适合使用 virtual 的情况

  • 接口设计稳定,需要支持多种实现;
  • 插件式系统或运行时策略切换场景;
  • 框架或库中暴露统一入口;
  • 对性能不敏感,但强调可扩展性与解耦。

不建议使用 virtual 的情况

  • 热路径性能敏感代码(图形渲染、游戏循环、音频编解码等);
  • 调用频率极高(每秒数百万次)的函数;
  • 只有一个实现,后续也不会扩展;
  • 可以通过模板 / CRTP 替代运行时多态。

可选替代方案:在性能与多态之间求平衡

1. CRTP(Curiously Recurring Template Pattern)

利用模板实现“编译期多态”,既支持内联,又不需要虚函数:

1
2
3
4
5
6
7
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};

适合单继承、可通过模板展开的场景。

2. 函数对象或 Lambda

可用 std::function、函数对象或 lambda 实现策略注入等模式,适用于组合行为逻辑而非类型派发。

3. 手写 vtable 表(LLVM/Dyno 风格)

极端情况下,可以手写一个 vtable 表,用函数指针模拟虚函数行为,节省 RTTI 和对象元信息开销,常见于编译器、解释器等系统中。

其他

在我们的项目中,图算法中存在的大量的Graph Edge Vertex,完美符合上面提到的2点:

  • 调用频率极高(每秒数百万次)的函数;
  • 可以通过模板 / CRTP 替代运行时多态。

Edge的创建少则上万,多则百万,效率和内存开销都很大,而且也没有真正的运行时多态的需求,所以采用了参见上一篇的CRTP进行优化,将虚函数改为模板函数。

在最终的测试中,改动前后的性能提升其实很小(2%~3%),但是分配的堆内存由11MB降低到了8MB(当然也不仅仅虚函数是这一点影响到了内存)。

附上benchmark代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <benchmark/benchmark.h>
#include <vector>
#include <memory>

// ========== 可调配置 ==========
constexpr int DEFAULT_N = 10000;

// 对象大小:small = 4字节, large = 1KB
template <bool Large>
struct ObjectBase {
virtual int virt() const { return get()[0] + 1; }
int normal() const { return get()[0] + 1; }
protected:
mutable int data[Large ? 256 : 1] = {};
int* get() const { return const_cast<int*>(data); }
};

template <bool Large>
struct ObjectDerived : public ObjectBase<Large> {
int virt() const override { return this->get()[0] + 1; }
};

// ========== Benchmark 主体模板 ==========

template <typename Obj, typename Base, bool IsVirtual>
void BenchmarkCall(benchmark::State& state) {
const int N = static_cast<int>(state.range(0));
std::vector<Obj> objects(N);
std::vector<const Base*> ptrs;

if constexpr (IsVirtual) {
ptrs.resize(N);
for (int i = 0; i < N; ++i)
ptrs[i] = &objects[i];
}

int result = 0;
for (auto _ : state) {
for (int i = 0; i < N; ++i) {
if constexpr (IsVirtual) {
result += ptrs[i]->virt();
} else {
result += objects[i].normal();
}
}
benchmark::DoNotOptimize(result);
benchmark::ClobberMemory();
}
}

// ========== 注册 Benchmark ==========

void RegisterAllBenchmarks() {
benchmark::RegisterBenchmark("Small_Normal", BenchmarkCall<ObjectDerived<false>, ObjectBase<false>, false>)
->Arg(1000)->Arg(10000)->Arg(100000);

benchmark::RegisterBenchmark("Small_Virtual", BenchmarkCall<ObjectDerived<false>, ObjectBase<false>, true>)
->Arg(1000)->Arg(10000)->Arg(100000);

benchmark::RegisterBenchmark("Large_Normal", BenchmarkCall<ObjectDerived<true>, ObjectBase<true>, false>)
->Arg(1000)->Arg(10000)->Arg(100000);

benchmark::RegisterBenchmark("Large_Virtual", BenchmarkCall<ObjectDerived<true>, ObjectBase<true>, true>)
->Arg(1000)->Arg(10000)->Arg(100000);
}

int main(int argc, char** argv) {
RegisterAllBenchmarks();
benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
return 0;
}

Virtual——潜在的性能杀手
http://example.com/2025/06/29/Virtual_in_cpp/
作者
icyyoung
发布于
2025年6月29日
许可协议