Link Time Optimization - ShenYj/ShenYj.github.io GitHub Wiki

Link-Time Optimization

Link Time Optimization (LTO) 链接时间优化是指: 链接阶段执行模块间优化

通过整个程序分析和跨模块优化来获得更好的运行时性能的方法。

在编译阶段,clang 将发出 LLVM bitcode 而不是目标文件。

链接器识别这些 Bitcode 文件,并在链接期间调用 LLVM 以生成将构成可执行文件的最终对象。

接下来会加载所有输入的 Bitcode 文件,并将它们合并在一起以生成一个模块。

通俗来讲,链接器将所有目标文件拉到一起,并将它们组合到一个程序中。链接器可以查看整个程序,因此可以进行整个程序的分析和优化。通常,链接器只有在将程序翻译成机器代码后才能看到该程序。

LLVM 的 LTO 机制是通过把 LLVM IR 传递给链接器,从而可以在链接期间执行整个程序分析和优化。所以,LTO 的工作方式是编译器输出的目标文件不是常规目标文件:它们是 LLVM IR 文件,仅通过目标文件文件扩展名伪装为目标文件。

.

LTO有两种模式

  • Full LTO 是将每个单独的目标文件中的所有 LLVM IR 代码组合到一个大的 module 中,然后对其进行优化并像往常一样生成机器代码
  • Thin LTO 是将模块分开,但是根据需要可以从其他模块导入相关功能,并行进行优化和机器代码生成

进行 LTO 而不是一次全部编译的优点是(部分)编译与 LTO 并行进行。对于完整的 LTO(-flto=full),仅并行执行语义分析,而优化和机器代码生成则在单个线程中完成。对于 ThinLTO(-flto=thin),除全局分析步骤外,所有步骤均并行执行。因此,ThinLTO 比 FullLTO 或一次编译快得多。 使用的编译链接参数有

clang:
-flto=<value> 设置LTO的模式:full或者thin,默认full。
-lto_library <path> 指定执行LTO方式的库所在位置。当执行链接时间优化(LTO)时,链接器将自动去链接libLTO.dylib,或者从指定路径链接。

Xcode 设置路径 .

  • Monolithic -> Full LTO

dead strip 之后还可能存在不使用的符号,LTO在 dead strip 继续优化剥离此类符号

资料: Link-Time Optimization - LLVM

实例

准备代码

  • a.h

    extern int foo1(void);
    extern void foo2(void);
    extern void foo4(void);
  • a.c

    #include "a.h"
    
    static signed int i = 0;
    
    void foo2(void) {
    i = -1;
    }
    
    static int foo3() {
    foo4();
    return 10;
    }
    
    int foo1(void) {
    int data = 0;
    
    if (i < 0)
        data = foo3();
    
    data = data + 42;
    return data;
    }
  • main.c

    #include <stdio.h>
    #include "a.h"
    
    void foo4(void) {
    printf("Hi\n");
    }
    
    int main() {
    return foo1();
    }

编译链接

  • a.c 编译生成 bitcode 格式文件

    clang -flto -c a.c -o a.o
  • main.c 正常编译成目标文件

    clang -c main.c -o main.o
  • 通过 LTOa.cmain.c 通过 LTO 方式链接到一起

    clang -flto a.o main.o -o main

按照LTO优化方式:

  1. 链接器首先按照顺序读取所有目标文件(此时,是bitcode文件,仅伪装成目标文件)并收集符号信息。
  2. 接下来,链接器使用全局符号表解析符号。找到未定义的符号,替换weak符号等等。
  3. 按照解析的结果,告诉执行LTO的库文件(默认是libLTO.dylib)那些符号是需要的。紧接着,链接器调用优化器和代码生成器,返回通过合并bitcode文件并应用各种优化过程而创建的目标文件。然后,更新内部全局符号表。
  4. 链接器继续运行,直到生成可执行文件

我们的实例中,LTO整个的优化顺序为:

  1. 首先读取a.o(bitcode文件)收集符号信息。链接器将foo1()、foo2()、foo4()识别为全局符号。
  2. 读取main.o(真正的目标文件),找到目标文件中使用的符号信息。此时,main.o使用了foo1(),定义了foo4().
  3. 链接器完成了符号解析过程后,发现foo2()未在任何地方使用它将其传递给LTO。foo2()一旦可以删除,意味着发现foo1()里面调用foo3()的判断始终为假,也就是foo3()也没有使用,也可以删除。
  4. 符号处理完毕后,将处理结果传递给优化器和代码生成器,同时,将a.o合并到main.o中。
  5. 修改main.o的符号表信息。继续链接,生成可执行文件。

查看最后生成的可执行文件main的符号表信息:

❯ objdump --macho --syms main
main:
SYMBOL TABLE:
0000000100008008 l     O __DATA,__data __dyld_private
0000000100000000 g     F __TEXT,__text __mh_execute_header
0000000100003f70 g     F __TEXT,__text _foo1
0000000100003f30 g     F __TEXT,__text _foo4
0000000100003f50 g     F __TEXT,__text _main
0000000000000000         *UND* _printf
0000000000000000         *UND* dyld_stub_binder

可以看到,链接完成之后,我们自己声明的函数只剩下:main、foo1和foo4。

这个地方有个问题,foo4函数并没有在任何地方使用,为什么没有把它干掉?

因为 LTO 优化以入口文件需要的符号为准,来向外进行解析优化。所以,要优化掉foo4,那么就需要使用 dead strip

❯ clang -flto -Xlinker -dead_strip a.o main.o -o main
❯ objdump --macho --syms main
main:
SYMBOL TABLE:
0000000100000000 g     F __TEXT,__text __mh_execute_header
0000000100003f90 g     F __TEXT,__text _foo1
0000000100003f70 g     F __TEXT,__text _main
0000000000000000         *UND* dyld_stub_binder
⚠️ **GitHub.com Fallback** ⚠️