为什么链接器优化如此糟糕?

16

最近,我的一个同事指出,将所有内容编译成单个文件比编译单独的对象文件更有效 - 即使启用了链接时间优化。此外,项目的总编译时间显著缩短。考虑到使用C ++ 的主要原因之一是代码效率,这让我感到惊讶。

很明显,当归档程序/链接器将对象文件制作成库,或将它们链接到可执行文件中时,即使进行简单的优化也会受到惩罚。在下面的示例中,当由链接器而不是编译器进行简单内联时,性能下降了1.8%。看起来编译器技术应该足够先进,可以处理相当常见的情况,但实际上并没有发生。

以下是使用Visual Studio 2008的简单示例:

#include <cstdlib>
#include <iostream>
#include <boost/timer.hpp>

using namespace std;

int foo(int x);
int foo2(int x) { return x++; }

int main(int argc, char** argv)
{
  boost::timer t;

  t.restart();
  for (int i=0; i<atoi(argv[1]); i++)
    foo (i);
  cout << "time : " << t.elapsed() << endl;

  t.restart();
  for (int i=0; i<atoi(argv[1]); i++)
    foo2 (i);
  cout << "time : " << t.elapsed() << endl;
}

foo.cpp

int foo (int x) { return x++; }

运行结果:使用链接的foo相比内联foo2会有1.8%的性能损失。

$ ./release/testlink.exe  100000000
time : 13.375
time : 13.14

是的,链接器优化标志(/LTCG)已经启用。


4
你使用的编译器是什么?VC++有一个选项叫做“整个程序优化”,我认为这个选项可以实现你所要求的功能。 - Michael Myers
大多数现代编译器都有“链接时优化”选项,但通常需要手动选择,因为它会显著减慢链接速度。您在测试中启用了它吗?它通常允许跨对象内联等操作。 - Pavel Minaev
C++ 的主要原因之一是源代码的效率;将此论点应用于目标代码是牵强附会的。 - harpo
3
你进行了重复测量吗?你在差异方面有什么变化? - peterchen
3个回答

27

你的同事已经过时了。自2003年以来(在MS C++编译器上)就有这项技术:/LTCG。链接时代码生成正是处理这个问题的。据我所知,GCC在下一代编译器中有计划加入这个功能。

LTCG不仅仅优化代码,比如内联函数跨模块,而且实际上重新排列代码以优化缓存局部性和分支加载,参见Profile-Guided Optimizations。这些选项通常只用于发布版本构建,因为构建可能需要数小时才能完成:会链接一个带有仪器代码的可执行文件,运行一次负载分析,然后再根据分析结果进行链接。链接中包含了关于LTCG优化的详细信息:

内联 - 例如,如果存在一个频繁调用函数B的函数A,并且函数B相对较小,则基于配置文件的优化会将函数B内联到函数A中。
虚拟调用推测 - 如果虚拟调用或通过函数指针进行的其他调用频繁地指向某个特定函数,则基于配置文件的优化可以插入一个有条件执行的直接调用到该经常被指向的函数,并且该直接调用可以内联。
寄存器分配 - 使用配置文件数据进行优化可以得到更好的寄存器分配结果。
基本块优化 - 基本块优化允许将在给定帧内临时执行的常用基本块放置在同一组页面(局部性)中。这样可以最小化所使用的页面数量,从而减少内存开销。
大小/速度优化 - 可以对程序花费大量时间的函数进行速度优化。
函数布局 - 根据调用图和分析过的调用者/被调用者行为,倾向于沿着相同的执行路径的函数被放置在同一节中。
条件分支优化 - 通过值探测,基于配置文件的优化可以找出在switch语句中某个给定的值是否比其他值更常用。然后可以将该值从switch语句中提取出来。对于if/else指令也可以采取同样的做法,优化器可以根据哪个块更常为真来调整if/else的顺序,使其先放置。
死代码分离 - 在分析期间未调用的代码会被移到一个特殊的节中,该节附加在节集的末尾。这有效地将此节排除在经常使用的页面之外。
EH(异常处理)代码分离 - 通过基于配置文件的优化可以确定异常仅在异常条件下发生时,异常处理代码通常可以移动到一个单独的节中。
内存内嵌函数 - 如果能确定内嵌函数是否频繁调用,则更好地决定内嵌函数的扩展。内嵌函数也可以根据移动或复制的块大小进行优化。

链接时代码生成比编译为一个文件要慢。 - Andrew Prock
@drewster: 确实。将整个项目保持在一个单一模块中的操作开销会更大(源代码控制、分支和集成、代码所有权,甚至是编辑和导航)。单个文件可以适用于家庭产品。LTCG是为编译Windows代码库而设计的... - Remus Rusanu
@drewster:还有关于将基于代表性测试负载的剖析指南嵌入到代码生成决策中的整个讨论别忘了。没有编译器能够通过像死代码分离和基于实际测试负载值的分支优化这样的技巧来帮助处理这个问题。 - Remus Rusanu
1
@Remus,你不需要把所有的代码都放在一个文件中。#include可以很好地解决这个问题。问题是为什么即使在像这样微不足道的例子中,也会有如此大的惩罚。 - Andrew Prock
@jalf:不行。上述许多优化都依赖于分析指南测试负载。 - Remus Rusanu
显示剩余3条评论

4
我不是编译器专家,但我认为编译器可以利用更多可用的信息来进行优化,因为它作用于语言树,而链接器只能操作对象输出,比编译器看到的代码表达力要少得多。因此,链接器和编译器开发团队花费的精力较少,以致于无法在理论上匹配编译器所使用的技巧。

顺便说一句,很抱歉我转移了你最初的问题,讨论了ltcg。我现在明白你的问题略有不同,更关注于链接时间与编译时间静态优化的可能性/可用性。


这并不适用于Visual C++。当使用/GL编译并使用/LTCG链接时,编译器会在目标文件中生成一个中间表示形式,而不是实际的机器代码,链接器会获取该中间表示形式并进行实际编译。 - Ted Mielczarek

2

你的同事比我们大多数人都聪明。即使一开始看起来有些粗糙,将项目内联到一个单独的.cpp文件中具有一件其他方法(如链接时优化)不具备且短时间内也无法具备的东西 - 可靠性

然而,你是两年前提出这个问题的,我可以证明自那时以来很多事情已经发生了变化(至少在g ++上)。例如,虚函数的去虚拟化更加可靠。


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