C++的`inline`关键字与编译器优化

5
我听说 inline 关键字在现代编译器中已经不再有用,但是在多源项目中仍然可以用来避免多次定义的错误。
但今天我遇到一个例子证明编译器还是会遵循这个关键字。
如果没有 inline 关键字,下面的代码将无法被编译:
#include <iostream>

using namespace std;

void func(const int x){
    if(x > 3)    
        cout << "HAHA\n";
    else
        cout << "KKK\n";
}

int main(){
    func(5);
}

用命令 g++ -O3 -S a.cpp,可以生成汇编代码,但是 func 没有内联。
如果在 func 定义前面添加内联关键字,func 将被内联到 main 中。
生成的汇编代码的部分如下:
.LC0:
    .string "HAHA\n"
.LC1:
.string "KKK\n"
.text
.p2align 4,,15
.globl  _Z4funci
.type   _Z4funci, @function
_Z4funci:
.LFB975:
    .cfi_startproc
    cmpl    $3, %edi
    jg  .L6
    movl    $4, %edx
    movl    $.LC1, %esi
    movl    $_ZSt4cout, %edi
    jmp _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
    .p2align 4,,10
    .p2align 3

main:
.LFB976:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $5, %edi
    call    _Z4funci
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

我的编译器是gcc 4.8.1 / x86-64。

我怀疑该函数在链接过程中可以进行内联,但我不确定是否会发生这种情况,如果是的话,我该如何知道?

我的问题是,为什么这段代码片段似乎与现代指南相矛盾,例如何时应为函数/方法编写关键字“inline”?


4
你已经表明了 inline 可能会影响内联的决策,但并没有证明它在任何方面都更好。 - user395760
3
它可以与static声明或者-flto标志相一致。 - zch
2
你引用的问题中最高评分的答案是完全错误的。请忽略它。 - James Kanze
3
编译器无法确定您是否可能希望在其他地方使用该函数,因为您只编译了一个单独的、未链接的翻译单元。编译整个程序(例如使用-fwhole-program-flto)可以改变这种情况,同时给函数添加内部链接(匿名命名空间)也可以。在原始情况下,一旦编译器生成了外部函数体,它认为调用该函数比在主函数中复制代码更加方便。 - Kerrek SB
2
@JamesKanze:当已经有一个函数定义时,如何避免重复? - Kerrek SB
显示剩余8条评论
4个回答

4
inline关键字有几个作用。其中之一是提示编译器你想要函数被内联 - 然而,这并不意味着编译器必须将其内联[一些编译器有一个扩展,无论如何都会内联,例如MS的__forceinline和gcc的__attribute__(always_inline)]。 inline关键字还允许您拥有具有相同名称的多个函数实例,如果该函数被内联,则不会因“同一函数的多个定义”而出现错误。[但每次函数必须是相同源]。
在这种情况下,我有点惊讶地看到编译器没有内联func。然而,将static添加到func也可以使其内联。因此,显然编译器基于“其他某个函数可能也在使用func,因此我们仍需要一个副本,并且从内联中获得的收益不大。事实上,如果将函数静态化,并且仅调用一次,即使函数非常大,gcc/g++几乎肯定会将其内联。
如果您希望编译器内联某些内容,添加inline永远不会有害。但是,在许多情况下,编译器无论哪种方式都会做出不错的选择。例如,如果我将代码更改为:
const char* func(const int x){
    if(x > 3)    
        return "HAHA\n";
    else
        return "KKK\n";
}

int main(){
    cout << func(5);
}

它会内联位于func左边的return "HAHA\n";部分。

编译器决定是否内联的逻辑是复杂的,其中一部分是“我们获得了多少好处,与它占用了多少代码空间相比” - 在这种情况下,调用operator<<(ostream& ,const char *)的开销可能太大而无法内联。不幸的是,理解编译器为什么做出某个决定并不总是容易的...


2
首先,情况并非非黑即白。 inline 关键字的唯一绝对效果是抑制ODR规则并避免多重定义错误。除此之外,编译器肯定可以将该关键字视为有关内联的提示,但它可能会或可能不会这样做。(就我所见,在实践中,大多数人不知道何时以及何时应该执行内联操作,而编译器可以更好地执行此操作)。但它不一定 必须 忽略提示。
第二,调用使用inline关键字进行内联可能还有另一个原因,而在没有使用关键字时则不然。
没有使用inline关键字,函数定义必须被导出,因为另一个TU可能需要链接到它。而且由于我们必须导出函数定义,代码已经存在,内联调用只意味着您有效地复制了函数体。总代码更多,可执行文件大小更大,指令缓存局部性受损。
但是使用inline关键字,编译器不必导出函数定义,因此它可以内联调用并完全删除原始定义。然后总代码大小就不会增加(我们只需将函数体移动到调用现场,而不是生成函数定义和对其进行调用)。
作为一个实验,尝试将函数标记为static而不是inline。这也意味着编译器不必导出定义,并且很有可能会决定内联是值得的。

