LLVM程序分析与编译转换框架论文分享

LLVM 2004年论文原文

概述

本文描述了 LLVM(低级虚拟机),一种编译器框架,旨在通过在编译时、链接时、运行时,以及运行之间的空闲时间。 LLVM 以静态单一赋值 (SSA) 形式定义了一种通用的低级代码表示,具有几个新颖的特性:一个简单的、独立于语言的类型系统,它公开了通常用于实现高级语言特性的原语;类型化地址算术指令;以及一种简单的机制,可用于统一高效地实现高级语言(以及 C 中的 setjmp/longjmp)的异常处理功能。 LLVM 编译器框架和代码表示形式共同提供了对程序的实际、终生分析和转换很重要的关键功能的组合。据我们所知,现有的编译方法没有提供所有这些功能。我们描述了 LLVM 表示和编译器框架的设计,并以三种方式评估设计:

(a)表示的大小和有效性,包括它提供的类型信息;

(b) 程序间的编译器性能问题

© LLVM 为几个具有挑战性的编译器问题提供的好用的说明性示例

1. 简介

现代应用程序的规模不断扩大,在执行过程中显着改变了它们的行为,支持动态扩展和升级,并且通常具有用多种不同语言编写的组件。虽然一些应用程序有小热点,但其他应用程序将它们的执行时间均匀地分布在整个应用程序中。为了最大限度地提高所有这些程序的效率,我们认为必须在程序的整个生命周期内执行程序分析和转换。这种“终身代码优化”技术包括在链接时执行的过程间优化(以保持单独编译的好处)、在每个系统上安装时依赖于机器的优化、运行时的动态优化和配置文件引导的优化使用从最终用户收集的配置文件信息在运行之间(“空闲时间”)。程序优化并不是终身分析和转换的唯一用途。静态分析的其他应用基本上是跨过程的,因此在链接时执行最方便(示例包括静态调试、静态泄漏检测 [24] 和内存管理转换 [30])。正在开发复杂的分析和转换以增强程序安全性,但必须在软件安装时或加载时进行 [19]。允许对程序进行终身重新优化,使架构师能够以更灵活的方式发展处理器和公开接口 [11, 20],同时允许遗留应用程序在新系统上运行良好。

本文介绍了 LLVM——低级虚拟机——一种编译器框架,旨在以对程序员透明的方式为任意软件提供终身程序分析和转换。 LLVM 通过两个部分实现这一点:

  • 具有几个新特性的代码表示,用作分析、转换和代码分发的通用表示;
  • 一种编译器设计,它利用这种表示来提供我们所知道的任何以前的编译方法中都不可用的功能组合。

LLVM 代码表示使用抽象的类 RISC 指令集描述程序,但具有用于有效分析的关键更高级别信息。这包括类型信息、显式控制流图和显式数据流表示(使用静态单一分配形式 [15] 中的无限类型化寄存器集)。 LLVM 代码表示中有几个新特性:

(a) 一种低级、独立于语言的类型系统,可用于从高级语言实现数据类型和操作,将它们的实现行为暴露给所有阶段优化。这种类型系统包括复杂的(但与语言无关的)技术所使用的类型信息,例如指针分析、依赖性分析和数据转换的算法。

(b) 在保留类型信息的同时执行类型转换和低级地址算术的指令。

© 两个低级异常处理指令,用于实现特定于语言的异常语义,同时向编译器显式公开异常控制流。

LLVM表示与源语言无关,有二个原因,

  1. 首先,它使用了仅比标准汇编语言稍微丰富的低级指令集和内存模型,并且类型系统不会阻止用很少的类型信息表示代码。
  2. 其次,它没有对程序强加任何特定的运行时要求或语义。

尽管如此,重要的是要注意 LLVM 并非旨在成为通用编译器 IR。特别是,LLVM 不直接表示高级语言特征(因此不能用于某些语言相关的转换),也不能捕获后端代码生成器使用的机器相关特征或代码序列(必须降低这样做)。由于不同的目标和表示,LLVM 是对高级虚拟机的补充(例如 SmallTalk [18]、Self [43]、JVM [32]、Microsoft 的 CLI [33] 等),并且不能替代这些系统。它在三个关键方面与这些不同。首先,LLVM 没有类、继承或异常处理语义等高级构造的概念,即使在编译具有这些特性的源语言时也是如此。其次,LLVM 没有指定运行时系统或特定对象模型:它足够低级,可以在 LLVM 本身中实现特定语言的运行时系统。事实上,LLVM 可用于实现高级虚拟机。第三,与物理处理器的汇编语言相比,LLVM 不能保证类型安全、内存安全或语言互操作性。 LLVM 编译器框架利用代码表示来提供五种我们认为很重要的功能的组合,以支持对任意程序的终身分析和转换。一般来说,这些能力很难同时获得,但 LLVM 设计天生就是这样做的。

