LTO 链接时优化

Posted rtoax

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LTO 链接时优化相关的知识,希望对你有一定的参考价值。

LTO 链接时优化

荣涛
2022-02-28

文档修改日志

日期修改内容修改人备注
2022-02-28创建荣涛

关键字

  • LTO:链接时优化(Link Time Optimization)
  • IPA:过程间分析(inter-procedural analysis)
  • IPO:过程见优化(inter-procedural optimization)
  • WHOPR:(Whole program assumptions, linker plugin and symbol visibilities)

引言

LTO(Link Time Optimization)链接时优化是链接期间的程序优化,多个中间文件通过链接器合并在一起,并将它们组合为一个程序,缩减代码体积,因此链接时优化是对整个程序的分析和跨模块的优化。

在link阶段优化,可以看到多个编译单元,一起进行优化,比如代码去重inline。选项:-flto.

LTO依赖在每个文件编译时候编译一些信息进去,在link阶段优化使用。加上-flto编译每个文件。

在link阶段执行LTO,比较慢,要load整个程序的所有代码来分析优化。

-fthinLTO, 可以让编译时记录的信息少一些,在link阶段也比fullLTO快。

链接时间优化 (LTO) 使 GCC 能够将其内部表示 (GIMPLE) 转储到磁盘,因此构成单个可执行文件的所有不同编译单元都可以作为单个模块进行优化。这扩展了程序间优化的范围以涵盖整个程序(或者更确切地说,包括链接时可见的所有内容)。

链接时间优化作为 GCC 前端实现,用于在.o文件的特殊部分发出的 GIMPLE 的字节码表示。目前,大多数基于 ELF 的系统以及 darwin、cygwin 和 mingw 系统都启用了 LTO 支持。

默认情况下,使用 LTO 支持生成的目标文件仅包含 GIMPLE 字节码。这样的对象被称为“slim”,它们需要工具像arnm解析 LTO 部分的符号表。对于大多数目标,这些工具已扩展为使用插件基础结构,因此 GCC 可以支持仅由中间代码组成的“slim”对象。

GIMPLE 字节码也可以与最终目标代码一起保存,如果-ffat-lto-对象选项被传递,或者如果没有检测到插件支持ar以及nm何时配置 GCC。它使使用 LTO 支持生成的目标文件比常规目标文件大。这种“胖”对象格式允许发布一组胖对象,可用于开发和优化构建的生产。A,也许令人惊讶的是,此功能的副作用是工具链中的任何错误都会导致 LTO 信息未被使用(例如,直接调用较旧的libtool调用ld)。这既是一个优点,因为系统更健壮,也是一个缺点,因为没有通知用户优化已被禁用。

在最高级别,LTO 将编译器一分为二。前半部分(“编写器”)生成优化和生成代码所需的所有内部数据结构的流式表示。这包括声明、类型、调用图和函数体的 GIMPLE 表示。

什么时候-flto在编译源文件期间给出,传递管理器执行所有传递all_lto_gen_passes。目前,此阶段由两个 IPA 通行证组成:

  • pass_ipa_lto_gimple_out 此 pass 执行中的lto_output函数 lto-streamer-out.cc,它遍历编码每个可达声明、类型和函数的调用图。这会生成下面描述的所有文件部分的内存表示。
  • pass_ipa_lto_finish_out 此 pass 执行中的produce_asm_for_decls函数 lto-streamer-out.cc,它获取在前一次传递中构建的内存映像并将其编码到相应的 ELF 文件部分中。

flto是使用lto的主要方法,是一个优化选项,禁用lto使用-fno-lto。flto主要做的操作有inline、ipa和alias分析等。

ThinLTO是一种可扩展和增量式的新型LTO,与LTO相比,表现甚至更好。要使用ThinLTO,只需添加-flto=thin选项即可进行编译和链接。第一阶段类似于传统LTO的步骤,在进行一些早期优化(主要是为了减小大小)之后,调用前端将每个输入源文件转换为包含IR的中间文件。只是使用ThinLTO,每个文件中都包含一个附加的摘要部分。

相关技术和背景

IPA/IPO: 过程间优化

inter-procedure-optimization 过程和过程之间优化,过程是指函数,这个优化是跨多个函数的,可以是一个编译单元内多个函数,也可以是多个编译单元一起——这要使用LTO。

过程间分析(inter-procedural analysis)是一个多步骤的过程,是LTO分析过程中的重要部分,也是一个跨模块的分析过程。跨模块的优化功能实现最早在1987年(Link time optimization - MIPS),后来相继出现了过程间分析和转换,动态链接程序的优化(IPA + LTO)。

