BOLT 的处理流程

BOLT 的处理流程可大致分为三个主要阶段:函数发现、CFG (控制流图) 构建以及二进制内容重写。以下对这三个阶段进行详细描述:

  1. 函数发现:

    • 目的:BOLT 需要知道二进制文件中每个函数的位置和大小,以便它能够进行进一步的优化。

    • 方法

      • 原始模式:BOLT 使用 ELF 符号表来确定二进制文件中的函数位置。ELF 文件包含了关于程序段和符号(如函数和变量)的信息。
      • 后续模式:为了更为全面地捕获所有的函数,BOLT 采用了一种策略,该策略依赖链接器在可执行文件中记录的重新定位信息[1]。重新定位信息提供了函数和其他符号的准确位置。
    • 结果:完成这个阶段后,BOLT 能够映射出二进制中的所有函数的名称、地址和大小。

  2. CFG (控制流图) 构建:

    • 目的:为每个函数生成一个控制流图,以揭示其内部的执行路径和结构。

    • 方法

      • BOLT 对每个已识别的函数进行反汇编。
      • 在反汇编过程中,BOLT 会识别所有的分支指令,并通过这些指令重建函数内的控制流信息。
    • 结果:为每个函数生成了详细的 CFG。CFG 描述了函数内基本块之间的流动关系,其中每个基本块是一串指令序列,这串指令没有内部跳转。

  3. 二进制内容重写:

    • 目的:基于前两个阶段收集的信息和执行的优化,重写并重新组织二进制内容,使其性能更优。

    • 方法

      • BOLT 在 CFG 表示中运行其优化管道,进行例如代码重新布局、循环优化等各种优化。
      • 它将新的、优化过的代码“发出”或写入到新的二进制内容中。
      • 使用 LLVM 的运行时动态链接器(为 LLVM JIT 系统创建)来解析函数与本地符号(如基本块)之间的引用。
    • 结果:最终生成了一个优化过的二进制文件,同时更新了 ELF 结构,以反映新的大小和其他相关的修改。

总的来说,BOLT 通过深入分析二进制文件和执行数据,对其进行精细的优化,而不需要源代码。这种方法提供了对那些不能或不愿重新编译源代码的场景的强大支持,如数据中心的大型应用或第三方闭源软件。

[1] ELF (Executable and Linkable Format) 是一个常用的文件格式,用于描述程序或某些程序片段。它确实有一个符号表,通常包含了很多关于函数和全局变量的信息。然而,这个表可能并不总是完整的。

以下是几个原因,说明为什么 BOLT 可能需要更多的信息,特别是重新定位信息,而不仅仅依赖 ELF 符号表:

  1. 静态链接的库:当静态库被链接到一个二进制文件中时,这些库中的很多符号可能不再出现在最终 ELF 二进制的符号表中。而重新定位信息还会包含这些符号的位置,因为它们在链接过程中是需要的。

  2. 去符号化:出于多种原因(如安全或大小考虑),发布的二进制文件可能已被去符号化,从而移除了很多符号表中的条目。重新定位信息在这种情况下可能仍然存在,因此可以为 BOLT 提供有关函数位置的额外信息。

  3. 内联函数:编译器可能会内联一些函数,这意味着它们不会作为独立的函数出现在最终二进制中。重新定位信息可以帮助 BOLT 识别这些被内联的函数的代码片段。

  4. 动态链接:对于动态链接的二进制文件,重新定位表描述了如何在加载时修复这些引用,从而使它们指向正确的位置。

  5. 更准确的函数边界:即使 ELF 符号表中有关于函数的信息,它可能不提供足够的细节来精确地确定函数的开始和结束。重新定位信息可以提供更准确的边界。

因此,BOLT 使用重新定位信息来确保它可以捕获和优化二进制文件中的所有代码,而不仅仅是那些仍然在符号表中的部分。这提供了一个更全面和准确的程序表示,从而允许更深入和高效的优化。

接下来,让我们使用一个简化的例子来说明 BOLT 的工作流程。

场景设定

假设我们有一个简单的应用,该应用由两个主要函数组成:frequentFunction()rareFunction()。在运行期间,frequentFunction() 被频繁地调用,而 rareFunction() 很少被调用。

void frequentFunction() {
    // ... some computations ...
}

void rareFunction() {
    // ... some other computations ...
}

int main() {
    for (int i = 0; i < 1000000; i++) {
        frequentFunction();
    }
    rareFunction();
    return 0;
}

我们先传统地编译并运行这个应用。

BOLT 的工作流程

  1. 性能采集
    我们首先使用性能工具(如 Linux 的 perf)来运行应用程序,并收集关于其执行的信息。

    perf record ./app
    

    这将生成一个名为 perf.data 的文件,其中包含应用程序的执行信息。

  2. BOLT 优化
    接下来,我们使用 BOLT 来优化我们的应用程序,基于 perf.data 中的信息:

    llvm-bolt ./app -o app.bolted -data perf.data
    
  3. BOLT 的分析与优化

    • BOLT 分析 perf.data,发现 frequentFunction() 是热点,因为它经常被调用。
    • BOLT 会重新布局二进制代码,确保 frequentFunction() 在内存中是连续的,这有助于提高 ICache 命中率。
    • rareFunction() 可能被移到二进制的一个较远的位置,因为它的调用频率低。
    • BOLT 还可能采用其他针对性能的优化,如减少不必要的跳转等。
  4. 运行优化的应用程序
    现在,我们可以运行 BOLT 优化过的应用程序,并期望看到性能的提升:

    ./app.bolted
    

在这个简化的例子中,主要的收益可能来自于代码布局的优化,特别是对于那些被频繁调用的函数。当然,对于更复杂的应用程序和代码基础,BOLT 可以执行更多种类的优化,并提供更显著的性能提升。

你可能感兴趣的:(编译优化,编译优化)