现在,我知道这是因为没有调用函数的开销,但是调用函数的开销真的很大吗(并且值得内联的膨胀)?
据我所记,当一个函数被调用时,比如 f(x,y),x 和 y 被压入栈中,栈指针跳转到一个空块,并开始执行。我知道这有点过于简单化了,但是我是否忽略了什么?几次推送和跳转来调用一个函数,真的有那么大的开销吗?
如果我遗漏了什么,请让我知道,谢谢!
现在,我知道这是因为没有调用函数的开销,但是调用函数的开销真的很大吗(并且值得内联的膨胀)?
据我所记,当一个函数被调用时,比如 f(x,y),x 和 y 被压入栈中,栈指针跳转到一个空块,并开始执行。我知道这有点过于简单化了,但是我是否忽略了什么?几次推送和跳转来调用一个函数,真的有那么大的开销吗?
如果我遗漏了什么,请让我知道,谢谢!
除了没有调用(因此没有相关的费用,如调用前的参数准备和调用后的清理),内联的另一个显着优点是函数体可以在调用方的特定上下文中重新解释。这可能立即允许编译器进一步减少和优化代码。
举个简单的例子,这个函数:
void foo(bool b) {
if (b) {
// something
}
else {
// something else
}
}
如果被调用为非内联函数,将需要实际分支。
foo(true);
...
foo(false);
然而,如果上述调用被内联,编译器将立即能够消除分支。实质上,在以上情况下,内联允许编译器将函数参数解释为编译时常量(如果参数是编译时常量),这在非内联函数中通常是不可能的。但它并不仅限于此。总体而言,内联所启用的优化机会更加深远。另一个例子是,当函数体内联到调用方的特定上下文中时,编译器通常能够将调用代码中存在的已知别名相关关系传播到内联函数代码中,从而使得更好地优化函数的代码成为可能。再次强调,可能的例子有很多,所有这些都源自基本事实,即内联调用被融合到特定调用方的上下文中,因此使得各种跨上下文的优化成为可能,这是非内联调用所不能做到的。通过内联,您基本上获得了原始函数的许多个独立版本,每个版本都为每个特定的调用方上下文进行了个性化和优化。这样做的代价显然是代码膨胀的潜在危险,但如果使用正确,就可以提供显著的性能优势。void foo(int *a, int *b)
内部,编译器无法对别名进行任何假设:a
和b
可以指向同一对象或不同的对象。这两种情况都提供了优化机会,但编译器无法利用这些机会。但在更高层次(在调用者上下文中),可能会有这些信息。例如,在内联int x; foo(&x, &x);
调用时,编译器可以立即针对a == b
条件进行优化。同样地,在int x, y; foo(&x, &y);
中,编译器可以优化为a != b
。 - AnT stands with Russia没有调用和堆栈活动,这肯定可以节省几个CPU周期。在现代CPU中,代码本地性也很重要:进行调用可能会清除指令流水线并强制CPU等待内存被获取。在紧密的循环中,这非常重要,因为主存储器比现代CPU要慢得多。
然而,如果您的代码只在应用程序中被调用了几次,那么不用担心内联。但是,如果它被调用了数百万次,而用户正在等待答案,请一定要重视内联!
经典的内联候选项是访问器,例如 std::vector<T>::size()
。
启用内联后,这只是从内存中获取变量,对于任何架构而言都只是单个指令。"少量推送和跳转"(加上返回)很容易会多次执行。
此外,被优化器视野范围内的代码越多,它的工作就能做得越好。使用大量内联,它可以同时看到更多的代码。这意味着它可能能够将值保留在CPU寄存器中,并完全避免昂贵的内存读取。现在我们谈论的可能是几个数量级的差异。
然后,还有模板元编程。有时,这会导致递归调用许多小函数,只为获取递归结尾的单个值。(想想在包含数十个对象的元组中获取特定类型第一个条目的值。)启用内联后,优化器可以直接访问该值(记住,可能在寄存器中),将折叠数十个函数调用为一次访问存储在CPU寄存器中的单个值。这可以将可怕的性能瓶颈转换为良好且快速的程序。
在对象中使用私有数据来隐藏状态(封装)是有成本的。从一开始,内联就是C++的一部分,以最小化这些抽象成本。当时,编译器在检测内联的好候选项(并拒绝坏的候选项)方面比现在差得多,因此手动内联会带来相当大的速度增益。
现今编译器在inline方面的表现被认为比我们聪明得多。编译器能够自动地inline函数,或者不将用户标记为inline
的函数进行inline,尽管它们本来可以这样做。有人认为完全应该将inline留给编译器,而我们甚至不需要再去标记函数为inline
。然而,我还没有看到一项全面的研究表明手动标记是否仍然值得。所以就目前而言,我会继续自己来操作,并在编译器认为它可以做得更好时覆盖我的操作。
让
int sum(const int &a,const int &b)
{
return a + b;
}
int a = sum(b,c);
等于
int a = b + c
sum()
这样的函数呢?我认为访问器是更相关的示例,以说明内联的作用。 - sbiint SimpleFunc (const int X, const int Y)
{
return (X + 3 * Y);
}
int main(int argc, char* argv[])
{
int Test = SimpleFunc(11, 12);
return 0;
}
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
内联速度更快的原因有多个,其中一个是显而易见的:
缓存利用也可能对您不利 - 如果内联使代码变得更大,则可能出现更多的缓存未命中。但这种情况不太可能发生。
一个典型的例子是在std::sort中,比较函数的时间复杂度为O(N log N),这时优化会有很大的影响。
尝试创建一个大规模的向量,并先使用内联函数和非内联函数分别调用std::sort,然后测量性能。
顺便说一下,在C++中,这也是sort比C语言中的qsort更快的原因之一,因为qsort需要使用函数指针。
(并且值得内联,即使会增加代码量)
并不总是内联导致代码变大。例如,一个简单的数据访问函数:
int getData()
{
return data ;
}
函数调用将会导致更多的指令周期,而内联函数则更适合这种情况。如果函数体包含大量代码,则函数调用开销确实不重要,如果它从多个位置调用,则可能会导致代码膨胀 - 尽管在这种情况下,编译器很可能会忽略内联指令。
您还应考虑调用频率; 即使对于较大的代码体,如果从一个位置频繁调用该函数,则在某些情况下节省时间可能是值得的。这取决于调用开销与代码体积的比率以及使用频率。
当然,您可以让编译器来决定。我只会显式地内联包含单个语句且不涉及其他函数调用的函数,这更多是为了加快类方法的开发速度而不是为了性能。
跳转的另一个潜在副作用是可能会触发页面错误,要么是第一次将代码加载到内存中,要么是如果它被不经常使用而被分页出内存。