为什么C ++函数调用便宜?

时间:2014-05-03 11:59:49

标签: c++

在阅读Stroustrup" The C ++ Programming Language"时,我在p上看到了这句话。 108:

"使用的语法分析风格通常称为递归下降;它是一种流行且直截了当的自上而下的技术。在诸如C ++之类的语言中,函数调用相对便宜,它也很有效。"

有人可以解释为什么C ++函数调用便宜吗?我对一般性解释感兴趣,即如果可能的话,什么使得任何语言的函数调用都很便宜。

3 个答案:

答案 0 :(得分:18)

调用C或C ++函数(特别是当它们不是虚函数时)非常便宜,因为它只涉及少量机器指令,并且跳转(带有返回地址的链接)到已知位置。

在其他一些语言(例如Common Lisp,应用未知可变参数函数)时,它可能更复杂。

实际上,你应该进行基准测试:许多最近的处理器是乱序的&超标量,所以一次做“几件事”。

然而,optimizing compilers能够制作奇妙的技巧。

对于许多函数式语言,被调用函数通常是closure,需要一些间接(并且还传递闭合值)。

一些面向对象的语言(如Smalltalk)可能涉及在调用选择器(在任意接收器上)时搜索方法的字典。

解释的语言可能会有更大的函数调用开销。

答案 1 :(得分:8)

与大多数其他语言相比,C ++中的函数调用很便宜,其原因之一是:C ++建立在函数内联的概念之上,而(例如)java建立在所有东西 - 虚拟函数的概念之上。

在C ++中,大多数时候你正在调用一个函数,你实际上并没有生成call指令。特别是在调用小函数或模板函数时,编译器很可能会内联代码。在这种情况下,函数调用开销只是零。

即使函数没有内联,编译器也可以对函数的作用做出假设。例如:windows X64调用约定指定调用者应保存寄存器R12-R15,XMM6-XMM15。调用函数时,编译器必须在调用站点生成代码以保存和恢复这些寄存器。但是如果编译器可以证明寄存器R12-R15,XMM6-XMM15未被被调用函数使用,则可以省略这样的代码。调用虚函数时,这种优化要困难得多。

有时无法进行内联。常见的原因包括函数体在编译时不可用,函数太大。在这种情况下,编译器生成直接call指令。但是,由于调用目标是固定的,因此CPU可以很好地预取指令。尽管直接函数调用很快,但仍然存在一些开销,因为调用者需要在堆栈上保存一些寄存器,增加堆栈指针等。

最后,当使用带有virtual关键字的java函数调用或C ++函数时,CPU将执行虚拟call指令。与直接调用的区别在于目标不是固定的,而是存储在内存中。目标函数可能在程序运行期间发生变化,这意味着CPU无法始终在函数位置预取数据。现代CPU和JIT编译器有各种各样的技巧来预测目标函数的位置,但它仍然没有直接调用那么快。

tldr:C ++中的函数调用很快,因为C ++实现内联,默认情况下使用直接调用虚拟调用。许多其他语言没有像C ++那样实现内联,并且默认使用虚函数。

答案 2 :(得分:6)

函数调用的成本与从给定范围到另一个范围所需的操作集相关联,即,从当前执行到另一个函数的范围。请考虑以下代码:

void foo(int w) { int x, y, z; ...; }
int main() { int a, b, c; ...; foo(b); ...; }

执行从main()开始,您可能会将一些变量加载到寄存器/内存中。当您到达foo()时,可供使用的变量集不同:函数a, b, c的{​​{1}}值无法访问,如果您用完了可用的寄存器,存储的值必须溢出到内存中。

寄存器问题以任何语言显示。但是有些语言需要更复杂的操作才能从范围更改为范围:C ++只需将函数所需的任何内容推送到内存堆栈中,保留指向周围范围的指针(在这种情况下,在运行foo()时,你就是&#39 ; d能够达到foo()范围内w的定义。

其他语言必须分配和传递复杂数据,以允许访问周围的范围变量。这些额外的分配,甚至是在周围范围内搜索特定标签,都会大大增加函数调用的成本。