我不确定你所说的“编译器通常会忽略这个优化提示”的意思。从质量角度来看,只有在编译器可以比程序员做得更好时,它才会忽略它,而这并不是大多数常见编译器的情况。正如问题所指出的,g++也没有忽略它;从我所看到的情况来看,VC++也没有。 - James Kanze
关于“大多数人不知道何时使用内联”的说法:我希望在出现性能问题之前,没有人会使用内联。真正的线索来自程序的实际性能。(否则,这是过早优化。) - James Kanze
关于您的第三段:根据我的经验,我为了性能而声明为内联的大多数函数都在未命名的命名空间中。您应该避免在头文件中使用内联,因为它会破坏封装性并引入编译器依赖性。 - James Kanze

1

现在(2018年),即使在现代编译器中,inline 属性仍然用于优化。

声称编译器会忽略它,而纯粹依靠自己的成本模型是不正确的,至少在开源编译器 GCC 和 Clang 中是如此。Simon Brand 写了一篇很好的博客文章(编译器将 inline 视为提示吗?),他通过查看编译器的源代码来揭穿这个谎言。

但这并不意味着这些编译器会盲目地遵循程序员的提示。如果他们有足够的证据表明它会影响性能,他们会覆盖你的提示。

有一些特定于供应商的扩展,它们将强制进行内联,即使编译器认为这是一个坏主意。例如,在 Visual Studio 中,它被称为__forceinline

__forceinline关键字覆盖了成本/效益分析,并依靠程序员的判断。在使用__forceinline时要小心。不加区分地使用__forceinline可能会导致更大的代码,而只有微不足道的性能提升,或者在某些情况下甚至会出现性能损失(例如,由于更大的可执行文件而增加页面交换)。

GCC和Clang将其称为inline __attribute__((__always_inline__))

总的来说,建议信任编译器的决策,特别是如果您可以使用基于剖面的优化。一个值得注意的例外是高质量代码库Boost,它使用强制内联(查找BOOST_FORCEINLINE)。


0

你一直听到的是错误的,或者应该是错误的。标准清楚地指定了inline意图:告诉编译器最好能够生成这段代码的内联形式。在编译器能够比程序员更好地判断何时需要内联之前,它应该考虑这个“提示”。也许有一天,inline会变得无关紧要(就像register一样),但我们还远远没有达到那个地步。

话虽如此,我很惊讶g++在你的情况下没有内联。即使函数没有标记为inline,g++通常也会相当积极地进行内联。也许它只是觉得既然这个函数不在循环中,那么内联就不值得麻烦了。


2
编译器通常更擅长决定何时进行内联。这并不意味着没有人类专家比它做得更好,但寄存器分配也是如此 - 只需问问Mike Pall。而且总会有特定于编译器的“永不内联”和“如果可以以任何方式内联则始终内联”的属性。 - user395760
1
你能展示一个使用 inline 优化提示会导致编译器内联一些本来不会被内联的代码的情况吗?我不知道有这样的情况(正如我在回答和评论中提到的,static 通常也会导致编译器内联,因为重要的不是提示,而是它可以避免为函数定义发出代码。仅仅是 提示 并不能在我看过的任何情况下产生影响。如果您知道这样的情况,我很乐意看到。 - jalf
@delnan,那是完全错误的。如果编译器基于分析器输出进行整个程序优化,它们可以接近实现;然而,这不是通常情况。而且,无论是g++还是VC++都不会忽略“inline”声明。内联不像“register”。 - James Kanze
我同意 inline 不像 register,但出于其他原因(语义上讲,一个只是限制了你的行为,而另一个允许你编写更适合内联的代码)。是的,它并不完全被忽略。但正如我在问题的第一条评论中所写的那样,"你可以用它来强制内联"和"使用它来强制内联是有用的"之间存在着差异。 - user395760
@JamesKanze,我在之前的评论中已经指出,所提出的问题并不是这种情况。如果编译器确定没有任何副作用,它将内联许多情况。这些情况包括“标记为inline的函数只被调用一次”。通过内联,您不会增加整个可执行文件的大小(可以消除原始定义,并仅保留内联版本),并且您可以获得更好的指令缓存局部性并节省一些跳转。无论是否给出任何提示,这都是一个净胜利。 - jalf
显示剩余8条评论

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