我在C中读到了函数指针。 每个人都说会让我的程序运行缓慢。 这是真的吗?
我制作了一个程序来检查它。 我在两个案例中得到了相同的结果。 (衡量时间。)
那么,使用函数指针是不是很糟糕? 提前谢谢。
回应一些人。 在循环中我比较的时候我说“运行缓慢”。 像这样:
int end = 1000;
int i = 0;
while (i < end) {
fp = func;
fp ();
}
执行此操作时,如果执行此操作,我会得到相同的时间。
while (i < end) {
func ();
}
所以我认为函数指针没有时间差异 许多人说,它并没有使程序运行缓慢。
答案 0 :(得分:81)
您可以看到,在从性能角度来看实际上很重要的情况下,例如在一个周期内多次重复调用该函数,性能可能完全不同。
对于那些习惯于将C代码视为抽象C机器执行的东西的人来说,这可能听起来很奇怪,它的“机器语言”与C语言本身密切相关。在这种情况下,“默认情况下”对函数的间接调用确实比直接调用慢,因为它正式涉及额外的内存访问以确定调用的目标。
然而,在现实生活中,代码由真实机器执行,并由优化编译器编译,该编译器非常了解底层机器架构,这有助于它为该特定机器生成最佳代码。在许多平台上,可能会发现从循环执行函数调用的最有效方法实际上会导致直接和间接调用的相同的代码,从而导致两者的相同性能。 / p>
例如,考虑x86平台。如果我们“直接”将直接和间接调用转换为机器代码,我们可能最终得到类似这样的东西
// Direct call
do-it-many-times
call 0x12345678
// Indirect call
do-it-many-times
call dword ptr [0x67890ABC]
前者在机器指令中使用立即操作数,并且通常比后者更快,后者必须从某个独立的内存位置读取数据。
此时请记住,x86架构实际上还有一种方法可以为call
指令提供操作数。它在寄存器中提供目标地址。关于这种格式的一个非常重要的事情是,它通常比上述更快。这对我们意味着什么?这意味着一个好的优化编译器必须并将利用这一事实。为了实现上述循环,编译器将尝试通过两个情况下的寄存器使用调用。如果成功,最终代码可能如下所示
// Direct call
mov eax, 0x12345678
do-it-many-times
call eax
// Indirect call
mov eax, dword ptr [0x67890ABC]
do-it-many-times
call eax
请注意,现在重要的部分 - 循环体中的实际调用 - 在两种情况下都完全相同。毋庸置疑,性能将与几乎完全相同。
有人甚至可能会说,无论听起来多么奇怪,在这个平台上直接调用(call
中具有立即操作数的调用)慢而不是间接调用因为间接调用的操作数是在寄存器中提供的(而不是存储在内存中)。
当然,在一般情况下,整个事情并不容易。编译器必须处理有限的寄存器可用性,别名问题等。但是像你的例子中那样简单的情况(甚至在更复杂的情况下),上述优化将由一个好的编译器执行并将完全消除循环直接调用和循环间接调用之间的性能差异。在调用虚函数时,这种优化在C ++中特别有效,因为在典型的实现中,所涉及的指针完全由编译器控制,使其充分了解别名图片和其他相关内容。
当然,总有一个问题是你的编译器是否足够聪明以优化这样的东西......
答案 1 :(得分:24)
我认为,当人们这样说时,他们指的是使用函数指针可能会阻止编译器优化(内联)和处理器优化(分支预测)。但是,如果函数指针是完成某些事情的有效方法,那么任何其他方法都可能会有同样的缺点。
除非您的函数指针在性能关键应用程序或非常慢的嵌入式系统中用于紧密循环,否则无论如何差异都可以忽略不计。
答案 2 :(得分:9)
很多人已经提出了一些好的答案,但我仍然认为有一点被遗漏了。函数指针确实添加了一个额外的解引用,这使得它们几个周期变慢,这个数字可以根据不良的分支预测而增加(顺便提一下,它几乎与函数指针本身无关)。另外,通过指针调用的函数不能内联。但人们缺少的是大多数人使用函数指针作为优化。
您将在c / c ++ API中找到函数指针的最常见位置是回调函数。这么多API这样做的原因是因为编写一个在事件发生时调用函数指针的系统比其他方法(如消息传递)更有效。我个人也使用函数指针作为更复杂的输入处理系统的一部分,其中键盘上的每个键都有一个通过跳转表映射到它的函数指针。这使我可以从输入系统中删除任何分支或逻辑,只需处理按键输入。
答案 3 :(得分:9)
每个人都说这会让我的 程序运行缓慢。这是真的吗?
这种说法很可能是错误的。首先,如果使用函数指针的替代方法类似于
if (condition1) {
func1();
} else if (condition2)
func2();
} else if (condition3)
func3();
} else {
func4();
}
这很可能相对比仅使用单个函数指针要慢得多。虽然通过指针调用函数确实有一些(通常是可忽略的)开销,但通常不是与比较相关的直接函数调用与通过指针调用的差异。
其次,在没有任何测量的情况下,永远不要优化性能。知道瓶颈在哪里是非常困难的(阅读不可能)知道,有时这可能非常不直观(例如Linux内核开发人员已经开始从函数中删除inline
关键字,因为它实际上伤害了表现。)
答案 4 :(得分:8)
通过函数指针调用函数比静态函数调用有些慢,因为前一个调用包含一个额外的指针解除引用。但是AFAIK在大多数现代机器上的这种差异可以忽略不计(除了一些资源非常有限的特殊平台)。
使用函数指针是因为它们可以使程序更简单,更清晰,更易于维护(当然,正确使用时)。这足以弥补可能的非常小的速度差异。
答案 5 :(得分:7)
使用函数指针比调用函数要慢,因为它是另一层间接。 (需要取消引用指针以获取函数的内存地址)。虽然速度较慢,但与程序可能执行的其他操作相比(读取文件,写入控制台)可以忽略不计。
如果你需要使用函数指针,请使用它们,因为任何试图做同样事情但避免使用它们的东西都会比使用函数指针更慢,更难维护。
答案 6 :(得分:6)
早期回复中有很多好处。
但是看看C qsort比较函数。因为比较函数不能内联并且需要遵循基于标准堆栈的调用约定,所以对于整数键,排序的总运行时间可以比数量级(更确切地说是3-10x)慢。否则相同的代码与直接,可内联的电话。
典型的内联比较是一系列简单的CMP和可能的CMOV / SET指令。函数调用还会产生CALL的开销,设置堆栈帧,进行比较,拆除堆栈帧并返回结果。注意,由于CPU流水线长度和虚拟寄存器,堆栈操作可能导致流水线停顿。例如,如果在上次修改的eax已完成执行的指令之前需要说eax的值(在最新的处理器上通常需要大约12个时钟周期)。除非CPU无法执行其他指令以等待,否则将发生管道停滞。
答案 7 :(得分:0)
可能。
答案取决于函数指针的用途,以及替代方案。如果函数指针用于实现作为程序逻辑的一部分的选择并且不能简单地删除,则将函数指针调用与直接函数调用进行比较会产生误导。我会继续前进,然后展示比较,然后回到这个想法。
与直接函数调用相比,函数指针调用在抑制内联时最有可能降低性能。因为内联是一种网关优化,所以我们可以编写一些病态的案例,其中函数指针比等效的直接函数调用任意慢:
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()
的
call_foo(int*, int):
xor eax, eax
ret
尼斯。 foo()
不仅被内联,而且这样做允许编译器消除整个前面的循环!生成的代码通过将寄存器与自身进行异或运算然后返回来简单地将返回寄存器清零。另一方面,编译器必须在call_foo_ptr()
(100多行gcc 7.3)中为循环生成代码,并且大多数代码实际上什么都不做(只要foo_ptr
仍然指向{{ 1}})。 (在更典型的场景中,您可以预期将一个小函数内联到一个热内循环中可能会将执行时间减少大约一个数量级。)
因此在最坏的情况下,函数指针调用比直接函数调用任意慢,但这是误导性的。事实证明,如果foo()
为foo_ptr
,则const
和call_foo()
会生成相同的代码。但是,这需要我们放弃call_foo_ptr()
提供的间接机会。 foo_ptr
成为foo_ptr
是否“公平”?如果我们对const
提供的间接感兴趣,那么不,但如果是这种情况,则直接函数调用也不是有效选项。
如果正在使用函数指针来提供有用的间接,那么我们可以移动间接或在某些情况下交换条件甚至宏的函数指针,但我们不能简单地删除它。如果我们已经确定函数指针是一个很好的方法但性能是一个问题,那么我们通常希望在调用堆栈中间接向上,以便我们在外部循环中支付间接成本。例如,在函数接受回调并在循环中调用它的常见情况下,我们可能会尝试将最里面的循环移动到回调中(并相应地更改每个回调调用的责任)。