过程间分析包含local分析和global分析。局部分析会为每一个过程和调用点生成Local Summary;全局分析时会根据具体要解决的数据流问题在Local Summary中去查找。但是使用IPA 分析无可避免的问题就是running out of memory!

Inlining操作

inline操作除了用户程序中使用关键字inline指定的代码会被inline之外,还有一些优化选项也会导致inline,比如-flto、-ipo选项。

LLVM或AOCC中flto

LLVM中lto work在IR(Intermediate representation)上,我们常用的选项-flto其实代表-flto=full,指lto将分散的目标文件的所有LLVM IR组合到一个大的LLVM模块中,然后对其进行整体分析优化并生成machinecode,该选项仅并行执行前端的语义分析,优化和machinecode生成在单线程完成。-flto=thin则是把模块分开,根据需要才从其他模块导入功能,并且除全局分析外均采用并行的方式进行优化和machinecode的生成。因此使用-flto=thin-flto=full编译链接的速度大大加快,而且对于SPEC CPU2017部分benchmark性能更好。

LTO 文件sections

LTO 信息存储在目标文件内的几个 ELF 部分中。节的数据结构和枚举代码定义在 lto-streamer.h.

这些部分是从lto-streamer-out.cc并从lto/lto.cc: lto_file_read. 处理每个部分的读/写的各个函数如下所述。

命令行选项 ( .gnu.lto_.opts)

本节包含用于生成目标文件的命令行选项。这在链接时用于确定优化级别和其他设置,如果它们没有在链接器命令行中明确指定。

目前,GCC 不支持将使用不同命令行选项集编译的 LTO 目标文件合并到单个二进制文件中。在链接时,命令行上给出的选项和保存在链接时集中所有文件上的选项将全局应用。不尝试验证标志的组合(除了由选项处理完成的通常验证)。这是在 lto/lto.cc: lto_read_all_file_options.

示例:

    .section    .gnu.lto_.opts,"e",@progbits
    .string "'-fno-openmp' '-fno-openacc' '-fno-pie' '-fcf-protection=none' '-mtune=generic' '-march=x86-64-v2' '-flto=auto'"
    .text

符号表 ( .gnu.lto_.symtab)

此表替换了 LTO IL 中表示的函数和变量的 ELF 符号表。“胖”对象的优化汇编代码使用和导出的符号可能与中间代码使用和导出的符号不匹配。该表是必要的,因为中间代码的优化程度较低,因此需要单独的符号表。

此外,“胖”对象中的二进制代码将缺少对函数的调用,因为调用在中间语言流出后的编译时进行了优化。在某些特殊情况下,链接时优化期间可能不会发生相同的优化。如果只使用一个符号表,这将导致未定义的符号。

符号表在 lto-streamer-out.cc: produce_symtab.

示例:

    .section    .gnu.lto_.symtab.f2cb3d49024ea67,"e",@progbits
    .string "func1"
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string "\\300"
    .string ""
    .string ""
    .string "main"
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string ""
    .string "\\327"
    .string ""
    .string ""
    .text

全局声明和类型 ( .gnu.lto_.decls)

本节包含表示调用图、静态变量和顶级调试信息所需的所有声明和类型的中间语言转储。

本节内容发布于 lto-streamer-out.cc: produce_asm_for_decls. 类型和符号以拓扑顺序发出,当文件被读回 (lto.cc: read_cgraph_and_symbols)。

