C ++链接器会自动内联函数(没有“inline”关键字,而不在头文件中实现)?

时间:2011-08-28 21:15:43

标签: c++ optimization linker inline compiler-optimization

C ++链接器是否会自动内联“传递”函数,这些函数未在标题中定义,并且未明确要求通过inline关键字“内联”? < / p>

例如,以下发生经常,并且总是受益于“内联”,似乎每个编译器供应商都应该“自动”处理它“通过链接器(在可能的情况下)内联:

//FILE: MyA.hpp
class MyA
{
  public:
    int foo(void) const;
};

//FILE: MyB.hpp
class MyB
{
  private:
    MyA my_a_;
  public:
    int foo(void) const;
};

//FILE: MyB.cpp
// PLEASE SAY THIS FUNCTION IS "INLINED" BY THE LINKER, EVEN THOUGH
// IT WAS NOT IMPLICITLY/EXPLICITLY REQUESTED TO BE "INLINED"?
int MyB::foo(void)
{
  return my_a_.foo();
}

我知道MSVS链接器将通过其链接时间代码生成(LTGCC)执行一些“内联”,并且GCC工具链还支持链接时间优化(LTO)< / em>(见:Can the linker inline functions?)。

此外,我知道有些情况下无法被“内联”,例如当实现对链接器“不可用”时(例如,跨共享库边界,其中发生单独的链接。)

但是,如果这是代码链接到不跨越DLL / shared-lib边界的可执行文件,我期望编译器/链接器供应商自动内联函数,作为一种简单明了的优化(有利于性能和尺寸)?

我的希望太天真了吗?

8 个答案:

答案 0 :(得分:18)

以下是对您的示例的快速测试(使用MyA::foo()实现只返回42)。所有这些测试都是使用32位目标 - 64位目标可能会出现不同的结果。值得注意的是,使用-flto选项(GCC)或/GL选项(MSVC)可以实现全面优化 - 只要调用MyB::foo(),就会将其替换为42

使用GCC(MinGW 4.5.1):

gcc -g -O3 -o test.exe myb.cpp mya.cpp test.cpp

对MyB :: foo()的调用没有被优化掉。 MyB::foo()本身略微优化为:

Dump of assembler code for function MyB::foo() const:
   0x00401350 <+0>:     push   %ebp
   0x00401351 <+1>:     mov    %esp,%ebp
   0x00401353 <+3>:     sub    $0x8,%esp
=> 0x00401356 <+6>:     leave
   0x00401357 <+7>:     jmp    0x401360 <MyA::foo() const>

哪个入口序言留在原位,但是立即撤消(leave指令)并且代码跳转到MyA :: foo()来完成实际工作。但是,这是编译器(而不是链接器)正在进行的优化,因为它意识到MyB::foo()只返回MyA::foo()返回的内容。我不确定为什么要留下序幕。

MSVC 16(来自VS 2010)处理的事情略有不同:

MyB::foo()结束了两次跳跃 - 一次到某种'thunk':

0:000> u myb!MyB::foo
myb!MyB::foo:
001a1030 e9d0ffffff      jmp     myb!ILT+0(?fooMyAQBEHXZ) (001a1005)

thunk只是跳到了MyA::foo()

myb!ILT+0(?fooMyAQBEHXZ):
001a1005 e936000000      jmp     myb!MyA::foo (001a1040)

再次 - 这在很大程度上(完全是?)由编译器执行,因为如果你查看链接之前生成的目标代码,MyB::foo()被编译为普通跳转到MyA::foo()

所以要把所有这些都搞砸了 - 看起来没有明确调用LTO / LTCG,今天的链接器不愿意/无法执行完全删除对MyB::foo()的调用的优化,即使MyB::foo()是简单地跳转到MyA::foo()

所以我想如果你想要链接时间优化,请使用-flto(对于GCC)或/GL(对于MSVC编译器)和/LTCG(对于MSVC链接器)选项。

答案 1 :(得分:11)

这是常见的吗?是的,对于主流编译器而言。

是自动的吗?一般不是。 MSVC需要/GL开关,gcc和clang -flto标志。

它是如何工作的?(仅限gcc)

gcc工具链中使用的传统链接器是ld,它有点愚蠢。因此,令人惊讶的是,gcc工具链中的链接器不会执行链接时优化。

Gcc有一个特定的中间表示,在其上执行与语言无关的优化: GIMPLE 。使用-flto(激活LTO)编译源文件时,它会将中间表示保存在目标文件的特定部分中。

