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

现在,我知道这是因为没有调用函数的开销,而是调用函数的开销真的很重(并且值得让它内联)。

从我记得,当一个函数被调用时,说f(x,y),x和y被压入堆栈,堆栈指针跳转到一个空的块,并开始执行。 我知道这是一个过分简单化,但我错过了什么? 一些推动和跳转来调用一个函数,真的有那么多的开销?

让我知道,如果我忘记了一些东西,谢谢!

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

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

 void foo(bool b) { if (b) { // something } else { // something else } } 

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

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

但是,如果上面的调用内联,编译器将立即能够消除分支。 实际上,在上面的例子中,内联允许编译器将函数参数解释为编译时常量(如果该参数是编译时常量) – 对于非内联函数通常是不可能的。

但是,它甚至不受限于此。 一般来说,启用内联的优化机会显着更为深远。 又如,当函数体被内联到特定调用者的上下文中时,编译器通常情况下将能够将调用代码中存在的已知混叠相关关系传播到内联函数代码中,从而使得可以优化函数的代码更好。

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

“几个推,一个函数调用,真的有那么多的开销吗?”

这取决于function。

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

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

所以答案总是说到优化,首先是MEASURE。

没有调用和堆栈活动,这肯定节省了几个CPU周期。 在现代CPU中,代码局部性也很重要:进行调用可以刷新指令stream水线 ,强制CPU等待内存被提取。 这在严格的循环中很重要,因为主内存比现代CPU慢很多。

但是,不要担心内联是否只在应用程序中调用了几次代码。 担心,如果在用户等待答案时被调用了数百万次!

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

在内联启用的情况下,这只是从内存中获取variables,可能任何体系结构上的单个指令 。 “less推一跳”(加上回报)很容易多次

除此之外,一个优化器可以同时看到更多的代码,它可以更好地工作。 有了大量的内联,它会立即看到大量的代码。 这意味着它可能能够将值保存在CPU寄存器中 ,并且完全省去了昂贵的内存访问。 现在我们可能要差几个数量级

然后是模板元编程 。 有时这会导致recursion调用很多小函数,只是在recursion结束时获取一个值。 (想象一下,获取具有数十个对象的元组中的特定types的第一个条目的值)。启用内联后,优化器可以直接访问该值(记住,该值可能在寄存器中), 折叠几十个函数调用访问CPU寄存器中的单个值。 这可以把一个可怕的表演猪变成一个不错的和快速的程序。


隐藏状态作为对象中的私有数据(封装)有其成本。 内联从一开始就是C ++的一部分,目的是为了最大限度地减less这些抽象成本 。 那时候,编译器在检测出内联(而不是坏内容)的候选方面比现在要严重得多,因此手动内联导致了相当大的速度增加。
现在的编译器被称为比我们内联更聪明。 编译器能够自动内联函数,或者不内联标记为inline联函数的用户,尽pipe他们可以。 有人说内联应该完全留给编译器,我们甚至不应该把inline函数标记为inline函数。 但是,我还没有看到一个全面的研究,表明是否手动这样做仍然是值得的。 所以暂时我会一直这样做,如果它认为它可以做的更好,让编译器重写。

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

等于

 int a = b + c 

没有跳跃 – 没有开销

考虑一个简单的function,如:

 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%是函数开销。

对于一个这样的微不足道的函数来说,函数开销代码和主函数体本身一样运行的可能性很大。 当你在深循环体内调用了几百万/十亿次的函数调用时,函数调用开销就会变大。

与往常一样,关键是分析/测量,以确定是否内联某个特定的function可以获得任何净性能收益。 对于更多的“复杂”function,这些“复杂”function并不经常被称为“内部增益”,可能是无法估量的。

内联的速度有很多原因,其中只有一个是显而易见的:

  • 没有跳转指示。
  • 更好的本地化,导致更好的caching利用率。
  • 编译器的优化器有更多机会进行优化,例如在寄存器中保留值。

caching利用率也可以对付你 – 如果内联使得代码变大,caching未命中的可能性就会增加。 这是一个不太可能的情况下,虽然。

一个典型的例子就是std :: sort,它的比较函数是O(N log N)。

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

顺便说一句,这里的C ++比q中的qsort要快,它需要一个函数指针。

跳转的另外一个潜在的副作用是,您可能会触发页面错误,或者是第一次将代码加载到内存中,或者是不经常使用,以致稍后导致页面内存不足。

安德烈的回答已经给了你一个非常全面的解释。 但是,为了补充一点,他错过了,内联对于很短的函数也是非常有价值的。

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

重要的不是真正的绝对的函数调用的代价(可能只需要5个时钟周期或类似的东西),而是相对于函数被调用的频率需要多长时间。 如果function如此短以至于每10个时钟周期就可以调用一次,那么每次调用“不必要”的按压/popup指令花费5个周期是非常糟糕的。

(值得内联的膨胀)

内联并不总是导致更大的代码。 例如一个简单的数据访问函数,例如:

 int getData() { return data ; } 

将导致作为函数调用的指令周期比作为在线的更多,并且这样的函数最适合于内联。

如果函数体包含大量的代码,那么函数调用的开销确实是微不足道的,如果从多个位置调用它,可能确实会导致代码膨胀 – 尽pipe编译器可能简单地忽略内联指令在这种情况下。

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

当然你可以把它留给你的编译器来决定。 我只有明确的内联函数,包含一个不涉及进一步函数调用的单一语句,这更多的是为了类方法的开发速度而不是性能。

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

内联函数是编译器用定义replace函数调用的build议。 如果它被replace,那么将不会有函数调用栈操作[push,pop]。 但它并不总是保证。 🙂

– 干杯

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

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

内联当一个函数被多次调用时会产生很大的不同。

因为不执行跳转。