C 函数指针会使程序变慢吗?

C 函数指针会使程序变慢吗?,c,pointers,function-pointers,C,Pointers,Function Pointers,我读过C语言中的函数指针。 每个人都说这会使我的程序运行缓慢。 这是真的吗 我做了一个程序来检查它。 我在这两种情况下都得到了相同的结果。测量时间 那么,使用函数指针不好吗? 提前谢谢 为了回应一些人。 我说“慢跑”,这是我在循环中比较的时间。 像这样: int end = 1000; int i = 0; while (i < end) { fp = func; fp (); } 当你执行这个的时候,如果我执行这个,我会得到相同的时间 while (i < end) {

我读过C语言中的函数指针。 每个人都说这会使我的程序运行缓慢。 这是真的吗

我做了一个程序来检查它。 我在这两种情况下都得到了相同的结果。测量时间

那么,使用函数指针不好吗? 提前谢谢

为了回应一些人。 我说“慢跑”,这是我在循环中比较的时间。 像这样:

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}
当你执行这个的时候,如果我执行这个,我会得到相同的时间

while (i < end) {
 func ();
}
所以我认为函数指针没有时间差
而且它不会像许多人所说的那样使程序运行缓慢。

通过函数指针调用函数比静态函数调用慢一些,因为前者调用包括一个额外的指针解引用。但是,除一些资源非常有限的特殊平台外,在大多数现代机器上,这种差异可以忽略不计


使用函数指针是因为当正确使用时,它们可以使程序更简单、更干净、更易于维护。这大大弥补了可能出现的非常小的速度差异。

使用函数指针比只调用函数要慢,因为它是另一层间接寻址。指针需要取消引用以获取函数的内存地址。虽然它的速度较慢,但与您的程序读取文件所做的一切相比,写入控制台的速度可以忽略不计


如果需要使用函数指针,使用它们是因为任何试图做同样的事情但避免使用它们的东西都会比使用函数指针更慢、更不易维护。

我认为人们这样说是指使用函数指针可能会阻止编译器优化内联和处理器优化分支预测。然而,如果函数指针是一种有效的方法来完成您正试图做的事情,那么任何其他方法都可能有同样的缺点


除非您的函数指针在性能关键型应用程序或速度非常慢的嵌入式系统的紧密循环中使用,否则这种差异很可能是微不足道的。

很多人都给出了一些很好的答案,但我仍然认为有一点被忽略了。函数指针确实增加了一个额外的解引用,这使它们慢了几个周期,这个数字可能会因为分支预测差而增加,这与函数指针本身几乎没有关系。此外,通过指针调用的函数不能内联。但人们缺少的是,大多数人使用函数指针作为优化

在c/c++API中,最常见的函数指针是回调函数。如此多的API之所以这样做,是因为编写一个在事件发生时调用函数指针的系统比其他方法(如消息传递)效率更高。就我个人而言,我还使用函数指针作为更复杂的输入处理系统的一部分,其中键盘上的每个键都有一个通过跳转表映射到它的函数指针。这使我能够从输入系统中删除任何分支或逻辑,只需处理输入的按键

每个人都说这会让我 程序运行缓慢。这是真的吗

这种说法很可能是错误的。首先,如果使用函数指针的替代方法是

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}
这很可能比只使用单个函数指针慢得多。虽然通过指针调用函数确实有一些通常可以忽略的开销,但通常与比较相关的不是直接函数调用与通过指针调用之间的差异


第二,不要在没有任何测量的情况下优化性能。知道瓶颈在哪里是非常困难的,不可能知道,有时这可能是非常不直观的,例如linux内核开发人员已经开始从函数中删除内联关键字,因为它实际上会影响性能。

您可以看到,在从性能角度看确实重要的情况下,就像在一个周期中多次重复调用函数一样,性能可能根本没有什么不同

对于习惯于将C代码视为由抽象C机器执行的东西的人来说,这听起来可能有些奇怪,因为抽象C机器的机器语言与C语言本身非常相似。在这种情况下,默认情况下,对函数的间接调用确实比直接调用慢,因为它正式涉及额外的内存访问以确定调用的目标

