C++虚函数与普通成员函数基准测试问题咨询
C++虚函数与普通成员函数基准测试问题解答
问题背景
每个C开发者或许都听过“虚函数速度较慢”的说法,因此我决定对虚函数与普通成员函数进行基准测试。但我缺乏底层C代码基准测试经验,不清楚如何规避编译器或硬件(主要是分支预测器)的优化影响。
当前我的测试结果显示,虚函数与普通成员函数调用速度几乎一致,有时虚函数甚至更快。
测试代码(使用google/benchmark库)
static void BM_MemberCall(benchmark::State& state) { Base* b = new Derived(); for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(b->Foo()); benchmark::ClobberMemory(); } } BENCHMARK(BM_MemberCall); static void BM_VirtualCall(benchmark::State& state) { Base* b = new Derived(); for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(b->VirtualFoo()); benchmark::ClobberMemory(); } } BENCHMARK(BM_VirtualCall);
类定义(util.h)
#pragma once // 生成10个虚函数 #define VFOO(NAME, ID, ...) \ __attribute__((noinline)) virtual int NAME ## ID ## _0() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _1() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _2() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _3() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _4() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _5() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _6() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _7() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _8() __VA_ARGS__; \ __attribute__((noinline)) virtual int NAME ## ID ## _9() __VA_ARGS__; // 生成100个虚函数 #define VFOO100(NAME, ...) \ VFOO(NAME, 0, __VA_ARGS__) \ VFOO(NAME, 1, __VA_ARGS__) \ VFOO(NAME, 2, __VA_ARGS__) \ VFOO(NAME, 3, __VA_ARGS__) \ VFOO(NAME, 4, __VA_ARGS__) \ VFOO(NAME, 5, __VA_ARGS__) \ VFOO(NAME, 6, __VA_ARGS__) \ VFOO(NAME, 7, __VA_ARGS__) \ VFOO(NAME, 8, __VA_ARGS__) \ VFOO(NAME, 9, __VA_ARGS__) struct Base { int Foo(); VFOO100(A); __attribute__((noinline)) virtual int VirtualFoo(); VFOO100(Z); }; struct Derived : public Base { VFOO100(A, override); __attribute__((noinline)) int VirtualFoo() override; VFOO100(Z, override); };
函数实现(util.cpp)
所有函数的实现相同:
{ asm(""); return 0; }
测试结果样本
(两者速度相同) --------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------- BM_MemberCall 1.23 ns 1.23 ns 524344569 BM_VirtualCall 1.23 ns 1.23 ns 564752961 (虚函数更慢) --------------------------------------------------------- Benchmark Time CPU Iterations --------------------------------------------------------- BM_MemberCall 1.22 ns 1.22 ns 522068585 BM_VirtualCall 1.48 ns 1.33 ns 557440234 (虚函数更快!) Benchmark Time CPU Iterations --------------------------------------------------------- BM_MemberCall 1.33 ns 1.33 ns 475669505 BM_VirtualCall 1.25 ns 1.25 ns 555128195
测试环境
2024-02-12T17:41:29+03:00 Running ./src/apps/bench/function_calls/calls Run on (12 X 2600 MHz CPU s) CPU Caches: L1 Data 32 KiB L1 Instruction 32 KiB L2 Unified 256 KiB (x6) L3 Unified 12288 KiB Load Average: 3.04, 4.39, 4.52
问题解答
1. 如何正确测量虚函数与普通成员函数的性能差异?当前测试方法的错误?
虚函数的性能开销主要来自编译期未知的间接跳转和分支预测失效的惩罚,你的测试没有触发这些真实开销,核心问题如下:
- 固定目标的虚函数调用被分支预测完全优化:你始终用
Base*指向Derived对象,每次调用的都是同一个虚函数实现。现代CPU的分支预测器会100%命中这个跳转,直接消除了虚函数的核心开销。 - 函数体过于轻量化:你的函数只有空汇编和返回指令,调用开销(包括虚表查找、跳转)占比极低,测试结果的波动会被系统噪声(比如缓存命中、进程调度)主导。
- 虚表大小的设计无效:虽然你扩大了虚表,但
VirtualFoo的位置固定,CPU缓存会持续命中这个虚表条目,无法体现虚表访问的潜在延迟。 - 单一测试对象无法模拟真实场景:循环中重复访问同一个对象的虚表,缓存命中率接近100%,而真实场景中通常是多类型对象交替调用虚函数。
正确的测量方案:
- 引入多类型随机访问:创建包含多个不同派生类对象的数组,在循环中随机选择对象调用虚函数,让分支预测器无法精准预测,暴露虚函数的间接跳转开销。
- 增加函数体计算量:在函数内部添加实际计算(比如累加运算、数组遍历),让函数执行时间足够长,降低调用开销的测量误差。
- 控制测试环境:测试时关闭其他CPU密集型程序,多次运行测试取平均值,减少系统负载带来的波动。
- 强化优化约束:除了
noinline,可以用volatile修饰对象指针,或者添加更严格的内存屏障,防止编译器将虚函数调用优化为直接调用。
2. 为何测试中有时会出现虚函数速度更慢的情况?
偶尔的性能倒退通常是由以下硬件/系统因素导致:
- 分支预测临时失效:系统进程调度、缓存刷新等干扰会让分支预测器暂时丢失对虚函数跳转的预测,此时CPU会触发分支预测错误惩罚(清空流水线、重新取指),导致耗时增加。
- 缓存命中差异:如果虚表条目被踢出CPU缓存(比如其他进程占用了缓存资源),虚函数调用需要从内存加载虚表,而普通函数的地址是编译期确定的,大概率在指令缓存中,从而产生耗时差异。
- 编译器优化波动:不同编译次或优化阶段中,编译器对普通函数和虚函数的优化策略可能略有差异,偶尔会让普通函数的调用被更高效地处理。
内容的提问来源于stack exchange,提问作者PepeHands
相关产品推荐
相关产品推荐