(1)持久的程序信息:编译模型在应用程序的整个生命周期中保留 LLVM 表示,允许在所有阶段执行复杂的优化,包括运行时和运行之间的空闲时间。

(2) 离线代码生成:尽管有最后一点,使用不适合运行时代码生成的昂贵代码生成技术,可以离线将程序编译成高效的本地机器代码。这对于性能关键型程序至关重要。

(3) 基于用户的分析和优化:LLVM 框架在运行时在现场收集分析信息,使其代表实际用户,并且可以在运行时和空闲时将其应用于配置文件引导的转换.

(4) 透明的运行时模型:系统不指定任何特定的对象模型、异常语义或运行时环境,因此允许使用它编译任何语言(或语言组合)。

(5) 统一的全程序编译:语言独立性使得以统一的方式(链接后)优化和编译组成应用程序的所有代码成为可能,包括特定于语言的运行时库和系统库。

我们认为,以前的系统没有提供所有这五个属性。源代码级编译器提供#2 和#4,但不尝试提供#1、#3 或#5。商业编译器中常见的链接时过程间优化器 [21、5、26] 提供了 #1 和 #5 的附加功能,但仅限于链接时。静态语言的配置文件引导优化器以透明度为代价提供了好处#2,最关键的是不提供#3。 JVM 或 CLI 等高级虚拟机提供 #3 并部分提供 #1 和 #5,但不旨在提供 #4,或者根本不提供 #2,或者不提供 #1 或 #3。二进制运行时优化系统提供#2、#4 和#5,但仅在运行时且在有限程度上提供#3,最重要的是不提供#1。

我们在第 3 节中更详细地解释这些。我们从三个问题评估 LLVM 系统的有效性:

(a)表示的大小和有效性,包括提取有用类型信息的能力C程序;

(b) 编译器性能(不是取决于所使用的特定代码生成器或优化序列的生成代码的性能);

© 示例说明 LLVM 为几个具有挑战性的编译器问题提供的关键功能。

我们的实验结果表明,LLVM 编译器可以为一系列 SPECINT 2000 C 基准测试中平均 68% 的静态内存访问指令提取可靠的类型信息,并且几乎可以用于更严格的程序中的所有访问。我们还根据我们的经验讨论了 LLVM 捕获的类型信息如何足以安全地执行许多积极的转换,这些转换传统上只能在源级编译器中的类型安全语言上尝试。代码大小测量表明,LLVM 表示在大小上与 X86 机器代码(一种 CISC 架构)相当,并且平均比 RISC 代码小 25%,尽管捕获了更丰富的类型信息以及无限的寄存器集SSA 表格。最后,我们展示了显示 LLVM 表示支持极快的过程间优化的时序示例。迄今为止,我们的 LLVM 实现支持传统上完全静态编译的 C 和 C++。我们目前正在探索 LLVM 是否有助于实现 JVM 和 CLI 等动态运行时。 LLVM 在非限制性许可下免费提供 2。

本文的其余部分安排如下。

第 2 节描述了 LLVM 代码表示。

第 3 节描述了 LLVM 编译器框架的设计。

第 4 节讨论了我们对上述 LLVM 系统的评估。

第 5 节将 LLVM 与相关的先前系统进行比较。第 6 节以论文摘要结束

2.LLVM代码设计

代码表示是 LLVM 区别于其他系统的关键因素之一。该表示旨在提供有关支持复杂分析和转换所需的程序的高级信息,同时足够低级以表示任意程序并允许在静态编译器中进行广泛的优化。本节概述了 LLVM 指令集并描述了与语言无关的类型系统、内存模型、异常处理机制以及离线和内存表示。表示的详细语法和语义在 LLVM 参考手册中定义 。

2.1 LLVM 指令集概述

