函数指针性能困惑:单次与多次调用的速度差异及顺序影响
函数指针调用的性能差异分析
我关注通过函数指针调用函数的执行速度,最初发现通过传入参数的函数指针调用,比本地声明的函数指针调用更慢。以下是测试代码,两个测试函数均通过函数指针执行lambda:
#include <chrono> #include <iostream> using namespace std; __attribute__((noinline)) int plus_one(int x) { return x + 1; } typedef int (*FUNC)(int); #define OUTPUT_TIME(msg) std::cout << "Execution time (ns) of " << msg << ": " << std::chrono::duration_cast<chrono::nanoseconds>(t_end - t_start).count() << std::endl; #define START_TIMING() auto const t_start = std::chrono::high_resolution_clock::now(); #define END_TIMING(msg) auto const t_end = std::chrono::high_resolution_clock::now(); OUTPUT_TIME(msg); auto constexpr g_count = 1000000; __attribute__((noinline)) int speed_test_no_param() { int r; auto local_lambda = [](int a) { return plus_one(a); }; FUNC f = local_lambda; START_TIMING(); for (auto i = 0; i < g_count; ++i) r = f(100); END_TIMING("speed_test_no_param"); return r; } __attribute__((noinline)) int speed_test_with_param(FUNC &f) { int r; START_TIMING(); for (auto i = 0; i < g_count; ++i) r = f(100); END_TIMING("speed_test_with_param"); return r; } int main() { int ret = 0; auto main_lambda = [](int a) { return plus_one(a); }; ret += speed_test_no_param(); FUNC fp = main_lambda; ret += speed_test_with_param(fp); return ret; }
在Ubuntu 20.04上使用以下命令编译:
g++ -ggdb -ffunction-sections -O3 -std=c++17 -DNDEBUG=1 -DRELEASE=1 -c speed_test.cpp -o speed_test.o && g++ -o speed_test -Wl,-gc-sections -Wl,--start-group speed_test.o -Wl,--rpath='$ORIGIN' -Wl,--end-group
观测到的现象
- 多次调用(
g_count=1000000)时:无参数版本(speed_test_no_param)耗时仅74ns,带参数版本耗时1173849ns;查看汇编发现无参数版本直接调用plus_one,带参数版本需先获取lambda地址再跳转。 - 单次调用时:结果反转,无参数版本耗时61ns,带参数版本耗时31ns。
- 调用顺序影响:先调用带参数版本时,无参数版本更快;先调用无参数版本时,带参数版本更快。
现象解释
1. 多次循环下的编译器优化差异
- 无参数版本中,本地无捕获lambda的函数指针是编译期可确定的常量。结合
-O3优化,编译器能彻底消除函数指针的间接调用,直接将循环内的f(100)替换为plus_one(100),甚至整个循环都可能被优化成直接赋值(因为每次循环结果相同)。74ns的耗时基本是计时API本身的开销,没有实际执行多少有效指令。 - 带参数版本中,函数指针通过引用传入,编译器无法在编译期确定其指向(存在被外部修改的可能性),只能保留间接跳转的逻辑。每次循环都要从内存中读取指针地址,再执行函数跳转,100万次循环的累积开销被放大,导致耗时剧增。
2. 单次调用时的缓存与计时误差影响
当调用次数仅为1次时,优化带来的差异消失,性能由以下因素主导:
- CPU指令缓存(ICache):先执行的函数会将自身及依赖的指令(如
plus_one)加载到高速缓存中,后执行的函数能直接命中缓存,减少内存访问开销,因此看起来耗时更短。 - 计时精度限制:单次调用的执行时间极短,
high_resolution_clock的精度有限,31ns和61ns的差异可能只是计时波动,而非两种调用方式的本质性能差距。
3. 调用顺序影响的核心是缓存预热
调用顺序导致的性能反转,本质是缓存预热效应:第一个被调用的函数完成了指令缓存的加载,第二个函数受益于已预热的缓存,避免了缓存未命中的开销,因此表现出更快的执行速度。
内容的提问来源于stack exchange,提问作者Wad
相关产品推荐
相关产品推荐

