You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

函数指针性能困惑:单次与多次调用的速度差异及顺序影响

函数指针调用的性能差异分析

我关注通过函数指针调用函数的执行速度,最初发现通过传入参数的函数指针调用,比本地声明的函数指针调用更慢。以下是测试代码,两个测试函数均通过函数指针执行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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 05:01:42