LLVM 指令集捕获普通处理器的关键操作,但避免了特定于机器的约束,例如物理寄存器、管道和低级调用约定。 LLVM 提供了一组无限的类型化虚拟寄存器,它们可以保存原始类型(布尔、整数、浮点和指针)的值。虚拟寄存器采用静态单一分配 (SSA) 形式 [15]。 LLVM 是一种加载/存储架构:程序仅通过使用类型化指针的加载和存储操作在寄存器和内存之间传输值。 LLVM 内存模型在第 2.3 节中描述。整个 LLVM 指令集仅包含 31 个操作码。这是可能的,因为首先,我们避免了相同操作的多个操作码3。其次,LLVM 中的大多数操作码都是重载的(例如,add 指令可以对任何整数或浮点操作数类型的操作数进行操作)。大多数指令,包括所有算术和逻辑运算,都是三地址形式的:它们采用一个或两个操作数并产生一个结果。 LLVM 使用 SSA 形式作为其主要代码表示形式,即每个虚拟寄存器都写在一个指令中,并且寄存器的每次使用都受其定义支配。 LLVM 中的内存位置不是 SSA 形式,因为许多可能的位置可能会在单个存储中通过指针进行修改,因此很难为这些位置构造一个合理紧凑、显式的 SSA 代码表示。 LLVM 指令集包括一个显式的 phi 指令,它直接对应于 SSA 形式的标准(非门控)φ 函数。 SSA 形式提供了一个紧凑的定义使用图,它简化了许多数据流优化,并支持快速、对流不敏感的算法来实现流敏感算法的许多好处,而无需进行昂贵的数据流分析。 SSA 形式的非循环变换被进一步简化,因为它们不会遇到对 SSA 寄存器的反依赖或输出依赖。非内存转换也大大简化,因为(与 SSA 无关)寄存器不能有别名。 LLVM 还使每个函数的控制流图 (CFG) 在表示中显式。一个函数是一组基本块,每个基本块是一个 LLVM 指令序列,以一个终止符指令结束(分支、返回、展开或调用;后两者将在后面解释)。每个终结器都明确指定其后续基本块。

2.2 语言无关类型信息,类型转换,

LLVM 的基本设计特征之一是包含了与语言无关的类型系统。每个 SSA 寄存器和显式内存对象都有一个关联的类型,所有操作都遵循严格的类型规则。此类型信息与指令操作码一起使用,以确定指令的确切语义(例如浮点与整数加法)。这种类型的信息可以对低级代码进行广泛的高级转换例如,参见第 4.1.1 节)。此外,类型不匹配对于检测优化器错误很有用。 **LLVM 类型系统包括具有预定义大小的与源语言无关的原始类型(void、bool、8 到 64 位的有符号/无符号整数,以及单精度和双精度浮点类型)。**这使得使用这些类型编写可移植代码成为可能,尽管不可移植代码也可以直接表示。

LLVM 还包括(仅)四种派生类型:指针、数组、结构和函数。

我们相信,大多数高级语言数据类型最终会根据它们的操作行为使用这四种类型的某种组合来表示。例如,具有继承的 C++ 类是使用结构、函数和函数指针数组实现的,如 4.1.2 节所述。同样重要的是,上面的四种派生类型甚至可以捕获复杂的独立于语言的分析和优化所使用的类型信息。例如,字段敏感的指向分析 [25, 31]、调用图构造(包括 C++ 等面向对象的语言)、聚合的标量提升和结构字段重新排序转换 [12],仅使用指针、结构、函数和原始数据类型,而数组依赖分析和循环转换使用所有这些加上数组类型。因为 LLVM 与语言无关并且必须支持弱类型语言,所以在合法 LLVM 程序中声明的类型信息可能不可靠。相反,必须使用一些指针分析算法来区分指针目标类型可靠地已知的内存访问和不可靠的内存访问。 LLVM 包括第 4.1.1 节中描述的此类分析。我们的结果表明,尽管允许将值任意转换为其他类型,但可靠的类型信息可用于编译为 LLVM 的 C 程序中的大部分内存访问。 LLVM ‘cast’ 指令用于将一种类型的值转换为另一种任意类型,并且是执行此类转换的唯一方法。因此,强制转换使所有类型转换都显式化,包括类型强制(LLVM 中没有混合类型操作)、物理子类型的显式强制转换以及非类型安全代码的重新解释强制转换。没有强制转换的程序必然是类型安全的(在没有内存访问错误的情况下,例如数组溢出 [19])。为低级代码保留类型信息的一个关键困难是实现地址算术。 LLVM 系统使用 getelementptr 指令以既保留类型信息又具有与机器无关的语义的方式执行指针运算。给定一个指向某个聚合类型的对象的类型化指针,该指令以类型保留的方式计算对象的子元素的地址(对于 LLVM 来说,实际上是组合的 ‘.’ 和 ‘[ ]’ 运算符)。例如,C 语句“X[i].a = 1;”可以翻译成一对 LLVM 指令

%p = getelementptr %xty* %X, long %i, ubyte 3;
store int 1, int* %p;

