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

Why are C++ function calls cheap?

本文关键字:函数调用 c++ 为什么      更新时间:2023-10-16

在阅读Stroustrup的《c++编程语言》时,我在第108页看到了这句话:

"使用的语法分析风格通常称为递归下降;这是一种流行且直接的自顶向下技术。在像c++这样的函数调用相对便宜的语言中,它也是高效的。"

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

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

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

实际上,您应该进行基准测试:许多最新的处理器都是乱序的&

然而,优化编译器有很多神奇的技巧。

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

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

解释型语言可能有相当大的函数调用开销。

与大多数其他语言相比,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++那样实现内联,并且在默认情况下使用虚函数。

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

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

执行开始于main(),你可能有一些变量加载到寄存器/内存。当您到达foo()时,可使用的变量集是不同的:a, b, c的值不能通过函数foo()访问,并且,如果您用完可用的寄存器,存储的值将不得不溢出到内存中。

寄存器的问题出现在任何语言中。但是有些语言需要更复杂的操作来改变作用域:c++只是将函数所需的任何东西推入内存堆栈,维护周围作用域的指针(在这种情况下,当运行foo()时,您可以在main()的作用域中达到w的定义)。

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