是否有理由不使用链接时间优化 (LTO)?

Posted

技术标签:

【中文标题】是否有理由不使用链接时间优化 (LTO)?【英文标题】:Is there a reason why not to use link-time optimization (LTO)? 【发布时间】:2014-07-07 08:09:54 【问题描述】:

GCC、MSVC、LLVM 和可能的其他工具链支持链接时间(整个程序)优化,以允许优化编译单元之间的调用。

编译生产软件时是否有理由不启用此选项?

【问题讨论】:

见Why not always use compiler optimization?。那里的答案在这里同样适用。 @Mankarse 他问“在编译生产软件时” 所以那里的大多数答案都不适用。 @user2485710:您有与 ld 不兼容的文档吗?我在当前的 gcc 文档 (gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) 和一个有点旧的 wiki (gcc.gnu.org/wiki/LinkTimeOptimization) 中读到的内容要么没有提及 ld 不兼容性(gcc 文档),要么明确说明了兼容性(wiki)。从 lto 的操作方式来看,即在目标文件中有 additional 信息,我的猜测是目标文件保持兼容性。 启用-O2 会有所不同。在 10 分钟内构建 +5 秒。启用 LTO 会产生 ca +3 分钟的差异,有时ld 会耗尽地址空间。这是始终使用 -O2 编译 的一个很好的理由(因此您调试的可执行文件与您将发布的可执行文件是二进制相同的!)并且在 LTO 足够成熟之前不要使用它(这包括可接受的速度)。您的里程可能会有所不同。 @Damon:发布版本不是我一直在调试的版本,而是在测试中幸存下来的版本。无论如何,测试都会获得一个单独的构建,安装在干净的机器上(所以我知道安装包没有丢失任何依赖项)。 【参考方案1】:

我假设 “生产软件” 是指您交付给客户/投入生产的软件。 Why not always use compiler optimization? 的答案(Mankarse 亲切地指出)主要适用于您想要调试代码的情况(因此该软件仍处于开发阶段 - 未投入生产)。

我写这个答案已经过去了 6 年,需要更新。早在 2014 年,问题是:

链接时间优化偶尔会引入微妙的错误,例如Link-time optimization for the kernel。我认为到 2020 年这已经不是什么大问题了。防范此类编译器和链接器错误:进行适当的测试以检查您即将发布的软件的正确性。 Increased compile time。有人声称自 2014 年以来情况已显着改善,例如感谢slim objects。 大量内存使用。 This post 声称,由于分区,近年来情况已大大改善。

从 2020 年开始,我将尝试在我的任何项目中默认使用 LTO。

【讨论】:

我同意这样的回答。我也不知道为什么不默认使用 LTO。感谢您的确认。 @Honza:可能是因为它倾向于使用大量资源。尝试使用 LTO 编译 Chromium、Firefox 或 LibreOffice...(仅供参考:即使没有 LTO,至少其中一个甚至无法在具有 GNU ld 的 32 位机器上编译,仅仅是因为工作集不适合 虚拟地址空间!) 可以介绍吗?除非编译器坏了,否则不会可能会发现吗? 当然。 任何其他对损坏代码的优化都可以。 @Deduplicator 你确实知道答案是在 2014 年写的,对吧?当时,LTO 的实现仍然有些漏洞。另请参阅我链接到的文章。 @Bogi 根据我的经验,开发人员不必等待发布版本的编译完成。构建发布版本应该是发布过程或 CI/CD 管道的一部分。即使 LTO 很慢,对开发人员来说也不重要,因为他们不会等待它。较长的发布构建时间不应阻碍他们的日常工作。【参考方案2】:

This recent question 提出了另一种可能(但相当具体)的情况,在这种情况下 LTO 可能会产生不良影响:如果所讨论的代码针对时序进行了检测,并且已使用单独的编译单元来尝试保持检测的相对顺序和检测语句,那么 LTO 很有可能破坏必要的排序。

我确实说过这是具体的。

【讨论】:

【参考方案3】:

