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

43

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

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

如果我遗漏了什么,请让我知道,谢谢!


6
根据具体实现,不能保证堆栈一定存在,因此函数调用时发生的具体情况可能会因平台而异。至于开销是否显著... 这取决于你在做什么。 - wrosecrans
并不总是有骑手在那里。http://www.parashift.com/c++-faq-lite/inline-functions.html#faq-9.3 - DumbCoder
5
不需要担心这个问题。编译器会分析代码并决定是否应该内联函数(它会忽略你提供的任何提示)。由于编译器更了解函数的调用方式,它可以做出有根据的成本效益分析,并基于具体情况决定是否值得内联代码。无需进行用户交互(也不应该试图击败编译器)。 - Martin York
9
@Martin York:他不是在询问是否应该将函数内联,而是在问为什么内联很重要。因此,理解这一点是完全有效的。 - Chris Pitman
2
@ Michael Foukarakis:没有人关闭了“内联特性”。编译器不再使用“inline关键字”来确定是否内联代码。因此,编译器肯定会忽略你(除非你强制它做出相反的决定)。 - Martin York
显示剩余8条评论
16个回答

65

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

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

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

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

foo(true);
...
foo(false);
然而,如果上述调用被内联,编译器将立即能够消除分支。实质上,在以上情况下,内联允许编译器将函数参数解释为编译时常量(如果参数是编译时常量),这在非内联函数中通常是不可能的。但它并不仅限于此。总体而言,内联所启用的优化机会更加深远。另一个例子是,当函数体内联到调用方的特定上下文中时,编译器通常能够将调用代码中存在的已知别名相关关系传播到内联函数代码中,从而使得更好地优化函数的代码成为可能。再次强调,可能的例子有很多,所有这些都源自基本事实,即内联调用被融合到特定调用方的上下文中,因此使得各种跨上下文的优化成为可能,这是非内联调用所不能做到的。通过内联,您基本上获得了原始函数的许多个独立版本,每个版本都为每个特定的调用方上下文进行了个性化和优化。这样做的代价显然是代码膨胀的潜在危险,但如果使用正确,就可以提供显著的性能优势。

7
内联提供给你的另一个优化是指令缓存效率,内联代码更有可能已经在缓存中,而调用代码则很容易导致缓存未命中。 - Detmar
2
@Detmar, @sbi:我们同意这可能是一个神秘的问题。使用内联函数可以将热代码推出L1指令缓存区,而使用函数调用则意味着每个函数都能独立地在缓存中,占用更少的缓存空间。这就是为什么使用-GCC编译的代码,使用-Os(减小大小)选项会比O2或O3的选项更快,这似乎与直觉相反。 - Zan Lynx
1
是的!!!函数体被内联了,...突然编译器可以消除大部分代码。这是为什么内联(特别是与链接时代码生成一起使用)是非常好的原因之一。 - sharptooth
@AnT:什么是“已知别名相关关系”?你指的是什么? - Destructor
1
@meet:我的意思是,例如在函数void foo(int *a, int *b)内部,编译器无法对别名进行任何假设:ab可以指向同一对象或不同的对象。这两种情况都提供了优化机会,但编译器无法利用这些机会。但在更高层次(在调用者上下文中),可能会有这些信息。例如,在内联int x; foo(&x, &x);调用时,编译器可以立即针对a == b条件进行优化。同样地,在int x, y; foo(&x, &y);中,编译器可以优化为a != b - AnT stands with Russia
显示剩余6条评论

27
“只需几次推动和跳跃就能调用函数,真的有那么多开销吗?” 这取决于函数本身。 如果函数的主体只是一个机器码指令,调用和返回的开销可能会非常高。比如说,6倍,500%的开销。那么,如果你的程序仅由大量对该函数的调用组成,并且没有内联,则会将运行时间增加500%。 然而,在另一方面,内联化可能会产生不利影响,例如因为没有内联化的代码可以适合在一页内存中。 因此,优化时的答案总是先测量。

11
此外,一个非常短小的函数可能比调用该函数时的设置和拆除指令还要更加紧凑,内联实际上可能会使代码更小。进行度量和分析。 - David Thornley

12

没有调用和堆栈活动,这肯定可以节省几个CPU周期。在现代CPU中,代码本地性也很重要:进行调用可能会清除指令流水线并强制CPU等待内存被获取。在紧密的循环中,这非常重要,因为主存储器比现代CPU要慢得多。

然而,如果您的代码只在应用程序中被调用了几次,那么不用担心内联。但是,如果它被调用了数百万次,而用户正在等待答案,请一定要重视内联!


