3 LLVM 虚拟机指令集
LLVM 系统架构采用积极的持续的系统优化,产生高性能可执行文件。LLVM 与其它系统的一个关键不同是它所采用的程序表现。这样的程序表现,首先必须足够低级,允许编译的早期各个阶段进行大量优化;同时又要足够的高级,可以支持积极的链接时和链接后优化。
LLVM 虚拟指令集,是具有高级类型信息的低级表现,提供广泛的独立于语言的类型信息,把内存分配直接暴露给编译器,并且提供同一的抽象。本章讨论 LLVM 虚拟指令集的主要特性,每个指令的具体语法和语义可以参考 LLVM 引用手册。
3.1 LLVM虚拟指令集概览
LLVM 指令集代表一个虚拟架构,用来捕获常规处理器的关键操作,同时避免特定机器的限制,如物理寄存器、管线、低级调用规范、陷阱等。LLVM 提供无限数量的类型化的虚拟寄存器,用来存储基础类型的值,如整形、浮点型、指针类型。虚拟寄存器采用静态单赋值形式[15](SSA),它广泛地用于编译器的优化表示,可参考3.2.1。 LLVM 类型系统在 3.3 中有更详细的介绍。LLVM 虚拟指令集,使用独特的方式,显示的表现异常控制流,可参考 3.5。
LLVM 程序在虚拟寄存器和内存之间,仅靠load 和 store 操作,使用类型指针的方式针传输值。内存被划分为全局区域、栈、堆(过程被视为全局对象)。 栈、堆上的对象分别使用 alloca 和 malloc 指令,并通过这两个操作返回的指针值来访问。栈对象在当前函数的栈帧中分配,当控制流(线程)离开函数时自动释放。堆对象必须使用 free 指令显示释放。这些操作的动机和实现可参考 3.4。
LLVM 是一个虚拟指令集,没有定义运行时和操作系统函数,如 I/O、内存管理、signals等等。这些特性由运行时库和 API 提供,程序可链接它们,同时 LLVM 虚拟指令集是一阶语言,有文本、二进制、内存表示。该决策的隐含意义可参考 3.6。
3.2 三地址码
三地址码早已经成为,RISC 架构和语言独立的编译器优化,选择的表现形式。在核心理念上,它与机器码非常接近,具有少数的简单且正交 (orthogonal)操作。三地址码非常易压缩,允许高密度的 LLVM 文件。
大多数 LLVM 操作,包括所有的算术和逻辑操作,都使用三地址形式:接收一个或两个操作数,产生单一的结果。LLVM 提供非常标准且正交的算术和逻辑操作集:add, sub, mul, div, rem, not, and, or, xor, shl, shr, and setcc。setcc 是比较指令的一个集合,如 seteq, setlt 等,其返回值都是 boolean 类型。
除了简单的二操作数指令(binary instructions 二操作数 VS 二进制**),一些指令接收 0 个、3 个或是可变数量的操作数,**如 call 指令、和用来以 SSA 形式表现代码的 phi 指令。
The LLVM Instruction Set and Compilation Strategy
没有一元运算符, not 和 neg 使用 xor 和 sub 实现
LLVM 指令是多态的:单个指令如 add,可以操作几种不同类型的操作数,该策略大幅降低了操作符的数量。操作数的类型会自动定义操作的语义和结果的类型,操作数遵循严格的类型规则。
下例展示几个简单的 LLVM 操作:
其中类型信息决定,有符号除法还是无符号除法,有符号比较操作还是无符号比较操作。
3.2.1 静态单赋值形式
LLVM 使用静态单赋值(SSA)形式作为器主要的代码表现。如果程序的变量只定义一次,只在定义时初始化一次,这样的程序是 SSA 的。SSA 极大的简化了数据流优化,因为所有变量的单次定义决定了其所有的属性。无需昂贵的数据流分析(稀疏属性),SSA 允许快速流不敏感算法,取得流敏感算法的许多优点。
flow-insensitive algorithms (sparseness property)
单一定义属性的一个隐含特性是,每个计算值的指令会隐式的创建一个新的虚拟寄存器,存储新值,如 add int %x, %y 。可以主动用一个显示名称,如 %z,来保存该只,否则系统会自动使用一个唯一的名称保持该值。该属性使 LLVM 可以**直接使用计算该值的操作,使高效的 **def-use 信息成为可能。
当考虑控制流时,简单变量重命名不足以让代码成为有效的 SSA 形式。为了处理控制流合并,SSA 形式定义了φ函数,根据控制流来自于那个基本块,来选择一个输入值。LLVM 提供 phi 指令,对应 SSA φ-node,语法为:
<result> = phi <type> [<val0>, <label0>], ... , [<valN>, <labelN>]
Figure 3.2 shows an example function which requires φ nodes.
含义为,如果控制流从基本块 label0 到达,则把 val0 的值赋给 result,以此类推。基本块中的所有 phi 指令,必须出现在基本块的开始。下图 3.2 中的函数,需要 φ函数:
如前所述,LLVM 虚拟寄存器采用 SSA 形式,而内存中的值不采用。因为标号不会有别名,,这极大地简化了转换。
= phi [ , ], ...
phi 是指令名称,φ函数;ty 类型名称,这里是 int 类型;后面的多个 [ , ] 表示特定分支来源 labeli 下,%result 会赋值为 vali 。比如在第一次进入Loop时,%result = 1,%i = 0,之后的所有迭代 %result = %result2,%i = %i2。
3.3 高级类型信息
LLVM 采用严格的类型表现,每个 SSA 值和内存位置都有一个关联类型,且所有的操作都遵循严格的类型规则。该类型信息,允许在低级码上进行广泛的高级转换。另外在优化中,LLVM 一致性检查器,可使用类型不匹配来检测错误。
LLVM 类型系统包含语言独立的基础类型 (void、bool、signed、unsigned integers from 8 to 64 bits, floating-point values in single and double precision、opaque) 和构造类型pointers、arrays、structures、functions)。这些类型都是从高级语言类型映射来且语言独立的数据表现。如 C++ 中类的继承和虚拟方法,可以使用结构体来表现其数据类型,使用带有间接函数调用的类型化函数表表现继承。 这允许很多高级的语言独立优化,如虚函数决议,在 LLVM 码上执行。下面图 3.4 说明 LLVM 类型:
所有的 LLVM 指令都具有严格类型限制,限制其操作数以简化转换,并保持类型正确性。例如下图 3.5 中: add 指令,要求其两个操作数必须类型一致,integral 或 floating-point,并产生一个同类型的返回值;load 指令,需要你个指针操作数,作为加载源);store 指令,把某类型 τ 的值,存入 τ* 内存中。图 3.5 显示了一些错误的代码示范:
使用类型信息,可以轻松地,从低级代码中,提取出高级信息。为此,必须保持内存数据域的访问是类型安全的。为此, LLVM 引入了 getelementptr,用以维护类型安全。
3.3.1 使用 getelementptr 指令进行类型安全指针算术
getelementptr 指令用来以类型安全的方式,计算聚合数据结构的子元素的地址。给定一个指向数据结构的指针和一个域号,getelementptr 指令将产生一个到该域的指针。同理数组类型。
下面是 C 前端产生的 LLVM 代码(⚠️s是一个链表,取第1号元素):
= getelementptr , * {, [inrange] }*
3.3.2 区分安全和不安全代码:cast 指令
有两个进行类型转换的原因:显示转换一个值的类型,这时可能不需要操作类型;或把数据重新解读为另外一个类型。在 LLVM 必须使用 cast 指令进行类型转换,如:
LLVM 作为通用目低级指令集,对任何高级语言,需要同时表现 **“type-safe” 和 “type-unsafe’ 程序,只有 **“type-safe” 操作数可以进行面向内存的优化。
LLVM 程序认为满足以下条件为类型安全:没有非指针类型到指针类型的转换指令、没有类型的指针到其它类型的指针的转换指令。图 3.8 中最后两个转换是不安全的。
如果一个程序是类型安全的,则可以利用类型信息进行分析(如别名分析)和安全地进行数据结构重组转换。如果程序是完全类型安全的,LLVM 代码可以**对指针算术使用 **getelementptr 指令,且无需要求任何不安全映射。如带有指针算术的某语言,naive 编译可能会导致类型冲突可参考 图 3.9:
在最后的语句中的指针算术,可编译为图 3.10 的 LLVM 代码:
%P2 映射是不安全的,任意值都可以被映射成指针。可以使用 getelementptr 指令代替换:
在实践中,根据上述定义,**大多数 **C 程序是完全的或大多数情况下类型安全的,并且可以通过简单转换消除掉多数的不安全映射操作。然而,某些程序必须使用不安全操作,如把整数映射成指针或把内存映射硬件设备的地址映射为指针,不能使用 getelementptr 指令进行类型转换。在这些情况中,当类型系统被违背时,LLVM 中的 cast 指令会提供关键的信息,用以提升分析并允许直接确定转换是否安全。
3.4 显式内存分配和同一内存模型
一些难以充分优化的程序是内存绑定程序,它们使用大量的堆上的复杂数据结构。 为了更好的分析编译器内存分配模式,给指令集中添加了类型化的内存分配指令。malloc 指令在堆上分配一个或是多特定类型的元素,返回一个指向分配内存的类型化的指针。free 指令释放 malloc 指令分配的内存。alloca 指令同 malloc 一样,但是它在当前函数的栈上分配空间,当函数返回时自动释放。
这些指令都是保证表现的类型安全的必要条件,并允许新的转换如 4.2 中的数据类型分析,和 4.3 中的自动释放池。即使对于非类型安全的编程语言例如C,这些积极的技术也是安全的。
LLVM 虚拟指令集处理内存的方式也非常的独特。在 LLVM 中,所有的可寻址对象(栈上、全局、函数、堆上)都是显示的分配的,同一内存模型。栈分配的本地对象(“automatic” variables and source-level alloca() calls)使用 alloca 指令;在Heap 上使用 malloc 指令分配内存;函数和全局变量声明静态分配的内存区域,并通过对象地址访问(全局值的名称会引用地址)。
使用地址访问内存对象的一个有意思的效果是,LLVM 根本不需要 “address-of” 操作符,且无法隐式访问内存,简化了内存访问分析。所有的内存访问都通过 load 和 store 指令执行。
3.5 函数调用和异常处理
LLVM 提供两个函数调用指令,抽象底层机器的调用规范,简化程序分析,提供异常处理支持。简单的 call 指令,接受一个函数指针和对应的传值参数,来调用函数。invoke 指令,被用于带有析构器的语言,进行异常处理。
LLVM 为“零成本”异常处理实现了堆栈展开机制[9]。“零成本”异常处理模型表明,在不抛出异常时,异常处理不会导致程序执行额外的指令。如抛出异常,堆栈将展开,即逐个遍历堆栈上函数调用的返回地址。LLVM运行时会保存返回地址到异常处理程序块的静态映射,在堆栈展开期间,调用这些异常处理程序块。
[9] David Chase. Implementation of exception handling, Part I. The Journal of C Language Translation, 5(4):229–240, June 1994.
为了构建具柄信息的静态映射,LLVM 提供了invoke 指令,接受一个异常具柄标号、函数指针、参数操作数。产生代码时,把invoke 指令的返回地址与异常具柄标号关联起来,当栈帧展开时调用异常处理历程或是清理历程。
LLVM 中的 invoke 指令,通过使用低级概念(返回映射中具柄的地址),可以直接表现高级异常,这使得 LLVM 独立于源语言的异常处理语意。使用这种表示,可直接指定异常的边缘且对LLVM 框架可见,遇到异常时,确保所有 LLVM 转换都是正确的。图 3.12 说明了 C++ 前端产生的 invoke 指令的一个例子:
这里的一个非常简单的关键点是,当代码块退出时(指函数出栈),C++确保所有栈上分配的对象的析构器被调用。如果 func() 调用抛出了异常,代码块将会退出,必须设置一个具柄用来调用对象析构器。图 3.13 显示了图 3.12 的 LLVM 代码:
invoke 指令和一饿异常具柄关联,如果调用函数时异常被抛出,调用该具柄处理异常。在下图3.1.3 中,观察本地对象析构器的调用及其异常具柄。在 java 语言中(栈展开时不用调用析构器),invoke 指令被用来解锁。在任何语言中,可以使用异常目标术语来实现 catch 语句。
虽然目前没有任何前端使用 LLVM 内置的异常具柄,所有的优化和转变都可意识到异常控制流的边界,单元测试也可正常工作。我们也计划使用这一工具来实现 C 中的setjmp 和 longjmp 。
3.6 纯文本、字节码和内存表示法
LLVM虚拟指令集是统一第2章所述系统设计的粘合剂。 因此,系统的有效性和易用性,依赖指令集设计的诸多方面。LLVM 虚拟指令集的一个重要的特性是: 它是一级语言,采用文本格式(本文档中包含了一些示例)、压缩的二进制格式、适合转换的内存格式。
LLVM 码能够在这些表现直接转换同时不丢失信息,这使得对转换的调试特别简单,非常容易的写测试用例,降低需要理解内存表现的时间。
资料
juejin.cn/post/687567… 第二章(翻译)
LLVM- An Infrastructure for Multi-Stage Optimization 翻译原文,LATTNER 的硕士论文
pllab.cs.nthu.edu.tw/cs340402/le…** 推荐阅读**
developer.aliyun.com/article/727… Getting Started with LLVM Core Libraries 的翻译,推荐阅读
book Getting Started with LLVM Core Libraries ,推荐阅读,第三章 Getting to know LLVM's basic libraries 和第五章 The LLVM Intermediate Representation 对IR的综合阐述
llvm.org/docs/Writin… 推荐阅读与实践
llvm.org/docs/Progra… 编码手册 实践必读
llvm.org/docs/Coding… 编码标准 实践必读