为啥调用函数时会有开销?
Posted
技术标签:
【中文标题】为啥调用函数时会有开销?【英文标题】:Why is there overhead when calling functions?为什么调用函数时会有开销? 【发布时间】:2015-10-25 02:10:16 【问题描述】:通常,人们谈到在程序中调用函数会产生一定量的开销,或者是不可避免的一组额外的关注点和情况。与没有函数调用的类似程序相比,这可以更好地解释和比较吗?
【问题讨论】:
What happens in assembly language when you call a method/function? 的可能重复项 你指的“这些套路”是什么? 内联并不总是可能的。递归函数、虚函数和函数指针都是示例。 (有时它们仍然可以内联,但不是在一般情况下) 同样重要的是要注意,输入参数有时是常量值(硬编码参数,例如循环计数,在编译时已知,但因调用站点而异)。inline
ing 此类函数会将这些常量值暴露给编译器,从而实现更积极的优化。
【参考方案1】:
这取决于您的编译器设置及其优化代码的方式。一些函数是内联的。其他人不是。这通常取决于您是针对大小进行优化还是针对速度进行优化。
一般来说,调用函数会导致延迟有两个原因:
程序需要挂钩到内存中函数代码开始的某个随机位置。为此,它需要将当前光标位置保存到堆栈中,以便知道从哪里返回。此进程消耗多个 CPU 周期。
根据您的 CPU 体系结构,可能存在一个管道,它与您当前的指令执行并行地将内存中的下几条指令提取到 CPU 缓存中。这是为了加快执行速度。当您调用一个函数时,游标会挂接到一个完全不同的地址,并且所有缓存的指令都会从管道中刷新。这会导致进一步的延迟。
【讨论】:
1) 和 2) 不是标准。现代架构可以很好地预测代码执行。另请注意,函数调用总是是可预测的,因为您知道执行的位置,因此您可以预取代码并填充管道。那里没有延误。 它们适用于 x86 架构 AFAIK 不,x86CALL
这些天大约是一个周期。
调用约定也很重要。需要保存和恢复被调用者和调用者破坏的寄存器。当您添加堆栈调整和帧指针时,内联小函数通常更便宜,即使在优化大小时也是如此。【参考方案2】:
另请参阅 here 和 here,了解有关内联何时有意义的讨论。
内联
一般情况下,您只能向编译器建议inline
一个函数,但编译器可能会做出其他决定。 Visual Studio 提供了自己的 forceinline
关键字。有些函数不能内联,例如当它们是递归的或在编译时无法确定目标函数时(通过函数表调用,C++ 中的虚函数调用)。
我建议你相信编译器是否应该内联函数。如果您真的想内联代码,请考虑改用宏。
开销
使用函数时内存开销最小,因为您不会重复代码;内联代码被复制到调用站点。现在的性能开销可以忽略不计,因为现代架构真的很好地预测和调用,只需大约 1-2 个周期开销。
【讨论】:
【参考方案3】:函数可以是inlined
,但规范(大多数情况下)是函数位于特定地址,传递给函数的值放入堆栈,然后将结果放入堆栈并返回。
【讨论】:
在优化代码以使数据尽可能接近执行时,几乎所有架构都使用基于寄存器的调用约定。返回值总是在寄存器中返回。 有些是,但我认为这是一种优化。我做了很多嵌入式编程,其中很多都没有多少寄存器空间来做到这一点。 @Jens x86 仍然使用基于堆栈的调用约定,并且由于缺少寄存器,大多数其他微控制器仍然使用堆栈来传递参数。仅当寄存器足够小以适合 1 或 2 个寄存器时,返回值才会通过寄存器传递,否则将通过堆栈返回【参考方案4】:如果满足某些条件,函数当然可以内联,但它们绝对不总是内联。大多数情况下,调用函数会产生真正的非内联函数调用。函数调用附带一些额外的费用,例如
根据函数的调用约定为函数准备参数 接收函数的返回值 函数序言和结语代码,负责本地内存管理、参数内存管理和寄存器值保存 函数可能会破坏某些 CPU 寄存器,从而破坏它们在调用代码中的使用,从而阻碍优化 以非线性方式执行的代码的 CPU 缓存友好和虚拟内存友好行为较少如果函数体内嵌到调用代码中,所有这些都会产生开销。
【讨论】:
以上是关于为啥调用函数时会有开销?的主要内容,如果未能解决你的问题,请参考以下文章