其中我们假设 a 是结构 X[i] 中的第 3 个字段,并且该结构的类型为 %xty。使所有地址算术显式化很重要,这样它才能暴露于所有 LLVM 优化(最重要的是,重新关联和冗余消除); getelementptr 实现了这一点,而不会模糊类型信息。加载和存储指令采用单个指针并且不执行任何索引这使得内存访问的处理简单而统一。

2.3 显式内存分配和统一内存模型

LLVM 提供类型化内存分配的指令。 malloc 指令在堆上分配一个或多个特定类型的元素,返回一个指向新内存的类型化指针。 free 指令释放通过 malloc4 分配的内存。 alloca 指令与 malloc 类似,只是它在当前函数的栈帧而不是堆中分配内存,并且在函数返回时自动释放内存。所有堆栈驻留数据(包括“自动”变量)都使用 alloca 显式分配。在 LLVM 中,所有可寻址对象(“左值”)都是显式分配的。全局变量和函数定义定义了提供对象地址的符号,而不是对象本身。这提供了一个统一的内存模型,其中所有内存操作(包括调用指令)都通过类型化指针发生。没有对内存的隐式访问,简化了内存访问分析,并且表示不需要“地址”运算符

2.4 函数调用和异常处理

对于普通的函数调用,LLVM 提供了一个调用指令,它接受一个类型化的函数指针(可能是一个函数名或一个实际的指针值)和类型化的实际参数。这抽象了底层机器的调用约定并简化了程序分析。

**LLVM 最不寻常的特性之一是它提供了一种显式的、低级的、独立于机器的机制来实现高级语言中的异常处理。**事实上,相同的机制还支持 C 中的 setjmp 和 longjmp 操作,允许以与其他语言中的异常功能相同的方式分析和优化这些操作。常见的异常机制基于两个指令,invoke 和 unwind。调用和展开指令在逻辑上支持基于堆栈展开的抽象异常处理模型(尽管 LLVM 到本机代码生成器可以使用“零成本”表驱动方法 [9] 或 setjmp/longjmp 来实现指令)。 invoke 用于指定在堆栈展开期间必须执行的异常处理代码。 unwind 用于抛出异常或执行 longjmp。我们首先描述这些机制,然后描述它们如何用于实现异常处理。调用指令的工作方式与调用类似,但它指定了一个额外的基本块,指示展开处理程序的起始块。当程序执行展开指令时,它会在逻辑上展开堆栈,直到它删除由调用创建的激活记录。然后它将控制转移到调用指定的基本块。这两条指令暴露了 LLVM CFG 中的异常控制流。这两个原语可用于实现多种异常处理机制。迄今为止,我们已经实现了对 C 的 setjmp/longjmp 调用的全面支持,并且对于普通的函数调用,LLVM 提供了一个调用指令,它接受一个类型化的函数指针(可能是一个函数名或一个实际的指针值)和类型化的实际参数。这抽象了底层机器的调用约定并简化了程序分析。 LLVM 最不寻常的特性之一是它提供了一种显式的、低级的、独立于机器的机制来实现高级语言中的异常处理。事实上,相同的机制还支持 C 中的 setjmp 和 longjmp 操作,允许以与其他语言中的异常功能相同的方式分析和优化这些操作。常见的异常机制基于两个指令,invoke 和 unwind。调用和展开指令在逻辑上支持基于堆栈展开的抽象异常处理模型(尽管 LLVM 到本机代码生成器可以使用“零成本”表驱动方法 [9] 或 setjmp/longjmp 来实现指令)。 invoke 用于指定在堆栈展开期间必须执行的异常处理代码。 unwind 用于抛出异常或执行 longjmp。我们首先描述这些机制,然后描述它们如何用于实现异常处理。调用指令的工作方式与调用类似,但它指定了一个额外的基本块,指示展开处理程序的起始块。当程序执行展开指令时,它会在逻辑上展开堆栈,直到它删除由调用创建的激活记录。然后它将控制转移到调用指定的基本块。这两条指令暴露了 LLVM CFG 中的异常控制流。这两个原语可用于实现多种异常处理机制。迄今为止,我们已经实现了对 C 的 setjmp/longjmp 调用的全面支持。

2.5 纯文本、二进制和内存中的表示

LLVM 表示是第一类语言,它定义了等效的文本、二进制和内存(即编译器的内部)表示。指令集被设计为有效地作为持久的离线代码表示和编译器内部表示,两者之间不需要语义转换。能够在这些表示之间转换 LLVM 代码而不会丢失信息,这使得调试转换更加简单,允许轻松编写测试用例,并减少理解内存中表示所需的时间