如果您的代码写得很好,那应该只是有利的。您可能会遇到编译器/链接器错误,但这适用于所有类型的优化,这种情况很少见。

最大的缺点是它大大增加了链接时间。

【讨论】:

为什么会增加编译时间?是不是编译器在某个点停止编译(它生成代码的一些内部表示,并将其放入目标文件而不是完全编译的代码),所以它应该更快? 因为编译器现在必须创建 GIMPLE 字节码以及目标文件,以便链接器有足够的信息进行优化。创建这个 GIMPLE 字节码有开销。 据我所知,在使用 LTO 时,编译器只生成字节码,即不会发出特定于处理器的程序集。所以应该更快。 GIMPLE 是目标文件的一部分好吗gcc.gnu.org/onlinedocs/gccint/LTO-Overview.html 如果你计时的话,它在任何代码库上都有额外的编译时间开销【参考方案4】:

除了到this,

考虑一个来自嵌入式系统的典型例子,

void function1(void)  /*Do something*/ //located at address 0x1000 
void function2(void)  /*Do something*/ //located at address 0x1100
void function3(void)  /*Do something*/ //located at address 0x1200

使用预定义地址的函数可以通过如下的相对地址调用,

 (*0x1000)(); //expected to call function2
 (*0x1100)(); //expected to call function2
 (*0x1200)();  //expected to call function3

LOT 可能导致意外行为。

【讨论】:

这是一个有趣的评论,因为 LTO 可能会导致链接器内联小型且很少使用的函数。我在 Fedora 上使用 GCC 9.2.1 和 Clang 8.0.0 测试了一个稍微不同的示例,它运行良好。唯一的区别是我使用了一个函数指针数组: ``` typedef int FUNC(); FUNC *ptr[3] = func1, func2, func3;返回 (*ptr)() + (*(ptr+1))() + (*(ptr+2))(); ```【参考方案5】:

鉴于代码已正确实现,链接时间优化应该不会对功能产生任何影响。但是,在某些情况下,不是 100% 正确的代码通常不会在没有链接时间优化的情况下工作,但在链接时间优化之后,不正确的代码将停止工作。切换到更高的优化级别时也有类似的情况,例如使用 gcc 从 -O2 到 -O3。

也就是说,根据您的具体情况(例如,代码库的年龄、代码库的大小、测试的深度、您是开始项目还是接近最终版本……)您将拥有来判断这种变化的风险。

链接时间优化可能导致错误代码出现意外行为的一种情况如下:

假设您有两个源文件read.cclient.c,您将它们编译成单独的目标文件。在文件read.c 中有一个函数read,除了从特定的内存地址读取之外什么也不做。但是,这个地址的内容应该标记为volatile,但不幸的是被遗忘了。从client.c 函数read 被同一个函数多次调用。由于read 只从地址执行一次单次读取,并且没有超出read 函数边界的优化,所以read 将始终在调用时访问相应的内存位置。因此,每次从client.c 调用read 时,client.c 中的代码都会从地址中获取一个新读取的值,就像使用了volatile 一样。

现在,通过链接时间优化,read.c 中的小函数 read 可能会被内联,无论从 client.c 中调用它。由于缺少volatile,编译器现在将意识到代码会从同一地址多次读取,因此可能会优化内存访问。因此,代码开始表现不同。

【讨论】:

另一个更相关的问题是代码不可移植,但在被实现处理时是正确的,作为“符合语言扩展”的一种形式,在比标准规定的更多情况下指定它们的行为。 【参考方案6】:

该标准并没有强制要求所有实现都支持完成所有任务所需的语义,而是允许旨在适用于各种任务的实现通过在超出 C 标准规定的极端情况下定义语义来扩展语言,其方式如下:对这些任务很有用。

这种形式的一个非常流行的扩展是指定跨模块函数调用将以与平台的应用程序二进制接口一致的方式处理,而不考虑 C 标准是否需要这种处理。

因此,如果对一个函数进行跨模块调用,例如:

uint32_t read_uint32_bits(void *p)

  return *(uint32_t*)p;