当使用-flto调用链接器驱动程序(注意:不是直接链接)时,驱动程序将读取这些特定部分,将它们捆绑在一起形成一个大块,并提供此捆绑包到编译器。编译器重新应用优化,因为它通常用于常规编译(常量传播,内联,这可能会为死代码消除,循环转换等提供新的机会......)并生成一个大的目标文件。

这个大对象文件最终会被提供给工具链的常规链接器(可能是ld,除非你正在尝试使用黄金),这会实现其链接器魔法。

Clang的工作方式类似,我推测MSVC使用了类似的技巧。

答案 2 :(得分:8)

这取决于。大多数编译器(链接器,真的)支持这种优化。但是为了完成它,整个代码生成阶段几乎必须推迟到链接时。 MSVC调用选项链接时代码生成(LTCG),默认情况下在发布版本IIRC中启用。

GCC有一个类似的选项,名称不同,但我不记得哪个-O级别(如果有的话)启用它,或者是否必须明确启用它。

然而,“传统上”,C ++编译器已经单独编译了一个单独的翻译单元 ,之后链接器只是绑定了松散的末端,确保当翻译单元A调用一个定义的函数时翻译单元B,查找正确的函数地址并将其插入到调用代码中。

如果您遵循此模型,则无法内联另一个翻译单元中定义的函数。

不仅可以“动态”完成一些“简单”优化,例如循环展开。它需要链接器和编译器协作,因为链接器必须接管编译器通常完成的一些工作。

请注意,编译器 很乐意内联未标有inline关键字的函数。但只有当它知道如何在调用函数的站点定义函数时。如果它看不到定义,则无法内联调用。这就是为什么你通常在标题中定义这样一些小的“有意内联”函数,使所有调用者都可以看到它们的定义。

答案 3 :(得分:5)

内联不是链接器函数。

支持整个程序优化(跨TU内联)的工具链通过在编译时不实际编译任何东西,只是解析和存储代码的中间表示来实现。然后链接器调用编译器,它执行实际的内联。

默认情况下不会这样做,您必须使用编译器和链接器的相应命令行选项显式请求它。

它不是也不应该是默认的一个原因是它会大大增加基于依赖性的重建时间(有时会增加几个数量级,具体取决于代码组织)。

答案 4 :(得分:0)

是的,如果您设置了正确的优化标志并且编译器认为它是性能加值,那么任何体面的编译器都完全能够内联该函数。

如果您真的想知道,请在调用函数之前添加断点,编译程序,然后查看程序集。如果你那样做会很清楚。

答案 5 :(得分:0)

编译代码必须能够查看函数的内容以获得内联的机会。通过使用统一文件和LTCG可以完成更多这种情况发生的可能性。

答案 6 :(得分:0)

inline关键字仅作为编译器在进行优化时内联函数的指导。在g ++中,优化级别-O2和-O3生成不同级别的内联。 g ++ doc指定以下内容:(i)如果指定了O2 -finline-small-functions打开。(ii)如果指定了O3,则-finline-functions与O2的所有选项一起打开。 (iii)然后还有一个相关的选项“no-default-inline”,只有在添加“inline”关键字时才会使成员函数内联。

通常,函数的大小(程序集中的指令数),如果使用递归调用,则确定是否发生内联。 g ++中的链接中定义了更多选项:

http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

请查看您正在使用的选项,因为您最终使用的选项决定了您的功能是否内联。

答案 7 :(得分:-1)

以下是我对编译器对函数的作用的理解:

如果函数定义在类定义中,并且假设没有阻止“内联”函数(例如递归)的场景,则函数将是“inline -d”。

如果函数定义在类定义之外,则函数将不是“inline -d”,除非函数定义明确包含inline关键字。

以下摘录自Ivor Horton的Beginning Visual C ++ 2010:

内联函数

使用内联函数,编译器会尝试扩展函数体中的代码,而不是调用函数。这避免了调用函数的大量开销,因此加快了代码的速度。

编译器可能无法始终为内联函数插入代码(例如使用递归函数或已获取地址的函数),但通常情况下,它可以正常工作。它最适合用于非常简短的函数,例如我们在CBox类中的Volume(),因为这些函数执行得更快,插入正文代码不会显着增加可执行模块的大小。

对于类定义之外的函数定义,编译器将函数视为普通函数,函数调用将以通常的方式工作;但是,也可以告诉编译器,如果可能,您希望将该函数视为内联函数。这可以通过简单地将关键字内联放在函数头的开头来完成。因此,对于此函数,定义如下:

inline double CBox::Volume()
{
    return l * w * h;
}