函数指针是否使程序变慢?

问题描述 投票:51回答:8

我在C中读到了函数指针。每个人都说这将使我的程序运行缓慢。这是真的吗?

我制作了一个程序来检查它。我在两个案例中得到了相同的结果。 (衡量时间。)

那么,使用函数指针是不是很糟糕?提前致谢。

为了回应一些人。在循环中我比较的时候我说“运行缓慢”。像这样:

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}

执行此操作时,如果执行此操作,我会得到相同的时间。

while (i < end) {
 func ();
}

所以我认为函数指针没有时间差异,也没有像许多人所说的那样使程序运行缓慢。

c pointers function-pointers
8个回答
81
投票

您可以看到,在从性能角度来看实际上很重要的情况下,例如在一个周期中多次重复调用该函数,性能可能根本不同。

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

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

例如,考虑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 ++中特别有效,因为在典型的实现中,所涉及的指针完全由编译器控制,使其充分了解别名图片和其他相关内容。

当然,总有一个问题是你的编译器是否足够智能以优化这样的事情......


24
投票

我想当人们这么说时,他们指的是使用函数指针可能会阻止编译器优化(内联)和处理器优化(分支预测)。但是,如果函数指针是一种有效的方法来完成你想要做的事情,那么任何其他方法都可能会有同样的缺点。

除非您的函数指针在性能关键应用程序或非常慢的嵌入式系统中用于紧密循环,否则无论如何差异都可以忽略不计。


9
投票

很多人都提出了一些好的答案,但我仍然认为有一点被遗漏了。函数指针确实添加了一个额外的解引用,这使得它们几个周期变慢,这个数字可以根据不良的分支预测而增加(顺便提一下,它几乎与函数指针本身无关)。另外,通过指针调用的函数不能内联。但人们缺少的是大多数人使用函数指针作为优化。

您将在c / c ++ API中找到函数指针的最常见位置是回调函数。这么多API这样做的原因是因为编写一个在事件发生时调用函数指针的系统比其他方法(如消息传递)更有效。我个人也使用函数指针作为更复杂的输入处理系统的一部分,其中键盘上的每个键都有一个通过跳转表映射到它的函数指针。这允许我从输入系统中删除任何分支或逻辑,只是处理进入的按键。


9
投票

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

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

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}

这很可能比使用单个函数指针慢得多。虽然通过指针调用函数确实有一些(通常是可忽略的)开销,但通常不是与比较相关的直接函数调用与通过指针调用的差异。

其次,在没有任何测量的情况下,永远不要优化性能。知道瓶颈在哪里是非常困难的(阅读不可能)知道,有时这可能是非直观的(例如Linux内核开发人员已经开始从函数中删除inline关键字,因为它实际上损害了性能)。


8
投票

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

使用函数指针是因为它们可以使程序更简单,更清晰,更易于维护(当然,正确使用时)。这足以弥补可能非常小的速度差异。


7
投票

使用函数指针比调用函数要慢,因为它是另一层间接。 (需要取消引用指针以获取函数的内存地址)。虽然速度较慢,但​​与程序可能执行的其他操作(读取文件,写入控制台)相比,它可以忽略不计。

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


6
投票

早期回复中有很多好处。

但是看看C qsort比较函数。因为比较函数不能内联并且需要遵循基于标准堆栈的调用约定,所以对于整数键,排序的总运行时间可以比一个直接的相同代码慢一个数量级(更确切地说是3-10x),内联,电话。

典型的内联比较将是一系列简单的CMP和可能的CMOV / SET指令。函数调用还会产生CALL的开销,设置堆栈帧,进行比较,拆除堆栈帧并返回结果。注意,由于CPU流水线长度和虚拟寄存器,堆栈操作可能导致流水线停顿。例如,如果在上次修改的eax已完成执行的指令之前需要说eax的值(在最新的处理器上通常需要大约12个时钟周期)。除非CPU无法执行其他指令以等待,否则将发生管道停顿。


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;
}

Code generatedcall_foo()

call_foo(int*, int):
  xor eax, eax
  ret

尼斯。 foo()不仅被内联,而且这样做允许编译器消除整个前面的循环!生成的代码通过将寄存器与自身进行异或运算然后返回来简单地将返回寄存器清零。另一方面,编译器必须为call_foo_ptr()中的循环生成代码(100+行与gcc 7.3)并且大多数代码实际上没有做任何事情(只要foo_ptr仍然指向foo())。 (在更典型的场景中,您可以预期将一个小函数内联到一个热内循环中可能会将执行时间缩短大约一个数量级。)

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

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

© www.soinside.com 2019 - 2024. All rights reserved.