生成的代码将读取地址 p 的 32 位存储块中的位模式,并使用平台的本机 32 位整数格式将其解释为 uint32_t 值,而不考虑该块如何的存储来保存该位模式。同样,如果给编译器类似的内容:

uint32_t read_uint32_bits(void *p);
uint32_t f1bits, f2bits;
void test(void)

  float f;
  f = 1.0f;
  f1bits = read_uint32_bits(&f);
  f = 2.0f;
  f2bits = read_uint32_bits(&f);

编译器将在堆栈上为f 保留存储,将 1.0f 的位模式存储到该存储,调用 read_uint32_bits 并存储返回值,将 2.0f 的位模式存储到该存储,调用 @ 987654327@ 并存储该返回值。

标准没有提供语法来指示被调用函数可能会使用类型uint32_t读取它接收到的地址的存储,也没有指示给函数的指针可能是使用类型float编写的,因为实现旨在用于低级编程的语言已经将语言扩展到支持此类语义,而无需使用特殊语法。

不幸的是,添加链接时间优化将破坏任何依赖该流行扩展的代码。有些人可能会认为这样的代码有问题,但如果人们认识到 C 原则的精神“不要阻止程序员做需要做的事情”,那么标准未能强制支持流行的扩展就不能被视为打算弃用如果标准未能提供任何合理的替代方案,则使用它。

【讨论】:

这有什么关系?类型双关语是与 LTO 完全无关的 C 语言功能。 @MattF.:在没有 LTO 的情况下,只要执行跨越编译单元边界,抽象和物理机器状态就会同步。如果代码将值存储到 64 位 unsigned long 并将其地址作为 void* 传递给不同编译单元中的函数,该函数将其转换为 64 位 unsigned long long* 并取消引用它,那么除非实现使用LTO 行为将根据平台 ABI 定义,而不考虑被调用函数是否使用与调用者相同的类型访问存储。 @MattF.:基本上,我的观点是,委员会认为标准没有必要让程序员要求编译器做程序员可能需要他们做的事情,但他们没有办法避免这样做,但随后编译器被更改,以便编译器可以避免此类事情,而无需考虑程序员是否需要它们。 would be defined in terms of the platform ABI without regard for whether the called function accesses storage using the same type as the caller. 无论 LTO 如何,都是如此。根据定义,指针转换会重新解释类型,而不管其实际数据如何。 @MattF.:如果编译器可以看到一个函数只写入 unsigned long long 类型的指针,并且从不取消引用任何 unsigned long 类型的指针,它可能会避免同步抽象和物理unsigned long 类型的对象在调用函数之前/之后的值,从而破坏了任何依赖于根据平台 ABI 处理的 unsigned long 类型的操作的代码。【参考方案7】:

LTO 还可以揭示代码签名算法中的边缘情况错误。考虑基于对某个对象或模块的 TEXT 部分的某些期望的代码签名算法。现在 LTO 优化了 TEXT 部分,或者以代码签名算法无法处理的方式将内容内联到其中。最坏的情况是,它只影响一个特定的分配管道,而不影响另一个,因为每个管道上使用的加密算法存在细微差别。祝你好运,弄清楚为什么应用从管道 A 而不是 B 分发时无法启动。

【讨论】:

【参考方案8】:

LTO 支持存在缺陷,LTO 相关问题对于编译器开发人员的优先级最低。例如:mingw-w64-x86_64-gcc-10.2.0-5 适用于 lto,mingw-w64-x86_64-gcc-10.2.0-6 segfauls 使用虚假地址。我们刚刚注意到 windows CI 停止工作。

请以following issue为例。

【讨论】:

以上是关于是否有理由不使用链接时间优化 (LTO)?的主要内容,如果未能解决你的问题,请参考以下文章

LTO 链接时优化

LTO 链接时优化

Mac 上的 G++ 链接时优化 - 编译器/链接器错误?

静态链接与动态链接

CUDA 11 中的链接时优化 - 它们是啥以及如何使用它们?

如何将 GCC LTO 与不同优化的目标文件一起使用?