然而,在现实生活中,代码是由一台真正的机器执行的,并由一个对底层机器体系结构非常了解的优化编译器编译,这有助于它为特定的机器生成最优化的代码。在许多平台上,从一个循环执行函数调用的最有效方法可能是 直接调用和间接调用都使用相同的代码,导致两者的性能相同

以x86平台为例。如果我们真的把直接和间接的调用转换成机器代码,我们可能会得到这样的结果

// Direct call
do-it-many-times
  call 0x12345678

// Indirect call
do-it-many-times
  call dword ptr [0x67890ABC]
前者在机器指令中使用即时操作数,并且通常比后者快,后者必须从某个独立的内存位置读取数据

在这一点上,让我们记住x86体系结构实际上还有一种方法可以为调用指令提供操作数。它在寄存器中提供目标地址。这种格式非常重要的一点是,它通常比上述两种格式都快。这对我们意味着什么?这意味着一个好的优化编译器必须并且将利用这一事实。为了实现上述循环,编译器将尝试在这两种情况下通过寄存器使用调用。如果成功,最终代码可能如下所示

// Direct call

mov eax, 0x12345678

do-it-many-times
  call eax

// Indirect call

mov eax, dword ptr [0x67890ABC]

do-it-many-times
  call eax
注意,现在重要的部分——循环体中的实际调用——在这两种情况下完全相同。不用说,性能将几乎相同

甚至有人可能会说,不管听起来有多么奇怪,在这个平台上,直接调用——调用中包含立即操作数的调用——要比间接调用慢,只要间接调用的操作数是在寄存器中提供的,而不是存储在内存中

当然,整个事情在一般情况下并不那么容易。编译器必须处理寄存器的有限可用性,别名问题等。但这是像您的示例中这样简单的情况,甚至在更复杂的情况下,上述优化将由一个好的编译器执行,并将完全消除循环直接调用和循环间接调用之间的任何性能差异。在调用一个虚函数时,这种优化在C++中特别好,因为在一个典型的实现中,所涉及的指针完全由编译器控制,使它完全了解混叠图像和其他相关的东西。
当然,始终存在一个问题,即您的编译器是否足够聪明,能够优化类似的东西…

在前面的回答中有很多好的观点

不过,看看C qsort比较函数。因为比较函数不能内联,并且需要遵循标准的基于堆栈的调用约定,所以对于整数键,排序的总运行时间可以比直接内联调用的相同代码慢3-10倍

典型的内联比较是一系列简单的CMP指令,可能还有CMOV/SET指令。函数调用还会产生调用的开销,包括设置堆栈帧、进行比较、分解堆栈帧和返回结果。请注意,由于CPU管道长度和虚拟寄存器,堆栈操作可能导致管道暂停。例如,如果在最后修改的eax指令完成执行之前需要say eax的值,则在最新处理器上通常需要大约12个时钟周期。除非CPU可以执行其他指令,否则将发生管道暂停。

可能

答案取决于函数指针的用途,以及替代方案。如果函数指针被用于实现一个选项,而该选项是我们程序逻辑的一部分,并且不能简单地删除,那么将函数指针调用与直接函数调用进行比较会产生误导。尽管如此,我还是会继续进行比较,然后再回到这个想法

当函数指针调用禁止内联时,与直接函数调用相比,函数指针调用最有可能降低性能。因为内联是一种网关优化,我们可以在函数指针比等效的直接函数调用任意慢的情况下:

void foo(int* x) {
    *x = 0;
}

void (*foo_ptr)(int*) = foo;

int call_foo(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo(&r);
    return r;
}

int call_foo_ptr(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo_ptr(&r);
    return r;
}
关于call_foo:

很好。foo不仅已经内联,而且这样做允许编译器消除整个前面的循环!生成的代码通过对寄存器本身进行XORing,然后返回,从而将返回寄存器归零。另一方面,编译器必须在gcc 7.3的call_foo_ptr 100+行中为循环生成代码,并且只要foo_ptr仍然指向foo,大部分代码实际上什么都不做。在更典型的场景中,可以预期将一个小函数内联到一个热的内部循环中可能会将执行时间减少大约一个数量级