3. 编译器架构

LLVM 编译器框架的目标是通过在所有阶段对程序的 LLVM 表示进行操作,在链接时、安装时、运行时和空闲时实现复杂的转换。然而,为了实用,它必须对应用程序开发人员和最终用户透明,并且必须足够高效以用于实际应用程序。本节介绍整个系统如何各个组件旨在实现所有这些目标

3.1 LLVM 编译器的高级设计框架

image-20220828222718784

LLVM 系统的高级架构。简而言之,静态编译器前端以 LLVM 表示形式发出代码,这些代码由 LLVM 链接器组合在一起。链接器执行各种链接时间优化,尤其是过程间优化。然后在链接时或安装时将生成的 LLVM 代码转换为给定目标的本机代码,并将 LLVM 代码与本机代码一起保存。 (也可以在运行时使用即时翻译器翻译 LLVM 代码。)本机代码生成器插入轻量级检测以检测频繁执行的代码区域(当前循环嵌套和跟踪,但也可能是函数) ,并且这些可以在运行时进行优化。在运行时收集的配置文件数据代表最终用户(而不是开发人员)的运行,并且可以被离线优化器用于在空闲时间在现场执行积极的配置文件驱动优化,为特定目标机器量身定制。这种策略提供了五个在本地机器代码的静态编译的传统模型中不可用的好处。我们在引言中指出,这些能力对于终身分析和转换很重要,我们将它们命名为

  1. 持久化程序信息
  2. 离线代码生成
  3. 基于用户的分析和优化
  4. 透明的运行时模型
  5. 统一的全程序编译

