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

时间:2010-10-25 15:23:20

标签: c++ stack inline

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

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

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

16 个答案:

答案 0 :(得分:60)

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

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

void foo(bool b) {
  if (b) {
    // something
  }
  else {
    // something else
  }
}
如果被称为非内联函数,

将需要实际分支

foo(true);
...
foo(false);

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

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

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

答案 1 :(得分:26)

“一些推动和跳转来调用函数,真的有那么多开销吗?”

这取决于功能。

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

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

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

答案 2 :(得分:12)

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

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

答案 3 :(得分:11)

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

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

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

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


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

答案 4 :(得分:5)

int sum(const int &a,const int &b)
{
     return a + b;
}
int a = sum(b,c);

等于

int a = b + c

没有跳跃 - 没有开销

答案 5 :(得分: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;
}

将其转换为以下代码(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

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

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

与往常一样,关键是分析/测量以确定内联特定函数是否产生任何净性能增益。对于那些不经常被称为“复杂”的“复杂”函数,内联的收益可能无法估量。

答案 6 :(得分:4)

内联的原因有多种,其中只有一个显而易见:

  • 没有跳转说明。
  • 更好的本地化,从而提高缓存利用率。
  • 编译器的优化器有更多机会进行优化,例如在寄存器中保留值。

缓存利用率也可能对您不利 - 如果内联使代码变大,则缓存未命中的可能性更大。尽管如此,这是一个不太可能的情况。

答案 7 :(得分:3)

它产生重大影响的一个典型例子是std :: sort,它的比较函数是O(N log N)。

尝试创建一个大尺寸的向量,首先使用内联函数调用std :: sort,然后使用非内联函数调用std :: sort并测量性能。

顺便说一句,这是C ++中的排序比C中的qsort快,这需要一个函数指针。

答案 8 :(得分:2)

跳转的另一个潜在副作用是,您可能会触发页面错误,或者是第一次将代码加载到内存中,或者是否经常使用它以便稍后从内存中读取内存。

答案 9 :(得分:2)

  

(值得拥有它的臃肿   内联)

内嵌并不总是会产生更大的代码。例如,简单的数据访问功能,例如:

int getData()
{
   return data ;
}

将导致作为函数调用的指令周期明显多于内联函数,并且此类函数最适合于内嵌函数。

如果函数体包含大量代码,则函数调用开销确实无关紧要,如果从多个位置调用它,可能确实会导致代码膨胀 - 尽管编译器很可能只是忽略在这种情况下的内联指令。

你还应该考虑打电话的频率;即使对于大型代码体,如果从一个位置频繁调用该函数,在某些情况下保存可能是值得的。它归结为调用开销与代码体大小的比率以及使用频率。

当然,您可以将其交由编译器来决定。我只是显式内联函数,它包含一个不涉及进一步函数调用的单个语句,这更多的是类方法的开发速度而不是性能。

答案 10 :(得分:2)

安德烈的答案已经给你一个非常全面的解释。但只是为了增加一个他错过的点,内联在非常短的函数上也是非常有价值的。

如果一个函数体只包含几条指令,那么序言/结尾代码(基本上是push / pop / call指令)实际上可能比函数体本身更昂贵。如果经常调用这样的函数(例如,从紧密循环中调用),那么除非函数被内联,否则最终会将大部分CPU时间花在函数调用上,而不是函数的实际内容。

重要的不是函数调用的绝对成本(它可能只需要5个时钟周期或类似的东西),但相对于调用函数的频率需要多长时间。如果函数太短以至于可以每10个时钟周期调用一次,那么每次调用“不必要的”推/弹指令就花费5个周期是非常糟糕的。

答案 11 :(得分:1)

因为没有电话。功能代码只是复制

答案 12 :(得分:1)

内联函数是建议编译器用定义替换函数调用。如果它被替换,那么将没有函数调用堆栈操作[push,pop]。但它总是不能保证。 :)

- 干杯

答案 13 :(得分:1)

优化编译器应用一组启发式方法来确定内联是否有益。

有时从缺少函数调用中获得的收益将超过额外代码的潜在成本,有时不会。

答案 14 :(得分:0)

在多次调用函数时,内联会产生很大的不同。

答案 15 :(得分:-1)

因为没有进行跳转。