调用函数时为什么会有开销?

22

通常,人们会谈到函数调用所产生的一定量的开销,或者是程序中不可避免的一组额外关注和情况。能否更好地解释并将其与没有函数调用的类似程序进行比较?


你所指的“这些例程”是什么? - Jens
5
并非总是能够进行内联。递归函数、虚函数和函数指针就是其中的例子。(有时它们仍然可以内联,但并非普遍情况)。 - Mysticial
同样需要注意的是,输入参数有时是常量值(硬编码参数,例如循环计数,在编译时已知但根据调用站点而异)。内联这样的函数会将这些常量值暴露给编译器,从而实现更激进的优化。 - user3528438
4个回答

11

这取决于您的编译器设置和代码优化方式。有些函数被内联,而其他函数则没有。通常这取决于您是为大小还是为速度进行优化。

通常,调用函数会导致延迟,原因如下:

  • 程序需要钩到某个随机内存位置,该位置是函数代码的起始位置。为了做到这一点,它需要将当前光标位置保存到堆栈中,以便知道返回的位置。这个过程消耗超过一个 CPU 周期。

  • 根据您的 CPU 架构,可能存在一个流水线,它可以将下一条指令从内存并行地提取到 CPU 缓存中,以加快执行速度。当您调用函数时,光标钩子到完全不同的地址,并且所有缓存的指令都从流水线中刷新。这会导致进一步的延迟。


7
  1. 和2) 不是常态。现代架构非常擅长预测代码执行。另外请注意,函数调用始终是可预测的,因为您知道执行的位置,因此可以预取代码并填充流水线。没有延迟。
- Jens
据我所知,它们是针对x86架构的。 - Francois Zard
不,现在x86的“CALL”只需要一个时钟周期。 - Jens
调用约定也很重要。被调用者和调用者破坏的寄存器需要保存和恢复。当你加入堆栈调整和帧指针时,即使在优化大小时,内联小函数通常更便宜。 - technosaurus

8

此外,这里这里有关于何时使用内联函数的讨论。

  • 内联

    通常情况下,你只能向编译器建议将函数inline,但编译器可能会作出不同的决定。Visual Studio提供了自己的forceinline关键字。有些函数不能内联,例如递归函数或目标函数不能在编译时确定(通过函数表调用、C++中的虚函数调用)。

    我建议你相信编译器是否应该内联函数。如果你真的想要内联你的代码,请考虑使用宏代替。

  • 开销

    使用函数可以将内存开销降到最低,因为你不会复制代码;内联代码会被复制到调用站点。现代架构非常擅长预测和调用,因此性能开销现在可以忽略不计,仅有约1-2个周期的开销。


2

这些函数可以被内联,但通常函数会在特定地址处,将传递给函数的值放在堆栈上,然后将结果放在堆栈上并返回。


几乎所有的架构在代码被优化时都使用基于寄存器的调用约定,以尽可能地将数据保持靠近执行。返回值总是在寄存器中返回。 - Jens
有些是这样的,但我认为这是一种优化。我做了很多嵌入式编程,其中许多在寄存器空间方面非常少。 - Keith Nicholas
@Jens x86 仍然使用基于堆栈的调用约定,而大多数其他微控制器由于缺乏寄存器仍然使用堆栈传递参数。如果返回值足够小,只需要使用1或2个寄存器即可传递,否则将通过堆栈返回。 - phuclv

2
如果满足某些条件,函数当然可以内联,但它们肯定不总是内联的。大多数情况下,调用一个函数会产生一个真正的非内联函数调用。函数调用会附带一些额外的开销,比如:
- 根据函数的调用约定准备参数 - 接收函数的返回值 - 函数序言和尾声代码,负责本地内存管理、参数内存管理和寄存器值保留 - 函数可能会破坏一些CPU寄存器,从而破坏调用代码中它们的使用,从而妨碍优化 - 在非线性方式执行的代码中,CPU缓存友好度和虚拟内存友好度较低
所有这些都会产生开销,如果函数主体被内嵌到调用代码中,这些开销很可能不存在。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接