11

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

启用内联后,这只是从内存中获取变量,对于任何架构而言都只是单个指令。"少量推送和跳转"(加上返回)很容易会多次执行

此外,被优化器视野范围内的代码越多,它的工作就能做得越好。使用大量内联,它可以同时看到更多的代码。这意味着它可能能够将值保留在CPU寄存器中,并完全避免昂贵的内存读取。现在我们谈论的可能是几个数量级的差异

然后,还有模板元编程。有时,这会导致递归调用许多小函数,只为获取递归结尾的单个值。(想想在包含数十个对象的元组中获取特定类型第一个条目的值。)启用内联后,优化器可以直接访问该值(记住,可能在寄存器中),将折叠数十个函数调用为一次访问存储在CPU寄存器中的单个值。这可以将可怕的性能瓶颈转换为良好且快速的程序。


在对象中使用私有数据来隐藏状态(封装)是有成本的。从一开始,内联就是C++的一部分,以最小化这些抽象成本。当时,编译器在检测内联的好候选项(并拒绝坏的候选项)方面比现在差得多,因此手动内联会带来相当大的速度增益。
现今编译器在inline方面的表现被认为比我们聪明得多。编译器能够自动地inline函数,或者不将用户标记为inline的函数进行inline,尽管它们本来可以这样做。有人认为完全应该将inline留给编译器,而我们甚至不需要再去标记函数为inline。然而,我还没有看到一项全面的研究表明手动标记是否仍然值得。所以就目前而言,我会继续自己来操作,并在编译器认为它可以做得更好时覆盖我的操作。


我真的很喜欢这个例子。我之前没有想过模板中的访问器和递归函数。非常感谢! - kodai
您在分隔符下方的第一句话中拼错了“costs”。 - Core Xii

5

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

等于

int a = b + c

没有跳跃 - 没有额外开销

更好的做法是:"int a = sum(4,5)" 可以变成 "int a = 9"。此外,通过引用读写变量通常比直接读写变量慢;在许多情况下,可以解析内联函数以使用更快的直接变量访问(请注意,在您的场景中,如果不是内联,则最好按值传递变量而不是按引用传递,但如果函数执行类似于"a+=b;"的操作,则必须使用引用)。 - supercat
“通过引用读写变量通常比直接读写变量慢”这种说法太笼统了,不一定正确。在大多数正常情况下,我认为这种说法高度不可信(看看过度概括是多么容易)。 - Martin York
我们有多频繁地编写像 sum() 这样的函数呢?我认为访问器是更相关的示例,以说明内联的作用。 - sbi

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%的代码都是函数开销。
对于像这样的简单函数,函数开销代码执行时间和主函数体本身一样长的可能性很高。当你有在深层循环体中被调用数百万/十亿次的简单函数时,函数调用开销就开始变大了。
因此,在特定情况下内联一个函数是否产生任何净性能增益,关键在于分析/测量。对于更“复杂”的函数,如果不经常调用,则从内联中获得的收益可能微不足道。

6
这是一个调试版本,其中存在内存保护和超大的堆栈框架以允许“编辑并继续”。您不能使用调试代码来分析优化! - Skizz

4

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

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

缓存利用也可能对您不利 - 如果内联使代码变得更大,则可能出现更多的缓存未命中。但这种情况不太可能发生。


3

一个典型的例子是在std::sort中,比较函数的时间复杂度为O(N log N),这时优化会有很大的影响。

尝试创建一个大规模的向量,并先使用内联函数和非内联函数分别调用std::sort,然后测量性能。

顺便说一下,在C++中,这也是sort比C语言中的qsort更快的原因之一,因为qsort需要使用函数指针。


2

(并且值得内联,即使会增加代码量)

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

int getData()
{
   return data ;
}

函数调用将会导致更多的指令周期,而内联函数则更适合这种情况。如果函数体包含大量代码,则函数调用开销确实不重要,如果它从多个位置调用,则可能会导致代码膨胀 - 尽管在这种情况下,编译器很可能会忽略内联指令。

您还应考虑调用频率; 即使对于较大的代码体,如果从一个位置频繁调用该函数,则在某些情况下节省时间可能是值得的。这取决于调用开销与代码体积的比率以及使用频率。

当然,您可以让编译器来决定。我只会显式地内联包含单个语句且不涉及其他函数调用的函数,这更多是为了加快类方法的开发速度而不是为了性能。


2

跳转的另一个潜在副作用是可能会触发页面错误,要么是第一次将代码加载到内存中,要么是如果它被不经常使用而被分页出内存。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接