为什么内联被认为比函数调用更快?

kod*_*dai 40 c++ optimization compilation inline function-call

现在,我知道这是因为没有调用函数的开销,但是调用函数的开销真的那么重(并且值得让它内联的膨胀)?

根据我的记忆,当一个函数被调用时,比如说f(x,y),x和y被压入堆栈,堆栈指针跳转到一个空块,然后开始执行.我知道这有点过于简单了,但我错过了什么吗?一些推送和跳转来调用一个函数,真的有那么多的开销吗?

如果我忘了什么,请告诉我,谢谢!

AnT*_*AnT 61

除了没有调用(因此没有相关费用,如调用之前的参数准备和调用之后的清理)之外,内联还有另一个显着优势.当函数体被内联时,它的主体可以在调用者的特定上下文中重新解释.这可能会立即允许编译器进一步减少和优化代码.

举一个简单的例子,这个函数

void foo(bool b) {
  if (b) {
    // something
  }
  else {
    // something else
  }
}
Run Code Online (Sandbox Code Playgroud)

如果被称为非内联函数,则需要实际分支

foo(true);
...
foo(false);
Run Code Online (Sandbox Code Playgroud)

但是,如果上面的调用是内联的,编译器将立即消除分支.本质上,在上面的情况下,内联允许编译器将函数参数解释为编译时常量(如果参数是编译时常量) - 这对于非内联函数通常是不可能的.

然而,它甚至不仅限于此.一般而言,启用内联的优化机会更为深远.再举一个例子,当函数体被内联到特定调用者的上下文中时,编译器在一般情况下将能够将调用代码中存在的已知别名相关关系传播到内联函数代码中,从而可以优化函数的代码更好.

同样,可能的示例很多,所有这些都源于内联调用沉浸在特定调用者的上下文中的基本事实,从而实现了各种上下文优化,这对于非内联调用是不可能的.通过内联,您基本上可以获得原始函数的许多单独版本,每个版本都针对每个特定的调用者上下文进行了单独定制和优化.显然,这样做的代价是代码膨胀的潜在危险,但如果使用得当,它可以提供显着的性能优势.

  • 内联为您提供的另一个甜蜜优化是指令缓存效率.内联代码已经在缓存中的可能性更大,而被调用的代码很容易导致缓存未命中. (6认同)
  • @ Detmar,@ sbi:同意这可能很神秘。使用内联可以将热代码从甜蜜的L1指令高速缓存中推出,而使用函数调用意味着每个函数独立地位于高速缓存中,从而减少了缓存空间。这就是为什么在GCC上使用-O(减小大小)编译的代码比O2或O3可以反直觉地更快的原因。 (2认同)

Che*_*Alf 26

"有几次推动和跳转来调用函数,真的有那么多开销吗?"

这取决于功能.

如果函数的主体只是一个机器代码指令,则调用和返回开销可以是很多百分之百.比如说,6次,500%的开销.然后,如果你的程序只包含大量的函数调用,没有内联,你的运行时间增加了500%.

但是,在另一个方向,内联可能会产生不利影响,例如,因为没有内联的代码会适合一页内存.

所以答案总是在优化方面,首先是MEASURE.

  • 此外,一个非常短的函数可能比函数调用的设置和拆卸指令小,而内联可能实际上使代码更小.测量和配置文件. (10认同)

Pon*_*gge 12

没有调用和堆栈活动,这肯定会节省一些CPU周期.在现代CPU中,代码局部性也很重要:执行调用可以刷新指令管道并强制CPU等待获取内存.这在紧密循环中非常重要,因为主存储器比现代CPU慢得多.

但是,如果您的代码仅在应用程序中被调用了几次,请不要担心内联.如果在用户等待答案时被叫数百万次,请担心很多!


sbi*_*sbi 11

内联的经典候选者是一个访问者,就像std::vector<T>::size().

启用内联后,这只是从内存中获取变量,可能任何体系结构上的单个指令."少数推动和跳跃"(加上回报)很容易多次.

除此之外,对优化器一次可见的代码越多,它的工作就越好.通过大量内联,它可以同时看到大量代码.这意味着它可以将值保存在CPU寄存器中,并完全避免昂贵的内存之旅.现在我们可能会有几个数量级的差异.

然后是theres 模板元编程.有时这会导致递归调用许多小函数,只是为了在递归结束时获取单个值.(考虑在具有几十个对象的元组中获取特定类型的第一个条目的值.)启用内联后,优化器可以直接访问该值(记住,可能在寄存器中),折叠了几十个函数调用访问CPU寄存器中的单个值.这可以将糟糕的性能变成一个漂亮而快速的程序.


将状态隐藏为对象中的私有数据(封装)会产生成本.内联是从一开始就是C++的一部分,以便最大限度地降低这些抽象成本.那时候,编译器在检测内联(并拒绝坏内容)的优秀候选者方面明显比现在更糟糕,因此手动内联导致了相当大的速度提升.
如今,编译器被认为比内联更加聪明.编译器能够自动内联函数,或者不标记为用户标注的内联函数inline,即使它们可以.有人说内联应该完全留给编译器,我们甚至不应该把功能标记为inline.但是,我还没有看到一个全面的研究表明手动这样做是否仍然值得.所以暂时,我会继续自己做,并让编译器覆盖它,如果它认为它可以做得更好.


kil*_*ras 5

int sum(const int &a,const int &b)
{
     return a + b;
}
int a = sum(b,c);
Run Code Online (Sandbox Code Playgroud)

等于

int a = b + c
Run Code Online (Sandbox Code Playgroud)

没有跳跃 - 没有开销


ues*_*esp 5

考虑一个简单的函数,如:

int SimpleFunc (const int X, const int Y)
{
    return (X + 3 * Y); 
}    

int main(int argc, char* argv[])
{
    int Test = SimpleFunc(11, 12);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这转换为以下代码(MSVC++ v6,debug):

10:   int SimpleFunc (const int X, const int Y)
11:   {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]

12:       return (X + 3 * Y);
00401038   mov         eax,dword ptr [ebp+0Ch]
0040103B   imul        eax,eax,3
0040103E   mov         ecx,dword ptr [ebp+8]
00401041   add         eax,ecx

13:   }
00401043   pop         edi
00401044   pop         esi
00401045   pop         ebx
00401046   mov         esp,ebp
00401048   pop         ebp
00401049   ret
Run Code Online (Sandbox Code Playgroud)

您可以看到函数体只有4条指令,但只有15条指令用于函数开销,不包括另外3条用于调用函数本身的指令.如果所有指令都花费了相同的时间(它们没有),则80%的代码是函数开销.

对于像这样的平凡函数,函数开销代码很可能与主函数体本身一样长.当你有一个在深循环体中调用的琐碎函数数百万/数十亿次,那么函数调用开销就会变得很大.

与往常一样,关键是分析/测量以确定内联特定函数是否产生任何净性能增益.对于更"复杂"的功能而言,这些功能并非经常被称为"内联",因此内联的收益可能会非常小.

  • 这是一个调试版本,有内存保护和超大堆栈帧允许编辑和继续.您不能使用调试代码来分析优化! (6认同)