由于至少两个原因,这些很难同时获得。首先,离线代码生成(#2)通常不允许在更高级别的表示而不是本机机器代码(#1 和#3)的后期阶段进行优化。其次,终身编译传统上只与基于字节码的语言相关联,这些语言不提供#4,通常不提供#2 或#5。事实上,我们在引言中指出,没有现有的编译方法提供上面列出的所有功能。我们的理由如下

  • 传统的源代码级编译器提供#2 和#4,但不尝试#1、#3 或#5。它们确实提供了过程间优化,但需要对应用程序 Makefile 进行重大更改。
  • 一些商业编译器通过将它们的中间表示导出到目标文件 [21、5、26] 并在链接时执行优化,在链接时提供了 #1 和 #5 的额外好处。我们所知道的没有这样的系统也能够保留其表示以供运行时或空闲时间使用(好处 #1 和 #3)。
  • 像 JVM 和 CLI 这样的高级虚拟机提供了第 3 条好处,并部分提供了第 1 条好处(特别是,它们专注于运行时优化,因为字节码验证的需要极大地限制了可能在运行时之前完成的优化[3])。 CLI 部分提供了#5,因为它可以支持多种语言的代码,但任何低级系统代码和不合格语言的代码作为“非托管代码”执行。此类代码以本机形式而不是 CLI 中间表示形式表示,因此不会暴露于 CLI 优化。这些系统不提供#2 和#1 或#3,因为运行时优化通常只有在使用JIT 代码生成时才有可能。他们的目标不是提供#4,而是为与其运行时和对象模型相匹配的语言(例如 Java 和 C#)提供丰富的运行时框架。 Omniware [1] 提供了 #5 和 #2 的大部分好处(因为,与 LLVM 一样,它使用允许广泛静态优化的低级表示),但代价是不提供高级分析的信息和优化(即#1)。它的目的不是提供#3 或#4
  • 透明的二进制运行时优化系统(如 Dynamo)和 Transmeta 处理器中的运行时优化器提供了 #2、#4 和 #5 的好处,但它们不提供 #1。它们仅在运行时提供第三个好处,并且仅在有限的程度上提供,因为它们仅适用于本机二进制代码,限制了它们可以执行的优化。
  • 静态语言的配置文件引导优化提供了好处#3,但代价是不透明(它们需要多阶段编译过程)。此外,PGO 还存在三个问题:(1)根据经验,开发人员不太可能使用 PGO,除非在编译基准测试时。 (2) 当使用 PGO 时,应用程序会根据训练运行的行为进行调整。如果训练运行不能代表最终用户的使用模式,性能可能不会提高,甚至可能会受到配置文件驱动的优化的影响。 (3) 分析信息是完全静态的,这意味着编译器无法利用程序中的阶段行为或适应不断变化的使用模式

LLVM 策略也有很大的局限性。首先,在生成 LLVM 代码之前,必须在前端执行特定于语言的优化。 LLVM 并非旨在直接表示源语言类型或功能。其次,需要复杂运行时系统(如 Java)的语言是否可以直接从 LLVM 中受益,这是一个悬而未决的问题。我们目前正在探索在 LLVM 之上实现更高级别的虚拟机(例如 JVM 或 CLI)的潜在好处。下面的小节描述了 LLVM 编译器架构的关键组件,强调了使上述功能实用和高效的设计和实现特性。

3.2 编译时:外部前端和静态优化

外部静态 LLVM 编译器(称为前端)将源语言程序转换为 LLVM 虚拟指令集。每个静态编译器可以执行三个关键任务,其中第一个和第三个是可选的:

(1) 执行特定于语言的优化,例如,优化具有高阶函数的语言中的闭包。

(2) 将源程序翻译成 LLVM 代码,尽可能多地合成有用的 LLVM 类型信息,尤其是暴露指针、结构和数组。

(3) 在模块级别调用 LLVM 通道以进行全局或过程间优化。

LLVM 优化内置于库中,使前端可以轻松使用它们。前端不必执行 SSA 构造。相反,可以在堆栈上分配变量(不是 SSA 形式),并且可以使用 LLVM 堆栈提升和标量扩展传递有效地构建 SSA 形式。如果堆栈分配的标量值没有转义当前函数,则堆栈提升会将堆栈分配的标量值转换为 SSA 寄存器,并根据需要插入 φ 函数以保留 SSA 形式。标量扩展在此之前并尽可能将局部结构扩展为标量,以便它们的字段也可以映射到 SSA 寄存器。请注意,许多“高级”优化实际上并不依赖于语言,并且通常是可以在 LLVM 代码上执行的更通用优化的特殊情况。例如,面向对象语言的虚函数解析(在第 4.1.2 节中描述)和对函数语言至关重要的尾递归消除都可以在 LLVM 中完成。在这种情况下,最好扩展 LLVM 优化器来执行转换,而不是将精力投入到只对特定前端有益的代码上。这也允许在程序的整个生命周期中执行优化

3.3 链接器和过程间优化器

链接时间是编译过程的第一阶段,其中大部分程序都可用于分析和转换。因此,链接时是在整个程序中执行积极的过程间优化的自然场所。 LLVM 中的链接时优化直接对 LLVM 表示进行操作,利用它包含的语义信息。 LLVM 目前包括许多过程间分析,例如上下文敏感的指向分析(数据结构分析 [31])、调用图构造和 Mod/Ref 分析,以及内联、死全局等过程间转换消除、死参数消除、死类型消除、常量传播、数组边界检查消除 [28]、简单结构字段重新排序和自动matic池分配[30]。

LLVM 中编译和链接时优化器的设计允许使用众所周知的技术来加速过程间分析。在编译时,可以为程序中的每个函数计算过程间摘要,并将其附加到 LLVM 字节码。然后,链接时过程间优化器可以将这些过程间摘要作为输入进行处理,而不必从头开始计算结果。当修改少量翻译单元时,这种技术可以显着加快增量编译 [7]。请注意,这是在不建立程序数据库或将输入源代码的编译推迟到链接时完成的情况下实现的

3.4 离线或 JIT 本机代码生成

在执行之前,使用代码生成器以两种方式之一将 LLVM 转换为目标平台的本地代码(我们目前支持 Sparc V9 和 x86 架构)。在第一个选项中,代码生成器在链接时或安装时静态运行,以使用可能昂贵的代码生成技术为应用程序生成高性能本机代码。如果用户决定使用链接后(运行时和离线)优化器,程序的 LLVM 字节码副本将包含在可执行文件本身中。此外,代码生成器在程序中插入轻量级工具,以识别频繁执行的代码区域。或者,可以使用即时执行引擎,它在运行时调用适当的代码生成器,一次转换一个函数以执行(或者如果没有可用的本地代码生成器,则使用便携式 LLVM 解释器)。 JIT 转换器还可以插入与离线代码生成器相同的工具。

3.5 运行时路径分析和重新优化

LLVM 项目的目标之一是为普通应用程序的运行时优化开发一种新策略。尽管这项工作超出了本文的范围,但我们简要描述了该策略及其主要优势。当程序执行时,最常执行的执行路径通过离线和在线检测的组合来识别[39]。离线工具(由本机代码生成器插入)识别代码中频繁执行的循环区域。当在运行时检测到热循环区域时,运行时检测库检测正在执行的本机代码,以识别该区域内频繁执行的路径。一旦识别出热路径,我们将原始 LLVM 代码复制到跟踪中,对其执行 LLVM 优化,然后将本机代码重新生成到软件管理的跟踪缓存中。然后我们在原始代码和新的本地代码之间插入分支。此处描述的策略非常强大,因为它结合了以下三个特征:

(a) 可以使用复杂算法提前执行本机代码生成,以生成高性能代码。

(b) 本机代码生成器和运行时优化器可以一起工作,因为它们都是 LLVM 框架的一部分,允许运行时优化器利用代码生成器的支持(例如,用于检测和简化转换)。

© 运行时优化器可以使用来自 LLVM 表示的高级信息来执行复杂的运行时优化

我们相信这三个特征共同代表了运行时优化器的一个“最佳”设计点,因为它们允许在三个关键方面做出最佳选择:高质量的初始代码生成(离线而不是在线),代码生成器的协作支持,以及执行复杂分析和优化的能力(使用 LLVM 而不是本机代码作为输入)

3. 6 使用 终端用户信息进行离线重新优化

由于 LLVM 表示被永久保留,它可以在最终用户系统的空闲时间对应用程序进行透明的离线优化。这样的优化器只是链接时过程间优化器的修改版本,但更加强调配置文件驱动和目标特定的优化。离线、空闲时间重新优化器有几个关键优势。首先,如前所述,与传统的配置文件引导优化器(即编译时或链接时优化器)不同,它可以使用从应用程序的最终用户运行收集的配置文件信息。它甚至可以多次重新优化应用程序以响应随时间变化的使用模式(或针对具有不同模式的用户进行不同的优化)。其次,它可以根据单个目标机器的详细特性定制代码,而传统的二进制代码分发通常必须在具有兼容架构和操作系统的许多不同机器配置上运行。第三,与运行时优化器(具有上述两个优点)不同,它可以执行更积极的优化,因为它是离线运行的。尽管如此,运行时优化可以进一步提高性能,因为它能够基于运行时值执行优化以及路径敏感的优化(如果积极离线完成,可能会导致显着的代码增长),并自适应地优化代码以适应变化运行中的执行行为。因此,对于动态的、长时间运行的应用程序,运行时和离线重新优化器可以协调以确保可实现的最高性能。

4. 应用和经验

第 2 节和第 3 节描述了 LLVM 代码表示和编译器架构的设计。在本节中,我们根据三类问题评估此设计:(a)表示的特征; (b) 在编译器中执行整个程序分析和转换的速度; © LLVM 系统在具有挑战性的编译器问题中的说明性使用,重点关注 LLVM 中的新功能如何使这些使用受益

暂未进行~

5. 相关工作

我们专注于将 LLVM 与之前的三类工作进行比较:其他基于虚拟机的编译器系统、类型化汇编语言的研究以及链接时间或动态优化系统。如引言中所述,LLVM 的目标与 SmallTalk、Self、JVM 和 Microsoft CLI 的托管模式等高级语言虚拟机的目标是互补的。诸如此类的高级虚拟机需要特定的对象模型和运行时系统才能使用。这意味着它们可以提供有关程序的更高级别的类型信息,但不能支持与其设计不匹配的语言(即使是面向对象的语言,如 C++)。此外,这些表示形式的程序(CLI 除外)必须是类型安全的。这对于支持移动代码很重要,但是对于非类型安全语言和低级系统代码来说,这些虚拟机是不够的。由于需要字节码验证,它还极大地限制了在运行前可以完成的优化量。 Microsoft CLI 虚拟机具有许多区别于其他高级虚拟机的功能,包括对多种语言的广泛功能的显式支持、语言互操作性支持、非类型安全代码和“非托管”执行模式。非托管模式允许 CLI 以任意语言表示代码,包括那些不符合其类型系统或运行时框架的代码,例如 ANSI 标准 C++ [34]。但是,非托管模式下的代码不在 CLI 中间表示 (MSIL) 中表示,因此不受 CLI 中动态优化的影响。相比之下,LLVM 允许来自任意语言的代码以统一、丰富的表示形式表示,并在代码的整个生命周期内进行优化。第二个关键区别是 LLVM 缺乏 CLI 的互操作性特性,但也不需要源语言来匹配运行时和对象模型以实现互操作性。相反,它需要源语言编译器来管理互操作性,但随后允许所有此类代码在所有阶段暴露给 LLVM 优化器。 Omniware 虚拟机 [1] 更接近 LLVM,因为它们使用抽象的低级 RISC 架构,并且可以支持来自任何源语言的任意代码(包括非类型安全代码)。然而,Omniware 指令集缺少 LLVM 的高级类型信息。事实上,它允许(并要求)源编译器选择数据布局、执行地址算术和执行寄存器分配(到一小组虚拟寄存器)。所有这些特性使得对生成的 Omniware 代码执行任何复杂的分析变得困难。之所以出现这些与 LLVM 的差异,是因为他们的工作目标主要是提供代码移动性和安全性,而不是终身代码优化的基础。

Kistler 和 Franz 描述了一种在现场执行优化的编译架构,使用简单的初始加载时代码生成,然后是配置文件引导的运行时优化 [27]。他们的系统以 Oberon 语言为目标,使用 Slim Binaries [22] 作为其代码表示,并提供类似于其他高级虚拟机的类型安全和内存管理。它们不会像 LLVM 那样尝试支持任意语言或使用透明的运行时系统。他们也不建议进行静态或链接时优化

关于类型化的中间表示已经进行了广泛的工作。函数式语言通常使用强类型中间语言(例如 [38])作为源语言的自然扩展。关于类型化汇编语言的项目(例如,TAL [35] 和 LTAL [10])专注于在编译和优化期间保留高级类型信息和类型安全性。 SafeTSA [3] 表示是类型信息与 SSA 形式的组合,旨在为 Java 程序提供比 JVM 字节码更安全但更有效的表示。相比之下,LLVM 虚拟指令集并不试图保护高级语言的类型安全,从这些语言中捕获高级类型信息,或者直接强制执行代码安全(尽管它可以用来这样做[ 19])。相反,LLVM 的目标是在静态编译时间之外实现复杂的分析和转换

已经尝试定义统一的、通用的、中间表示。这些在很大程度上都失败了,从最初的通用计算机导向语言 [42] (UNCOL) 到最近的架构和语言中性分布格式 [4] (ANDF),它被讨论过但从未实现,已实施但使用有限。这些统一的表示试图通过包含来自所有支持的源语言的特性来描述 AST 级别的程序。 LLVM 远没有那么雄心勃勃,更像是一种汇编语言:它使用一小组类型和低级操作,并根据这些类型来描述高级语言功能的“实现”。在某些方面,LLVM 只是简单地表现为严格的 RISC 架构几个系统在链接时执行过程间优化。有些对给定处理器的汇编代码进行操作 [36,41,14,37](主要关注与机器相关的优化),而另一些则以 IR 或注释的形式从静态编译器导出附加信息 [ 44、21、5、26]。这些方法都没有尝试支持在运行时或在现场安装软件后离线优化,并且很难直接扩展它们来做到这一点。还有几个系统可以对本机代码执行透明的运行时优化[6,20,16]。这些系统继承了优化机器级代码的所有挑战[36],除了在运行时优化的严格时间约束下操作的约束。相比之下,LLVM 旨在提供类型、数据流 (SSA) 信息和显式 CFG,以供运行时优化使用。例如,我们的在线跟踪框架(第 3.5 节)在运行时直接利用 CFG 来执行热循环区域的有限检测。最后,这些系统都不支持链接时间、安装时间或离线优化,有或没有个人资料信息

6. 结论

本文描述了 LLVM,一个用于执行终身代码分析和转换的系统,同时对程序员保持透明。该系统使用低级、类型化、基于 SSA 的指令集作为程序的持久表示,但不强加特定的运行时环境。 LLVM 表示与语言无关,允许程序的所有代码,包括系统库和以不同语言编写的部分,一起编译和优化。 LLVM 编译器框架旨在允许在软件生命周期的所有阶段进行优化,包括广泛的静态优化、使用来自 LLVM 代码的信息的在线优化以及使用从程序员那里收集的配置文件信息的空闲时间优化。场地。当前的实现包括强大的链接时全局和过程间优化器、用于运行时优化的低开销跟踪技术以及即时和静态代码生成器。我们通过实验和基于经验表明,LLVM 甚至可以为 C 程序提供广泛的类型信息,这些信息可用于安全地执行一些通常仅在源代码级编译中的类型安全语言上尝试的积极转换。厄。我们还展示了 LLVM 表示在大小上与 X86 机器代码相当,并且平均比 SPARC 代码小 25%,尽管它捕获了更丰富的类型信息以及 SSA 形式的无限寄存器集。最后,我们给出了几个在 LLVM 表示上执行非常有效的全程序优化示例。我们目前正在探索的一个关键问题是高级语言虚拟机是否可以在 LLVM 运行时优化和代码生成框架之上有效实现

猜你喜欢

转载自blog.csdn.net/sexyluna/article/details/129222886