示例:

    .section    .gnu.lto_.decls.f2cb3d49024ea67,"e",@progbits
    .string "(\\265/\\375`\\347\\002\\325\\027"
    .string "\\026\\257\\243H\\300\\026U:\\324%\\310M\\354\\332\\337\\220\\300 \\317\\255\\266j\\305\\246xo\\200\\025\\nu_\\214\\020.a\\301\\036(\\2249\\263\\265N\\016\\177\\b\\037X\\261\\267\\346\\033\\376\\007BP\\002s\\fE\\020\\314\\0332K\\001\\310\\360\\341\\202\\230\\210d[\\"S\\204"
    .string "\\205"
    .string "\\221"
    .ascii  "9\\351\\237\\263\\373\\351\\301\\377\\366\\307@\\235BZ\\215\\013\\313\\243"
    .ascii  "\\275\\202\\263\\245\\037w\\250P\\370\\262\\302\\2038"
    .string "\\032\\f\\363_P\\246\\317\\275\\206\\236\\346<\\234\\202\\027\\177\\271\\250\\371\\227E?\\256t\\376\\217\\325\\200\\376\\032*\\236\\301\\\\\\356\\347\\341\\026\\334\\344\\375\\247\\335\\n\\230\\302\\234\\034N\\350\\363\\025?\\357B\\335I\\366\\224\\351\\267\\313\\214\\030*R\\206b\\265\\355\\203bh\\264W\\007\\211-m\\033\\331>b;\\265=\\3056i8\\034\\016m\\213\\260\\355\\302&\\035\\b'\\016B\\375\\362E\\247\\023\\253\\3550\\336\\324\\266\\200\\301\\300\\322_\\344?\\203\\352\\003Ko\\207X\\275Fl\\300\\3121\\230\\001\\351\\004\\244I\\3636h\\320\\231\\3763\\304\\340\\213j\\245\\321^\\357z!\\3304KZ\\007\\266\\353\\317\\3417\\275\\230\\270\\f\\223\\020\\021\\253\\305W\\264\\334\\333tG\\267\\275<\\375\\027\\274\\201\\037\\375SZI\\322\\276f\\252\\273\\231\\266\\177@\\212\\036\\365\\016\\267\\333\\320\\222\\374+\\032\\342\\177\\212~\\305m7~\\236\\316 =\\272\\234\\347:\\361\\001\\002xdtr\\210u\\213\\f\\004@C\\267\\343#V\\207Y\\"\\333\\001\\b"
    .string "\\371\\201\\342#\\032\\242\\261\\341\\243\\323\\262C\\366\\002-\\222\\025\\"\\231/\\320\\003\\375i\\201\\017?ld\\021\\353&Y%^\\241\\r\\"\\225;+t\\001\\375i+d*\\363\\323L\\273\\rv\\033\\324!;\\035Ac\\265\\213t\\272\\bF\\022\\005\\351\\375\\355\\362\\330\\333\\263G\\326\\267<\\006\\326\\323m\\273\\337\\321\\317K@-\\202\\261\\356\\201\\3216$\\3715p\\364\\022\\331\\"\\336\\236o\\233\\326\\377ez\\031\\003\\206\\020!c\\0362\\031k\\265R\\215'P&jlfb\\252\\024\\253Zg\\254\\030\\311"
    .string "\\f\\233\\341\\264R\\255\\224\\314\\245\\302i\\255R,^\\256\\324L\\306\\232\\245f7 R0\\325\\314\\365\\202\\305R,\\025\\260\\024\\251\\b\\326P\\343\\001Ub\\004\\brE\\341u\\243R1\\326\\254^,Z3^\\335\\304X/\\226\\213\\264\\205\\026\\023.d\\230\\220a\\203\\013\\001QR\\323.G\\217H\\254??kT\\377\\032\\026\\334\\021\\201*<\\266\\364,\\361\\032y\\237\\214@\\252\\304\\330\\021\\253\\327\\311\\002\\355"
    .ascii  "$\\2351W~bu\\214\\\\&\\333\\244\\204n\\341\\351\\211\\325\\"\\316\\203\\305"
    .ascii  "\\001\\346\\211N, p\\202\\221J\\352\\001'T\\325\\033\\243\\022\\244\\371\\362"
    .ascii  "\\330\\366|\\215\\224\\245^F\\222d\\232\\342\\326\\204$;\\n[\\007\\0030O4"
    .ascii  "^;\\034\\003\\2027\\034L\\276\\\\7\\n\\301U\\261\\225\\370\\2667HD\\f\\224\\242"
    .ascii  "e\\232\\205ty\\035\\313)G\\331`\\276a^\\302\\213\\036\\242h\\224RCK\\366"                                                                  
    .ascii  "\\3301\\250\\275\\324\\333\\206\\301\\254\\373\\314\\336[\\305,<\\006"
    .text

调用图 ( .gnu.lto_.cgraph)

本节包含 GCC 过程间优化基础结构使用的基本数据结构。本节存储一个带注释的多图,它表示函数和调用站点以及变量、别名和顶级asm语句。

本节在 lto-streamer-out.cc:output_cgraph并读入 lto-cgraph.cc: input_cgraph.

国际音标参考 ( .gnu.lto_.refs)

本节包含函数和静态变量之间的引用。它由发出lto-cgraph.cc:output_refs 并阅读lto-cgraph.cc: input_refs.

示例:

    .section    .gnu.lto_.refs.f2cb3d49024ea67,"e",@progbits                                                                                
    .string "(\\265/\\375 \\013Y"
    .string ""
    .string "\\007"
    .string ""
    .string ""
    .string "\\001"
    .string ""
    .string "\\001\\001"
    .string ""
    .text

