41

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

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

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

4

16 回答 16

63

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

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

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

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

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

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

然而,它甚至不限于此。一般来说,内联启用的优化机会要深远得多。再例如,当函数体被内联到特定调用者的上下文中时,编译器通常能够将调用代码中存在的已知别名相关关系传播到内联函数代码中,从而可以优化函数的代码更好。

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

于 2010-10-25T15:34:36.250 回答
27

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

这取决于功能。

如果函数体只是一条机器代码指令,则调用和返回开销可能高达数百%。比如说,6 次,500% 的开销。然后,如果您的程序只包含对该函数的大量调用,而没有内联,则您的运行时间增加了 500%。

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

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

于 2010-10-25T15:37:31.810 回答
12

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

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

于 2010-10-25T15:28:36.670 回答
11

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

启用内联后,这只是从内存中获取变量,可能是任何架构上的一条指令。“几次推动和跳跃”(加上返回)很容易成为数倍

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

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


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

于 2010-10-25T17:53:45.370 回答
5

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

等于

int a = b + c

没有跳跃 - 没有开销

于 2010-10-25T15:29:16.033 回答
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,调试):

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

对于像这样的微不足道的函数,函数开销代码很可能与主函数体本身的运行时间一样长。当您在深度循环体中调用数百万/数十亿次的琐碎函数时,函数调用开销开始变大。

与往常一样,关键是分析/测量以确定内联特定函数是否会产生任何净性能增益。对于不被“经常”调用的更“复杂”的函数,内联的收益可能非常小。

于 2010-10-25T15:41:51.053 回答
4

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

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

缓存利用率也可能对您不利 - 如果内联使代码变大,则缓存未命中的可能性更大。不过,这种情况的可能性要小得多。

于 2010-10-25T15:36:20.757 回答
3

它产生重大差异的一个典型示例是 std::sort ,它的比较函数为 O(N log N)。

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

顺便说一句,这就是 C++ 中的 sort 比 C 中的 qsort 更快的地方,后者需要一个函数指针。

于 2010-10-25T15:45:36.410 回答
2

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

于 2010-10-25T15:28:08.633 回答
2

(值得让它内联的臃肿)

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

int getData()
{
   return data ;
}

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

如果函数体包含大量代码,则函数调用开销确实微不足道,并且如果从多个位置调用它,则确实可能导致代码膨胀-尽管您的编译器很可能会简单地忽略 inline 指令在这种情况下。

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

当然,你可以把它留给你的编译器来决定。我只显式地内联函数,这些函数由一个不涉及进一步函数调用的语句组成,这更多的是为了类方法的开发速度而不是性能。

于 2010-10-25T18:17:49.727 回答
2

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

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

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

于 2010-10-26T11:16:16.137 回答
1

因为没有电话。功能码就是复制过来的

于 2010-10-25T15:24:55.417 回答
1

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

- 干杯

于 2010-10-25T15:37:28.070 回答
1

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

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

于 2010-10-25T15:44:38.523 回答
0

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

于 2010-10-25T15:27:40.007 回答
-1

因为没有执行跳转。

于 2010-10-25T15:25:42.287 回答