所以在最坏的情况下,函数指针调用比直接函数调用任意慢,但这是有误导性的。事实证明,如果foo_ptr是const,那么call_foo和call_foo_ptr将生成相同的代码。然而,这将要求我们放弃foo_ptr提供的间接寻址机会。我 foo_ptr成为const公平吗?如果我们对foo_ptr提供的间接寻址感兴趣,那么不是,但是如果是这样,那么直接函数调用也不是有效的选项


如果函数指针被用来提供有用的间接寻址,那么我们可以将间接寻址四处移动,或者在某些情况下将函数指针替换为条件甚至宏,但我们不能简单地删除它。如果我们认为函数指针是一种很好的方法,但性能是一个问题,那么我们通常希望将间接寻址向上拉到调用堆栈中,以便在外部循环中支付间接寻址的成本。例如,在函数接受回调并在循环中调用它的常见情况下,我们可以尝试将最内层的循环移动到回调中,并相应地更改每个回调调用的责任。

假设取消引用需要一个CPU周期。在2GHz的机器上,这是500皮秒或0.5纳秒。即使它需要一个以上的周期,它仍然会少于一毫秒。@Peter K.谢谢-我真的不确定它是在微秒还是纳秒范围内:-分支预测+推测执行意味着CPU实际上不必等待内存或L1d缓存的加载,然后再执行调用reg或调用[mem]间接分支。但是,如果不能尽早检查目标地址,它确实会增加分支预测失误的惩罚。当你说运行速度慢时,你会将其与什么进行比较?一切都是相对的。您的程序无法测量静态函数调用或通过函数指针之间的差异。编译器将用静态调用替换该调用。在您的示例中,优化器在编译时知道目标,并将删除间接调用。继续@tristopia的注释,为进行有效比较,请选择同时接受函数指针和函数对象的函数,如排序程序,例如std::sort,并对两种方法的结果进行比较。它们的运行时间是相同的,因为它们都是无限循环+1,我同意,与这里的任何其他代码相比,速度的减慢都可以忽略不计。最底层的答案总是最相关的。是的,我认为许多人关心的开销不是去引用的时间浪费,而是与恒定的地址值相比,它对预测执行不友好。但没有人无缘无故地使用函数指针。跳转表当我们编写长切换案例时,编译器通常会生成一个函数指针数组,因为慢预测比错误预测好。大多数现代CPU对间接分支和条件分支都有很好的预测。不过,一些较旧/低功耗的CPU对间接分支的预测较弱。但是,如果调用站点每次都使用函数指针,它们通常仍然可以。函数调用内联的可能性如何?我认为,直接通话的可能性略高于间接通话。这是胡说八道。编译器不会使用调用保留寄存器(如ebx,而不是eax)将直接调用转换为寄存器间接调用。在正确预测的情况下,调用rel32的速度同样快,预测失误的惩罚更低,并且可能消耗更少的分支预测资源。“英特尔优化手册”中的链接都没有提到这项技术,事实上,编译器只要有可能就会反其道而行使设备虚拟化,即使它们选择不内联。当您不必选择call reg时,唯一一次选择call reg的是对一个函数的多个辅助函数调用进行代码大小优化。紧循环中至少有一个函数指针可以很好地预测。但是,不内联的代价可能很高,特别是当函数很小,有多个参数,和/或通过引用传递/返回任何内容时。是的,阻止内联是不好的,但其余部分是错误的。所有现代x86 CPU都使用无序执行和寄存器重命名,这完全避免了所有WAW和战争危险。对eax的独立写入将启动一个新的依赖链。请参阅,和.Hi,您说过的函数指针确实添加了一个额外的解引用,这会使它们慢几个周期,这个数字可能会根据糟糕的分支预测而增加。听起来调用函数指针需要一个分支预测?但是你说,我个人也使用函数指针…键盘上的每个键都有一个通过跳转表映射到它的函数指针。这允许我删除任何分支…,这意味着使用跳转表调用函数指针可以避免分支预测失误。这两种说法不矛盾吗?谢谢
call_foo(int*, int):
  xor eax, eax
  ret