函数体 ( .gnu.lto_.function_body.)

本节包含中间语言表示形式的函数体。每个函数体都在一个单独的部分中,以允许将该部分独立复制到不同的目标文件或按需读取函数。

函数在 lto-streamer-out.cc:output_function并读入 lto-streamer-in.cc: input_function.

如我的函数:

static int ret = 1024;

int func1(void)

    return ret;


int main(int argc, char *argv[])

	return func1();


gcc main.c -flto=auto -S -o main.1.flto.s编译后,对应:

    .section    .gnu.lto_func1.1.f2cb3d49024ea67,"e",@progbits                                                                              
    .string "(\\265/\\375`S"
    .string "U\\b"
    .string "\\302\\21786P\\215\\322\\0010\\022nR\\224\\004\\256\\001E\\t\\231\\322Dr.\\324-\\013\\2024\\237\\356\\177\\341=h\\236\\357\\374\\330\\337\\204,k.n\\021\\355\\257+m\\355\\214\\330?\\022B\\244L)\\336\\024\\204<\\033\\b\\300\\235\\367\\341 \\007E\\235\\355\\303m`C\\250\\003=\\021\\307\\203\\236'\\001v\\233\\327\\tu\\f\\220\\003\\352\\026`\\346\\212J\\375\\352\\b\\033\\"\\244\\214%\\026\\210\\245x\\355n\\306\\007\\243<\\271j\\212\\365\\017\\273|\\016;\\270\\225?=\\363\\335\\322\\236\\245\\006X\\220\\022\\305\\377%\\246\\246\\354_\\024e?\\235\\252m\\032\\273\\0171eY\\031H\\007\\246d9\\2252&^kZ\\315\\355\\333\\376l\\001\\373\\036W\\266\\013c)`\\204T\\230Lv\\027bN\\240=\\303>\\240=\\323\\213\\177\\327\\374|lt\\375\\352\\225\\021\\225\\304\\023\\373b\\214\\256D\\334\\230mkl\\033\\330&\\t\\016"
    .ascii  "4\\003\\320j\\207Ls\\334\\006\\0323\\226U\\033\\373\\303 \\031\\357@a\\326"
    .ascii  "\\026\\322\\033\\232Bh\\226\\313\\211v>\\001"
    .text

静态变量初始化器 ( .gnu.lto_.vars)

本节包含全局变量池中的所有符号。它由发出lto-cgraph.cc:output_varpool并读入 lto-cgraph.cc: input_cgraph.

IPA 通行证使用的摘要和优化摘要

这些部分由 IPA 通行证使用,这些通行证需要在 LTO 生成期间发出摘要信息,以便在链接时读取和聚合。每个通道负责实现两个通道管理器挂钩:一个用于编写摘要,另一个用于读取摘要。这些部分的格式完全取决于每个单独的通道。唯一的要求是编写器和读取器挂钩在格式上达成一致。

.gnu.lto_.jmpfuncs

示例:

    .section    .gnu.lto_.jmpfuncs.dc9758b1558318ba,"e",@progbits                                                                           
    .string "(\\265/\\375 \\016E"
    .string ""
    .string "\\020\\002"
    .string "\\001"
    .ascii  "\\034\\300\\002"
    .text

.gnu.lto_.pureconst

.gnu.lto_.reference

在 IPA 通行证中使用摘要信息

程序在内部表示为调用图(一个多图,其中节点是函数,边是调用站点)和varpool (程序中的静态和外部变量列表)。

程序间优化被组织为一系列单独的传递,它们在调用图varpool 上运行。为了使 WHOPR 的实施成为可能,每个过程间优化过程都被分成几个阶段,这些阶段在 WHOPR 编译期间的不同时间执行:

  • LGEN时间
  1. 生成摘要(generate_summaryin struct ipa_opt_pass_d)。此阶段分析每个函数体,检查变量初始化器并将相关信息存储到特定于传递的数据结构中
  2. 写总结(write_summaryin struct ipa_opt_pass_d)。此阶段写入由generate_summary. 摘要进入它们自己的LTO_section_*部分,必须在lto-streamer.h: enum lto_section_type. 通过调用创建一个新部分, create_output_block并且可以使用 lto_output_*例程写入数据。
  • WPA时间
  1. 阅读摘要(read_summaryin struct ipa_opt_pass_d)。此阶段以与write_summary.
  2. 执行(execute在struct opt_pass)。这执行过程间传播。这必须在没有实际访问各个函数体或变量初始化程序的情况下完成。通常,这会导致对调用图中所有节点的摘要信息进行传递闭包操作。
  3. 写优化总结 (write_optimization_summaryin struct ipa_opt_pass_d)。这会将跨过程传播的结果写入目标文件。这可以使用与write_summary.
  • LTRANS 时间
  1. 阅读优化摘要 (read_optimization_summaryin struct ipa_opt_pass_d)。的对应物 write_optimization_summary。这以完全相同的格式读取过程间优化决策 write_optimization_summary。
  2. 变换(function_transform和 variable_transformin struct ipa_opt_pass_d)。实际的函数体和变量初始值设定项会根据从Execute阶段 传递下来的信息进行更新。

LTO、WHOPR 和经典的非 LTO 编译之间共享过程间传递的实现。

  • 在传统的逐文件模式下,每次传递都会 在编译器的单个执行上下文中 执行其自己的Generate summary、Execute和Transform阶段。
  • 在 LTO 编译模式下,每个 pass 在编译时使用Generate summary和Write summary阶段,而Read summary、Execute和 Transform阶段在链接时执行。
  • 在 WHOPR 模式下,使用所有阶段。

整个程序假设、链接器插件和符号可见性

单独使用时,链接时优化带来的好处相对较小。问题是过程间信息的传播在其他编译单元(例如来自动态链接库)调用或引用的函数和变量之间效果不佳。我们说这样的函数和变量是外部可见的。

为了使情况更加困难,许多应用程序将自己组织为一组共享库,并且默认的 ELF 可见性规则允许在运行时用不同的符号覆盖任何外部可见的符号。这基本上禁用了跨此类函数和变量的任何优化,因为编译器无法确定它所看到的函数体是否与将在运行时使用的函数体相同。任何未在源代码中声明的函数或变量都会static降低过程间优化的质量。

为了避免这个问题,编译器必须假设它在进行链接时优化时看到了整个程序。严格来说,即使在链接时,整个程序也很少可见。标准系统库通常是动态链接的,或者不提供链接时间信息。在 GCC 中,整个程序选项 (-f整个程序) 断言当前编译单元中定义的每个函数和变量都是静态的,除了函数main(注意:在链接时,当前单元是使用 LTO 编译的所有对象的联合)。由于某些函数和变量需要在外部引用,例如由另一个 DSO 或从汇编文件中引用,GCC 还提供了函数和变量属性externally_visible,可用于禁用-f整个程序在特定符号上。

整个程序模式假设在 C++ 中稍微复杂一些,其中标头中的内联函数被放入COMDAT 部分。COMDAT 函数和变量可以由多个目标文件定义,它们的主体在链接时和动态链接时是统一的。COMDAT 函数仅在其地址未被占用时才更改为本地函数,因此不与库共享它们是无害的。COMDAT 变量始终保持外部可见,但是对于只读变量,假定它们的初始化程序不能被不同的值覆盖。

GCC 提供了函数和变量属性 visibility,可用于指定外部可见符号的可见性(或者 -f默认可见性命令行选项)。ELF 定义了default、protected和可见性 hidden。 internal

最常用的是可见性hidden。它指定不能从当前共享库的外部引用该符号。不幸的是,编译器中的链接时优化不能直接使用此信息,因为整个共享库也可能包含非 LTO 对象,并且这些对象对编译器不可见。

GCC 使用链接器插件解决了这个问题。链接器插件是链接器的一个接口,它允许外部程序声明给定目标文件的所有权。然后,链接器通过向插件查询所声明对象的符号表来执行链接过程,一旦链接决策完成,插件就可以在进行实际链接之前提供最终的对象文件。链接器插件获取符号解析信息,该信息指定声明的对象提供的哪些符号与被链接的二进制文件的其余部分绑定。

GCC 被设计为独立于工具链的其余部分,旨在支持没有插件支持的链接器。因此,默认情况下它不使用链接器插件。collect2相反,在传递给链接器之前检查目标文件,发现具有 LTO 部分的对象lto1首先传递给。此模式不适用于图书馆档案。需要存档中的哪些目标文件的决定取决于实际的链接,因此 GCC 必须自己实现链接器。分辨率信息也丢失了,因此 GCC 需要根据-f整个程序. hidden 如果没有链接器插件,GCC 还假定默认情况下非 LTO 代码 已声明符号而不引用符号。

参考链接


Copyright (C) CESTC Com.

以上是关于LTO 链接时优化的主要内容,如果未能解决你的问题,请参考以下文章

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

可以跨 C 和 C++ 方法优化 gcc 或 clang 的 LTO

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

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

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

防止 GCC LTO 删除函数