LLVM-精要-全-
LLVM 精要(全)
原文:
zh.annas-archive.org/md5/bfaacf5bfdae70f660abe9e9b6bb3615译者:飞龙
前言
LLVM 是近年来非常热门的话题之一。它是一个开源项目,拥有越来越多的贡献者。每个程序员在编程过程中都会遇到编译器。简单来说,编译器将高级语言转换为机器可执行代码。然而,在底层进行的是大量的复杂算法。因此,要开始学习编译器,LLVM 将是研究的最简单的基础设施。用面向对象的 C++编写,设计模块化,概念易于映射到理论,LLVM 对经验丰富的编译器程序员和愿意学习的新手学生都具有吸引力。
作为作者,我们坚持认为,简单的解决方案通常比复杂的解决方案更有效,也更易于理解。在整本书中,我们将探讨各种有助于提升你的技能并驱使你深入学习更多知识的话题。
我们还相信,这本书对那些不直接参与编译器开发的人也会有所帮助,因为编译器开发的知识将帮助他们编写更优化的代码。
本书涵盖内容
第一章, 玩转 LLVM,介绍了 LLVM 的模块化设计和 LLVM 中间表示。在这一章中,我们还探讨了 LLVM 提供的一些工具。
第二章, 构建 LLVM IR,介绍了 LLVM 基础设施提供的某些基本函数调用,用于构建 LLVM IR。本章演示了使用 LLVM API 构建模块、函数、基本块、条件语句和循环。
第三章, 高级 LLVM IR,介绍了某些高级 IR 范式。本章向读者解释了高级 IR,并展示了如何使用 LLVM 函数调用在 IR 中生成它们。
第四章, 基本 IR 变换,讨论了使用 LLVM 优化工具 opt 和 LLVM Pass 基础设施在 IR 级别进行的基本变换优化。你将学习如何在一个 Pass 中使用另一个 Pass 的信息,然后探讨指令简化与指令组合 Pass。
第五章, 高级 IR 块变换,讨论了在 IR 块级别上的优化。我们将讨论各种优化,如循环优化、标量演化、向量化等,随后对本章进行总结。
第六章, IR 到选择 DAG 阶段,带您了解目标无关代码生成器的抽象基础设施。我们探讨了 LLVM IR 如何转换为选择 DAG 以及随后的各个阶段。它还介绍了指令选择、调度、寄存器分配等内容。
第七章, 为目标架构生成代码,向读者介绍了 tablegen 概念。它展示了如何使用 tablegen 表示目标架构规范,例如寄存器集、指令集、调用约定等,以及如何使用 tablegen 的输出为特定架构生成代码。本章可以作为读者启动目标机器代码生成器的参考。
您需要本书的以下内容
完成本书中的大多数示例,您只需要一台 Linux 机器,最好是 Ubuntu。您还需要一个简单的文本或代码编辑器、互联网接入和浏览器。我们建议安装 meld 工具来比较两个文件;它在 Linux 平台上表现良好。
适用于本书的读者
本书旨在为那些已经了解一些编译器概念的人而写,希望他们能快速熟悉 LLVM 的基础设施和它提供的丰富库集。对于熟悉编译器概念并希望在他们的工作中以有意义的方式深入了解、探索和使用 LLVM 基础设施的编译器程序员来说,本书将是有用的。
本书也适用于那些虽然没有直接参与编译器项目,但经常参与编写数千行代码的开发阶段的程序员。了解编译器的工作原理后,他们将以最佳方式编码,并通过编写干净的代码来提高性能。
习惯用法
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"LLVM Pass Manager 使用显式提到的依赖信息。"
代码块如下设置:
int add(int a) {
return globvar + a;
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
Value *StartVal = Builder.getInt32(1);
Value *Res = createLoop(Builder, List, VL, StartVal, Arg2);
Builder.CreateRet(Res);
任何命令行输入或输出如下所示:
$ clang -emit-llvm -c -S add.c
$ cat add.ll
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"点击下一步按钮将您带到下一屏幕。"
注意
警告或重要注意事项如下所示。
小贴士
小技巧和窍门看起来是这样的。
读者反馈
我们始终欢迎读者的反馈。请告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们来说非常重要,因为它帮助我们开发出你真正能从中获得最大收益的书籍。
要发送一般性反馈,请简单地发送电子邮件到 <feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。
如果你在一个你具有专业知识的主题上,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件,网址为 www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接发送给你。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。这样做可以让你帮助其他读者避免挫败感,并帮助我们改进这本书的后续版本。如果你发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择你的书籍,点击错误提交表单链接,并输入你的错误详细信息来报告它们。一旦你的错误清单得到验证,你的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。
要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在错误清单部分。
盗版
互联网上对版权材料的盗版是一个持续存在的问题,所有媒体都存在这个问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上发现任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢你在保护我们的作者和为你提供有价值内容的能力方面提供的帮助。
问题
如果你对这本书的任何方面有问题,你可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 玩转 LLVM
LLVM 编译器基础设施项目始于 2000 年的伊利诺伊大学,最初是一个研究项目,旨在为任意静态和动态编程语言提供基于现代、SSA 的编译技术。现在它已经发展成为一个包含许多子项目的母项目,提供了一组具有良好定义接口的可重用库。
LLVM 使用 C++ 实现,其核心是它提供的 LLVM 核心库。这些库为我们提供了 opt 工具,即目标无关的优化器,以及针对各种目标架构的代码生成支持。还有其他一些工具使用了核心库,但本书的主要关注点将是上述提到的三个。这些工具围绕 LLVM 中间表示(LLVM IR)构建,几乎可以映射所有高级语言。所以基本上,要使用 LLVM 的优化器和代码生成技术来处理某种编程语言编写的代码,我们只需要编写一个前端,该前端将高级语言转换为 LLVM IR。对于 C、C++、Go、Python 等语言,已经有许多前端可用。在本章中,我们将涵盖以下主题:
-
模块化设计和库集合
-
熟悉 LLVM IR
-
使用命令行使用 LLVM 工具
模块化设计和库集合
关于 LLVM 最重要的是它被设计为一个库集合。让我们通过 LLVM 优化器 opt 的例子来理解这些。优化器可以运行许多不同的优化过程。每个过程都作为从 LLVM 的 Pass 类派生出的 C++ 类来编写。每个编写的过程都可以编译成一个 .o 文件,随后它们被归档到一个 .a 库中。这个库将包含 opt 工具的所有过程。这个库中的所有过程都是松散耦合的,也就是说,它们明确地说明了对其他过程的依赖。
当优化器运行时,LLVM PassManager 使用明确提到的依赖信息,并以最佳方式运行过程。基于库的设计允许实现者选择过程的执行顺序,也可以根据需求选择要执行的过程。只有所需的过程会被链接到最终的应用程序,而不是整个优化器。
下图展示了每个过程如何链接到特定库中的特定对象文件。在下面的图中,PassA 引用了 LLVMPasses.a 以便为 PassA.o,而自定义过程则引用了不同的库 MyPasses.a 以便为 MyPass.o 对象文件。

代码生成器也像优化器一样利用这种模块化设计,将代码生成分割成单独的传递,即指令选择、寄存器分配、调度、代码布局优化和汇编输出。
在以下提到的每个阶段中,几乎每个目标都有一些共同点,例如为虚拟寄存器分配物理寄存器的算法,尽管不同目标的寄存器集合各不相同。因此,编译器编写者可以修改上述提到的每个传递,并创建自定义的目标特定传递。使用tablegen工具通过特定架构的表格描述.td文件来实现这一点。我们将在本书的后面讨论这是如何发生的。
从这一点产生的另一个能力是能够轻松地将错误定位到优化器中的特定传递。一个名为Bugpoint的工具利用这一能力来自动缩减测试用例并定位导致错误的传递。
熟悉 LLVM IR
LLVM 中间表示(IR)是 LLVM 项目的核心。一般来说,每个编译器都会生成一个中间表示,在其上运行大多数优化。对于针对多种源语言和不同架构的编译器,在选择 IR 时的重要决策是它既不应非常高级,如非常接近源语言,也不应非常低级,如接近目标机器指令。LLVM IR 旨在成为一种通用的 IR,通过足够低的级别,使得高级思想可以干净地映射到它。理想情况下,LLVM IR 应该是目标无关的,但由于某些编程语言本身固有的目标依赖性,它并非如此。例如,当在 Linux 系统中使用标准 C 头文件时,头文件本身是目标相关的,它可能指定一个特定类型给实体,以便它与特定目标架构的系统调用相匹配。
大多数 LLVM 工具都围绕这个中间表示展开。不同语言的接口生成这种 IR 从高级源语言。LLVM 的优化器工具运行在这个生成的 IR 上以优化代码以获得更好的性能,代码生成器利用这个 IR 进行目标特定的代码生成。这种 IR 有三种等效形式:
-
内存中的编译器 IR
-
磁盘上的位码表示
-
人类可读形式(LLVM 汇编)
现在让我们通过一个例子来看看 LLVM IR 是如何看起来。我们将取一小段 C 代码,并使用 clang 将其转换为 LLVM IR,然后通过将其映射回源语言来理解 LLVM IR 的细节。
$ cat add.c
int globvar = 12;
int add(int a) {
return globvar + a;
}
使用以下选项的 clang 前端将其转换为 LLVM IR:
$ clang -emit-llvm -c -S add.c
$ cat add.ll
; ModuleID = 'add.c'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@globvar = global i32 12, align 4
; Function Attrs: nounwind uwtable
define i32 @add(i32 %a) #0 {
%1 = alloca i32, align 4
store i32 %a, i32* %1, align 4
%2 = load i32, i32* @globvar, align 4
%3 = load i32, i32* %1, align 4
%4 = add nsw i32 %2, %3
ret i32 %4
}
attributes #0 = { nounwind uwtable "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.ident = !{!0}
现在,让我们来看看生成的 IR,看看它究竟是什么。你可以看到第一行给出了 ModuleID,它定义了 add.c 文件的 LLVM 模块。LLVM 模块是一个顶层数据结构,包含了整个输入 LLVM 文件的内容。它由函数、全局变量、外部函数原型和符号表条目组成。
以下几行显示了目标数据布局和目标三元组,我们可以从中知道目标是运行 Linux 的 x86_64 处理器。datalayout 字符串告诉我们机器的字节序('e' 表示小端字节序),以及名称修饰(m : e 表示 elf 类型)。每个规范由 '–' 分隔,每个后续规范都提供了有关该类型及其大小的信息。例如,i64:64 表示 64 位整数是 64 位。
然后我们有一个全局变量 globvar。在 LLVM IR 中,所有全局变量都以 '@' 开头,所有局部变量都以 '%' 开头。变量前缀这些符号有两个主要原因。第一个原因是,编译器不必担心与保留字发生名称冲突,另一个原因是编译器可以快速生成一个临时名称,而无需担心与符号表冲突。这个第二个特性对于将 IR 表示为 静态单赋值(SSA)非常有用,其中每个变量只被赋值一次,每个变量的使用都紧随其定义。因此,在将普通程序转换为 SSA 形式时,我们为每个变量的重新定义创建一个新的临时名称,并限制早期定义的范围直到这个重新定义。
LLVM 将全局变量视为指针,因此需要使用加载指令显式地解引用全局变量。同样,要存储一个值,也需要使用显式存储指令。
局部变量分为两类:
-
寄存器分配的局部变量:这些是临时变量和分配的虚拟寄存器。在代码生成阶段,虚拟寄存器会被分配到物理寄存器中,我们将在本书的后续章节中看到这一点。它们是通过为变量使用新的符号来创建的:
%1 = some value -
栈分配的局部变量:这些是通过在当前执行函数的栈帧上分配变量,使用
alloca指令创建的。alloca指令提供了一个指向分配类型的指针,需要显式使用加载和存储指令来访问和存储值。%2 = alloca i32
现在我们来看看在 LLVM IR 中 add 函数是如何表示的。define i32 @add(i32 %a) 与 C 语言中函数的声明非常相似。它指定了函数返回整数类型 i32 并接受一个整数参数。此外,函数名前有一个 '@' 符号,意味着它具有全局可见性。
函数内部是实际的功能处理。在此处需要注意的一些重要事项是,LLVM 使用三地址指令,即数据处理指令,它有两个源操作数并将结果放置在单独的目标操作数中(%4 = add i32 %2, %3)。此外,代码是 SSA 形式,即 IR 中的每个值都有一个单独的赋值定义了该值。这对于许多优化非常有用。
在生成的 IR 中跟随的属性字符串指定了函数属性,这些属性与 C++ 属性非常相似。这些属性是为已定义的函数设置的。对于每个定义的函数,在 LLVM IR 中都有一组属性定义。
属性之后的代码是为 ident 指令设置的,该指令用于标识模块和编译器版本。
LLVM 工具及其在命令行中的使用。
到目前为止,我们已经了解了 LLVM IR(可读形式)是什么以及它是如何用来表示高级语言的。现在,我们将查看一些 LLVM 提供的工具,这样我们就可以对这个 IR 进行格式转换,并再次转换回原始形式。让我们逐一查看这些工具,并附带示例。
-
llvm-as: 这是 LLVM 汇编器,它将汇编形式的 LLVM IR(可读)转换为位码格式。使用前面的
add.ll作为示例将其转换为位码。要了解更多关于 LLVM 位码文件格式,请参阅llvm.org/docs/BitCodeFormat.html。$ llvm-as add.ll –o add.bc要查看此位码文件的内容,可以使用
hexdump等工具。$ hexdump –c add.bc -
llvm-dis: 这是 LLVM 反汇编器。它接受位码文件作为输入,并输出 llvm 汇编。
$ llvm-dis add.bc –o add.ll如果你检查
add.ll并与之前的版本进行比较,它将与之前的版本相同。 -
llvm-link: llvm-link 将两个或多个 llvm 位码文件链接起来,并输出一个 llvm 位码文件。要查看演示,请编写一个
main.c文件,该文件调用add.c文件中的函数。$ cat main.c #include<stdio.h> extern int add(int); int main() { int a = add(2); printf("%d\n",a); return 0; }使用以下命令将 C 源代码转换为 LLVM 位码格式。
$ clang -emit-llvm -c main.c现在将
main.bc和add.bc链接起来生成output.bc。$ llvm-link main.bc add.bc -o output.bc -
lli: lli 使用即时编译器或解释器直接执行 LLVM 位码格式的程序,如果当前架构有可用的解释器。lli 不像虚拟机,不能执行不同架构的 IR,只能为宿主架构进行解释。使用由 llvm-link 生成的位码格式文件作为 lli 的输入。它将在标准输出上显示输出。
$ lli output.bc 14 -
llc: llc 是一个静态编译器。它将 LLVM 输入(汇编形式/位码形式)编译成指定架构的汇编语言。在以下示例中,它将由 llvm-link 生成的
output.bc文件转换为汇编文件output.s。$ llc output.bc –o output.s让我们来看看
output.s汇编的内容,特别是生成的代码的两个函数,这与本地汇编器生成的代码非常相似。Function main: .type main,@function main: # @main .cfi_startproc # BB#0: pushq %rbp .Ltmp0: .cfi_def_cfa_offset 16 .Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp .Ltmp2: .cfi_def_cfa_register %rbp subq $16, %rsp movl $0, -4(%rbp) movl $2, %edi callq add movl %eax, %ecx movl %ecx, -8(%rbp) movl $.L.str, %edi xorl %eax, %eax movl %ecx, %esi callq printf xorl %eax, %eax addq $16, %rsp popq %rbp retq .Lfunc_end0: Function: add add: # @add .cfi_startproc # BB#0: pushq %rbp .Ltmp3: .cfi_def_cfa_offset 16 .Ltmp4: .cfi_offset %rbp, -16 movq %rsp, %rbp .Ltmp5: .cfi_def_cfa_register %rbp movl %edi, -4(%rbp) addl globvar(%rip), %edi movl %edi, %eax popq %rbp retq .Lfunc_end1: -
函数内联 -
**instcombine**: 用于合并冗余指令 -
****licm: 循环不变量代码移动
-
****tailcallelim: 尾调用消除
注意
在继续之前,我们必须注意,本章中提到的所有工具都是为编译器编写者准备的。最终用户可以直接使用 clang 编译 C 代码,而无需将 C 代码转换为中间表示形式
提示
**下载示例代码
您可以从www.packtpub.com上的账户下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
# 摘要
在本章中,我们探讨了 LLVM 的模块化设计:它在 LLVM 的 opt 工具中的应用,以及它在 LLVM 核心库中的应用。然后我们查看 LLVM 中间表示,以及语言的各种实体(变量、函数等)如何映射到 LLVM IR。在最后一节中,我们讨论了一些重要的 LLVM 工具,以及如何使用它们将 LLVM IR 从一种形式转换为另一种形式。
在下一章中,我们将看到如何使用 LLVM 工具编写一个可以输出 LLVM IR 的语言前端。**
第二章:构建 LLVM IR
高级编程语言便于人与目标机器的交互。今天的大多数流行高级语言都有一些基本元素,如变量、循环、if-else 决策语句、块、函数等。变量持有数据类型的价值;基本块给出了变量的作用域的概念。if-else 决策语句有助于选择代码路径。函数使代码块可重用。高级语言可能在类型检查、类型转换、变量声明、复杂数据类型等方面有所不同。然而,几乎每种语言都有本节前面列出的基本构建块。
一种语言可能有自己的解析器,它将语句标记化并提取有意义的信息,如标识符及其数据类型;函数名称、其声明、定义和调用;循环条件等。这些有意义的信息可以存储在数据结构中,以便可以轻松检索代码的流程。抽象语法树(AST)是源代码的流行树形表示。AST 可以用于进一步的转换和分析。
语言解析器可以用各种方式编写,使用各种工具如 lex、yacc 等,甚至可以手动编写。编写一个高效的解析器本身就是一门艺术。但本章我们并不打算涵盖这一点。我们更希望关注 LLVM IR 以及如何使用 LLVM 库将解析后的高级语言转换为 LLVM IR。
本章将介绍如何构建基本的工作 LLVM 示例代码,包括以下内容:
-
创建一个 LLVM 模块
-
在模块中发射一个函数
-
向函数中添加一个块
-
发射全局变量
-
发射返回语句
-
发射函数参数
-
在基本块中发射一个简单的算术语句
-
发射 if-else 条件 IR
-
发射循环的 LLVM IR
创建一个 LLVM 模块
在上一章中,我们了解到了 LLVM IR 的外观。在 LLVM 中,一个模块代表了一个要一起处理的单个代码单元。LLVM 模块类是所有其他 LLVM IR 对象的最高级容器。LLVM 模块包含全局变量、函数、数据布局、主机三元组等。让我们创建一个简单的 LLVM 模块。
LLVM 提供了 Module() 构造函数用于创建模块。第一个参数是模块的名称。第二个参数是 LLVMContext。让我们在主函数中获取这些参数并创建一个模块,如下所示:
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
为了使这些函数正常工作,我们需要包含某些头文件:
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
int main(int argc, char *argv[]) {
ModuleOb->dump();
return 0;
}
将此代码放入一个文件中,比如 toy.cpp,然后编译它:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
$ ./toy
输出将如下所示:
; ModuleID = 'my compiler'
在模块中发射一个函数
现在我们已经创建了一个模块,下一步是输出一个函数。LLVM 有一个 IRBuilder 类,用于生成 LLVM IR 并使用模块对象的 dump 函数打印它。LLVM 提供了 llvm::Function 类来创建函数和 llvm::FunctionType() 来为函数关联返回类型。让我们假设我们的 foo() 函数返回整数类型。
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
FunctionType *funcType = llvm::FunctionType::get(Builder.getInt32Ty(), false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
最后,在 fooFunc 上调用函数 verifyFunction()。此函数对生成的代码执行各种一致性检查,以确定我们的编译器是否一切正常。
int main(int argc, char *argv[]) {
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
在包含部分添加 IR/IRBuilder.h、IR/DerivedTypes.h 和 IR/Verifier.h 文件。
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
FunctionType *funcType = llvm::FunctionType::get(Builder.getInt32Ty(), false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
int main(int argc, char *argv[]) {
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
使用之前所述的相同选项编译 toy.cpp:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
$ ./toy
; ModuleID = 'my compiler'
declare i32 @foo()
向函数添加一个块
函数由基本块组成。基本块有一个入口点。基本块由一系列 IR 指令组成,最后一条指令是终止指令。它有一个单一的出口点。LLVM 提供了 BasicBlock 类来创建和处理基本块。基本块可能以标签作为入口点,这表示在哪里插入后续指令。我们可以使用 IRBuilder 对象来保存这些新的基本块 IR。
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
FunctionType *funcType = llvm::FunctionType::get(Builder.getInt32Ty(), false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
int main(int argc, char *argv[]) {
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译 toy.cpp 文件:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
; ModuleID = 'my compiler'
define i32 @foo() {
entry:
}
输出全局变量
全局变量的可见性是给定模块内所有函数的。LLVM 提供了 GlobalVariable 类来创建全局变量并设置其属性,如链接类型、对齐等。Module 类有 getOrInsertGlobal() 方法来创建全局变量。它接受两个参数——第一个是变量的名称,第二个是变量的数据类型。
由于全局变量是模块的一部分,我们在创建模块后创建全局变量。在 toy.cpp 中创建模块后立即插入以下代码:
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
链接 决定了相同对象的多个声明是否引用同一个对象,还是不同的对象。LLVM 参考手册引用了以下类型的链接:
ExternalLinkage |
外部可见函数。 |
|---|---|
AvailableExternallyLinkage |
可供检查,但不进行输出。 |
LinkOnceAnyLinkage |
链接时(内联)保留函数的一个副本 |
LinkOnceODRLinkage |
相同,但仅替换为等效项。 |
WeakAnyLinkage |
链接时(弱)保留命名函数的一个副本 |
WeakODRLinkage |
相同,但仅替换为等效项。 |
AppendingLinkage |
特殊用途,仅适用于全局数组。 |
InternalLinkage |
链接时重命名冲突(静态函数)。 |
PrivateLinkage |
类似于内部,但省略符号表。 |
ExternalWeakLinkage |
ExternalWeak 链接描述。 |
CommonLinkage |
暂定定义 |
对齐提供了关于地址对齐的信息。对齐必须是 2 的幂。如果没有明确指定,则由目标设置。最大对齐为 1 << 29。
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
FunctionType *funcType = llvm::FunctionType::get(Builder.getInt32Ty(), false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
int main(int argc, char *argv[]) {
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译 toy.cpp:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo() {
entry:
}
发射返回语句
函数可能返回一个值,也可能返回 void。在我们的例子中,我们定义了我们的函数返回一个整数。让我们假设我们的函数返回 0。第一步是获取一个 0 值,这可以通过使用 Constant 类来完成。
Builder.CreateRet(Builder.getInt32(0));
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
FunctionType *funcType = llvm::FunctionType::get(Builder.getInt32Ty(), false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
int main(int argc, char *argv[]) {
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Builder.CreateRet(Builder.getInt32(0));
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译 toy.cpp 文件
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo() {
entry:
ret i32 0
}
发射函数参数
函数接受具有其自身数据类型的参数。为了简化,假设我们的函数所有参数都是 i32 类型(32 位整数)。
例如,我们将考虑将两个参数 a 和 b 传递给函数。我们将这两个参数存储在一个向量中:
static std::vector <std::string> FunArgs;
FunArgs.push_back("a");
FunArgs.push_back("b");
下一步是指定函数将有两个参数。这可以通过将整数参数传递给 functiontype 来完成。
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
std::vector<Type *> Integers(FunArgs.size(), Type::getInt32Ty(Context));
FunctionType *funcType =
llvm::FunctionType::get(Builder.getInt32Ty(), Integers, false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
最后一步是为函数参数设置名称。这可以通过在循环中使用 Function 参数迭代器来完成,如下所示:
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
std::vector<Type *> Integers(FunArgs.size(), Type::getInt32Ty(Context));
FunctionType *funcType =
llvm::FunctionType::get(Builder.getInt32Ty(), Integers, false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
FunArgs.push_back("b");
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Builder.CreateRet(Builder.getInt32(0));
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译 toy.cpp 文件:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo(i32 %a, i32 %b) {
entry:
ret i32 0
}
在基本块中发射一个简单的算术语句
基本块由一系列指令组成。例如,一个指令可以是一个简单的语句,根据一些简单的算术指令执行任务。我们将看到如何使用 LLVM API 发射算术指令。
例如,如果我们想将第一个参数 a 与整数值 16 相乘,我们将使用以下 API 创建一个常量整数值 16:
Value *constant = Builder.getInt32(16);
我们已经从函数参数列表中有了:
Value *Arg1 = fooFunc->arg_begin();
LLVM 提供了一个丰富的 API 列表来创建二元运算。你可以通过查看 include/llvm/IR/IRBuild.h 文件来获取更多关于 API 的详细信息。
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateMul(L, R, "multmp");
}
备注
注意,出于演示目的,前面的函数返回乘法。我们留给读者去使这个函数更灵活,以返回任何二元运算。你可以在 include/llvm/IR/IRBuild.h 中探索更多二元运算。
整个代码现在看起来如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
std::vector<Type *> Integers(FunArgs.size(), Type::getInt32Ty(Context));
FunctionType *funcType =
llvm::FunctionType::get(Builder.getInt32Ty(), Integers, false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateMul(L, R, "multmp");
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
FunArgs.push_back("b");
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *Arg1 = fooFunc->arg_begin();
Value *constant = Builder.getInt32(16);
Value *val = createArith(Builder, Arg1, constant);
Builder.CreateRet(val);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译以下程序:
$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
输出将如下所示:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo(i32 %a, i32 %b) {
entry:
%multmp = mul i32 %a, 16
ret i32 %multmp
}
你注意到返回值了吗?我们返回了乘法而不是常数 0。
发射 if-else 条件 IR
if-else 语句有一个条件表达式和两个代码路径来执行,取决于条件评估为真或假。条件表达式通常是一个比较语句。让我们在块的开始处发射一个条件语句。例如,让条件为 a<100。
Value *val2 = Builder.getInt32(100);
Value *Compare = Builder.CreateICmpULT(val, val2, "cmptmp");
在编译时,我们得到以下输出:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo(i32 %a, i32 %b) {
entry:
%multmp = mul i32 %a, 16
%cmptmp = icmp ult i32 %multmp, 100
ret i32 %multmp
}
下一步是定义then和else块表达式,这将根据条件表达式"booltmp"的结果执行。在这里,PHI指令的重要概念出现了。一个 phi 指令接受来自不同基本块的各种值,并根据条件表达式决定分配哪个值。
将创建两个单独的基本块"ThenBB"和"ElseBB"。假设then表达式是'将 a 加 1',而else表达式是'将 a 加 2'。
第三个块将表示合并块,其中包含在then和else块合并时需要执行的指令。这些块需要推入foo()函数中。
为了提高复用性,我们创建如下所示的BasicBlock和Value容器:
typedef SmallVector<BasicBlock *, 16> BBList;
typedef SmallVector<Value *, 16> ValList;
注意
注意,SmallVector<>是 LLVM 为了简化提供的向量容器包装器。
我们还将一些值推入Value*列表中,以便在 if-else 块中处理,如下所示:
Value *Condtn = Builder.CreateICmpNE(Compare, Builder.getInt32(0),
"ifcond");
ValList VL;
VL.push_back(Condtn);
VL.push_back(Arg1);
我们创建三个基本块并将它们推入容器中,如下所示:
BasicBlock *ThenBB = createBB(fooFunc, "then");
BasicBlock *ElseBB = createBB(fooFunc, "else");
BasicBlock *MergeBB = createBB(fooFunc, "ifcont");
BBList List;
List.push_back(ThenBB);
List.push_back(ElseBB);
List.push_back(MergeBB);
我们最终创建一个函数来生成 if-else 块:
Value *createIfElse(IRBuilder<> &Builder, BBList List, ValList VL) {
Value *Condtn = VL[0];
Value *Arg1 = VL[1];
BasicBlock *ThenBB = List[0];
BasicBlock *ElseBB = List[1];
BasicBlock *MergeBB = List[2];
Builder.CreateCondBr(Condtn, ThenBB, ElseBB);
Builder.SetInsertPoint(ThenBB);
Value *ThenVal = Builder.CreateAdd(Arg1, Builder.getInt32(1), "thenaddtmp");
Builder.CreateBr(MergeBB);
Builder.SetInsertPoint(ElseBB);
Value *ElseVal = Builder.CreateAdd(Arg1, Builder.getInt32(2), "elseaddtmp");
Builder.CreateBr(MergeBB);
unsigned PhiBBSize = List.size() - 1;
Builder.SetInsertPoint(MergeBB);
PHINode *Phi = Builder.CreatePHI(Type::getInt32Ty(getGlobalContext()), PhiBBSize, "iftmp");
Phi->addIncoming(ThenVal, ThenBB);
Phi->addIncoming(ElseVal, ElseBB);
return Phi;
}
整体代码:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
typedef SmallVector<BasicBlock *, 16> BBList;
typedef SmallVector<Value *, 16> ValList;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
std::vector<Type *> Integers(FunArgs.size(), Type::getInt32Ty(Context));
FunctionType *funcType =
llvm::FunctionType::get(Builder.getInt32Ty(), Integers, false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateMul(L, R, "multmp");
}
Value *createIfElse(IRBuilder<> &Builder, BBList List, ValList VL) {
Value *Condtn = VL[0];
Value *Arg1 = VL[1];
BasicBlock *ThenBB = List[0];
BasicBlock *ElseBB = List[1];
BasicBlock *MergeBB = List[2];
Builder.CreateCondBr(Condtn, ThenBB, ElseBB);
Builder.SetInsertPoint(ThenBB);
Value *ThenVal = Builder.CreateAdd(Arg1, Builder.getInt32(1), "thenaddtmp");
Builder.CreateBr(MergeBB);
Builder.SetInsertPoint(ElseBB);
Value *ElseVal = Builder.CreateAdd(Arg1, Builder.getInt32(2), "elseaddtmp");
Builder.CreateBr(MergeBB);
unsigned PhiBBSize = List.size() - 1;
Builder.SetInsertPoint(MergeBB);
PHINode *Phi = Builder.CreatePHI(Type::getInt32Ty(getGlobalContext()), PhiBBSize, "iftmp");
PhiBBSize, "iftmp");
Phi->addIncoming(ThenVal, ThenBB);
Phi->addIncoming(ElseVal, ElseBB);
return Phi;
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
FunArgs.push_back("b");
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *Arg1 = fooFunc->arg_begin();
Value *constant = Builder.getInt32(16);
Value *val = createArith(Builder, Arg1, constant);
Value *val2 = Builder.getInt32(100);
Value *Compare = Builder.CreateICmpULT(val, val2, "cmptmp");
Value *Condtn = Builder.CreateICmpNE(Compare, Builder.getInt32(0), "ifcond");
ValList VL;
VL.push_back(Condtn);
VL.push_back(Arg1);
BasicBlock *ThenBB = createBB(fooFunc, "then");
BasicBlock *ElseBB = createBB(fooFunc, "else");
BasicBlock *MergeBB = createBB(fooFunc, "ifcont");
BBList List;
List.push_back(ThenBB);
List.push_back(ElseBB);
List.push_back(MergeBB);
Value *v = createIfElse(Builder, List, VL);
Builder.CreateRet(v);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译后,输出如下所示:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo(i32 %a, i32 %b) {
entry:
%multmp = mul i32 %a, 16
%cmptmp = icmp ult i32 %multmp, 100
%ifcond = icmp ne i1 %cmptmp, i32 0
br i1 %ifcond, label %then, label %else
then: ; preds = %entry
%thenaddtmp = add i32 %a, 1
br label %ifcont
else: ; preds = %entry
%elseaddtmp = add i32 %a, 2
br label %ifcont
ifcont: ; preds = %else, %then
%iftmp = phi i32 [ %thenaddtmp, %then ], [ %elseaddtmp, %else ]
ret i32 %iftmp
}
循环的 LLVM IR 生成
与 if-else 语句类似,循环也可以使用 LLVM API 的稍作修改来生成。例如,我们想要以下循环的 LLVM IR:
for(i=1; i< b; i++) {body}
循环有一个循环变量i,它有一个初始值,在每次迭代后更新。在先前的例子中,循环变量在每次迭代后通过一个步长值更新,该步长值为1。然后有一个循环结束条件。在先前的例子中,"i=1"是初始值,"i<b"是循环的结束条件,"i++"是每次循环迭代后循环变量"i"增加的步长值。
在编写创建循环的函数之前,需要将一些Value和BasicBlock推入一个列表中,如下所示:
Function::arg_iterator AI = fooFunc->arg_begin();
Value *Arg1 = AI++;
Value *Arg2 = AI;
Value *constant = Builder.getInt32(16);
Value *val = createArith(Builder, Arg1, constant);
ValList VL;
VL.push_back(Arg1);
BBList List;
BasicBlock *LoopBB = createBB(fooFunc, "loop");
BasicBlock *AfterBB = createBB(fooFunc, "afterloop");
List.push_back(LoopBB);
List.push_back(AfterBB);
Value *StartVal = Builder.getInt32(1);
让我们创建一个用于生成循环的函数:
PHINode *createLoop(IRBuilder<> &Builder, BBList List, ValList VL,
Value *StartVal, Value *EndVal) {
BasicBlock *PreheaderBB = Builder.GetInsertBlock();
Value *val = VL[0];
BasicBlock *LoopBB = List[0];
Builder.CreateBr(LoopBB);
Builder.SetInsertPoint(LoopBB);
PHINode *IndVar = Builder.CreatePHI(Type::getInt32Ty(Context), 2, "i");
IndVar->addIncoming(StartVal, PreheaderBB);
Builder.CreateAdd(val, Builder.getInt32(5), "addtmp");
Value *StepVal = Builder.getInt32(1);
Value *NextVal = Builder.CreateAdd(IndVar, StepVal, "nextval");
Value *EndCond = Builder.CreateICmpULT(IndVar, EndVal, "endcond");
EndCond = Builder.CreateICmpNE(EndCond, Builder.getInt32(0), "loopcond");
BasicBlock *LoopEndBB = Builder.GetInsertBlock();
BasicBlock *AfterBB = List[1];
Builder.CreateCondBr(EndCond, LoopBB, AfterBB);
Builder.SetInsertPoint(AfterBB);
IndVar->addIncoming(NextVal, LoopEndBB);
return IndVar;
}
考虑以下代码行:
IndVar->addIncoming(StartVal, PreheaderBB);…
IndVar->addIncoming(NextVal, LoopEndBB);
IndVar是一个 PHI 节点,它从两个块中接收两个值——从预头块(i=1)的startval和从循环结束块(Nextval)。
整体代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
typedef SmallVector<BasicBlock *, 16> BBList;
typedef SmallVector<Value *, 16> ValList;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
std::vector<Type *> Integers(FunArgs.size(), Type::getInt32Ty(Context));
FunctionType *funcType =
llvm::FunctionType::get(Builder.getInt32Ty(), Integers, false);
Function *fooFunc = llvm::Function::Create(
funcType, llvm::Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
GlobalVariable *createGlob(IRBuilder<> &Builder, std::string Name) {
ModuleOb->getOrInsertGlobal(Name, Builder.getInt32Ty());
GlobalVariable *gVar = ModuleOb->getNamedGlobal(Name);
gVar->setLinkage(GlobalValue::CommonLinkage);
gVar->setAlignment(4);
return gVar;
}
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateMul(L, R, "multmp");
}
Value *createLoop(IRBuilder<> &Builder, BBList List, ValList VL,
Value *StartVal, Value *EndVal) {
BasicBlock *PreheaderBB = Builder.GetInsertBlock();
Value *val = VL[0];
BasicBlock *LoopBB = List[0];
Builder.CreateBr(LoopBB);
Builder.SetInsertPoint(LoopBB);
PHINode *IndVar = Builder.CreatePHI(Type::getInt32Ty(Context), 2, "i");
IndVar->addIncoming(StartVal, PreheaderBB);
Value *Add = Builder.CreateAdd(val, Builder.getInt32(5), "addtmp");
Value *StepVal = Builder.getInt32(1);
Value *NextVal = Builder.CreateAdd(IndVar, StepVal, "nextval");
Value *EndCond = Builder.CreateICmpULT(IndVar, EndVal, "endcond");
EndCond = Builder.CreateICmpNE(EndCond, Builder.getInt32(0), "loopcond");
BasicBlock *LoopEndBB = Builder.GetInsertBlock();
BasicBlock *AfterBB = List[1];
Builder.CreateCondBr(EndCond, LoopBB, AfterBB);
Builder.SetInsertPoint(AfterBB);
IndVar->addIncoming(NextVal, LoopEndBB);
return Add;
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
FunArgs.push_back("b");
static IRBuilder<> Builder(Context);
GlobalVariable *gVar = createGlob(Builder, "x");
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Function::arg_iterator AI = fooFunc->arg_begin();
Value *Arg1 = AI++;
Value *Arg2 = AI;
Value *constant = Builder.getInt32(16);
Value *val = createArith(Builder, Arg1, constant);
ValList VL;
VL.push_back(Arg1);
BBList List;
BasicBlock *LoopBB = createBB(fooFunc, "loop");
BasicBlock *AfterBB = createBB(fooFunc, "afterloop");
List.push_back(LoopBB);
List.push_back(AfterBB);
Value *StartVal = Builder.getInt32(1);
Value *Res = createLoop(Builder, List, VL, StartVal, Arg2);
Builder.CreateRet(Res);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译程序后,我们得到以下输出:
; ModuleID = 'my compiler'
@x = common global i32, align 4
define i32 @foo(i32 %a, i32 %b) {
entry:
%multmp = mul i32 %a, 16
br label %loop
loop: ; preds = %loop, %entry
%i = phi i32 [ 1, %entry ], [ %nextval, %loop ]
%addtmp = add i32 %a, 5
%nextval = add i32 %i, 1
%endcond = icmp ult i32 %i, %b
%loopcond = icmp ne i1 %endcond, i32 0
br i1 %loopcond, label %loop, label %afterloop
afterloop: ; preds = %loop
ret i32 %addtmp
}
概述
在本章中,你学习了如何使用 LLVM 提供的丰富库创建简单的 LLVM IR。记住,LLVM IR 是一个中间表示。高级编程语言通过自定义解析器转换为 LLVM IR,该解析器将代码分解为原子元素,如变量、函数、函数返回类型、函数参数、if-else 条件、循环、指针、数组等。这些原子元素可以存储到自定义数据结构中,然后可以使用这些数据结构来生成 LLVM IR,正如本章所演示的那样。
在解析器阶段,可以进行句法分析,而词法分析和类型检查可以在解析后、发射 IR 之前的中级阶段进行。
在实际应用中,几乎不会以本章所示的方式硬编码地发射红外线。相反,一种语言会被解析并表示为抽象语法树。然后,借助 LLVM 库,使用该树发射 LLVM IR,如前所述。LLVM 社区已经提供了一个优秀的教程,用于编写解析器并发射 LLVM IR。您可以访问llvm.org/docs/tutorial/获取相同的信息。
在下一章中,我们将看到如何发射一些复杂的数据结构,如数组、指针。我们还将通过 Clang(C/C++的前端)的一些示例,了解语义分析是如何进行的。
第三章。高级 LLVM IR
LLVM 为高效的编译器转换和分析提供了一种强大的中间表示,同时提供了调试和可视化转换的自然方式。IR 的设计使其可以轻松映射到高级语言。LLVM IR 提供了类型信息,可用于各种优化。
在上一章中,你学习了如何在函数和模块中创建一些简单的 LLVM 指令。从发出二进制操作等简单示例开始,我们在模块中构建了函数,并创建了诸如 if-else 和循环等一些复杂的编程范式。LLVM 提供了一套丰富的指令和内嵌函数,用于发出复杂的 IR。
在本章中,我们将通过一些涉及内存操作的更多 LLVM IR 示例。本章还将涵盖一些高级主题,例如聚合数据类型及其操作。本章涵盖的主题如下:
-
获取元素的地址
-
从内存中读取
-
向内存位置写入
-
将标量插入到向量中
-
从向量中提取标量
内存访问操作
内存是几乎所有计算系统的重要组件。内存存储数据,这些数据需要被读取以在计算系统中执行操作。操作的结果将存储回内存中。
第一步是从内存中获取所需元素的地址,并将该特定元素可以找到的地址存储起来。你现在将学习如何计算地址并执行加载/存储操作。
获取元素的地址
在 LLVM 中,getelementptr 指令用于获取聚合数据结构中元素的地址。它只计算地址,并不访问内存。
getelementptr 指令的第一个参数是一个用作计算地址基础的类型。第二个参数是指针或指针的向量,它作为地址的基础 - 在我们的数组情况下将是 a。接下来的参数是要访问的元素的索引。
语言参考(llvm.org/docs/LangRef.html#getelementptr-instruction)中提到了关于 getelementptr 指令的重要注意事项如下:
第一个索引始终索引第一个参数给出的指针值,第二个索引索引指向的类型(不一定是直接指向的值,因为第一个索引可能不为零),等等。第一个索引的类型必须是指针值,后续的类型可以是数组、向量和结构体。注意,后续索引的类型不能是指针,因为这需要在继续计算之前加载指针。
这本质上意味着两件重要的事情:
-
每个指针都有一个索引,第一个索引始终是数组索引。如果它是一个结构体的指针,你必须使用索引 0 来表示(第一个这样的结构体),然后是元素的索引。
-
第一个类型参数帮助 GEP 识别基结构及其元素的大小,从而轻松计算地址。结果类型(
%a1)不一定相同。
更详细的解释请参阅 llvm.org/docs/GetElementPtr.html
假设我们有一个指向两个 32 位整数向量 <2 x i32>* %a 的指针,并且我们想要访问向量中的第二个整数。地址将被计算如下
%a1 = getelementptr i32, <2 x i32>* %a, i32 1
要发出此指令,可以使用如下所示的 LLVM API:
首先创建一个数组类型,该类型将被作为参数传递给函数。
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 2);
Type *ptrTy = vecTy->getPointerTo(0);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), ptrTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
Value *getGEP(IRBuilder<> &Builder, Value *Base, Value *Offset) {
return Builder.CreateGEP(Builder.getInt32Ty(), Base, Offset, "a1");
}
整个代码看起来如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 2);
Type *ptrTy = vecTy->getPointerTo(0);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), ptrTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
Value *getGEP(IRBuilder<> &Builder, Value *Base, Value *Offset) {
return Builder.CreateGEP(Builder.getInt32Ty(), Base, Offset, "a1");
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
Value *Base = fooFunc->arg_begin();
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *gep = getGEP(Builder, Base, Builder.getInt32(1));
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译代码:
$ clang++ toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -fno-rtti -o toy
$ ./toy
输出:
; ModuleID = 'my compiler'
define i32 @foo(<2 x i32>* %a) {
entry:
%a1 = getelementptr i32, <2 x i32>* %a, i32 1
ret i32 0
}
从内存读取
现在,因为我们有了地址,我们准备从该地址读取数据并将读取的值赋给一个变量。
在 LLVM 中,load 指令用于从内存位置读取。这个简单的指令或类似指令的组合可以映射到底层汇编中的某些复杂的内存读取指令。
一个 load 指令接受一个参数,即从该内存地址读取数据的内存地址。我们在上一节中通过 getelementptr 指令在 a1 中获得了地址。
load 指令看起来如下:
%val = load i32, i32* a1
这意味着 load 将取由 a1 指向的数据并将其保存到 %val 中。
要发出此,我们可以在函数中使用 LLVM 提供的 API,如下所示:
Value *getLoad(IRBuilder<> &Builder, Value *Address) {
return Builder.CreateLoad(Address, "load");
}
让我们也返回加载的值:
builder.CreateRet(val);
整个代码如下:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 2);
Type *ptrTy = vecTy->getPointerTo(0);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), ptrTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
Value *getGEP(IRBuilder<> &Builder, Value *Base, Value *Offset) {
return Builder.CreateGEP(Builder.getInt32Ty(), Base, Offset, "a1");
}
Value *getLoad(IRBuilder<> &Builder, Value *Address) {
return Builder.CreateLoad(Address, "load");
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
Value *Base = fooFunc->arg_begin();
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *gep = getGEP(Builder, Base, Builder.getInt32(1));
Value *load = getLoad(Builder, gep);
Builder.CreateRet(load);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译以下代码:
$ clang++ toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -fno-rtti -o toy
$ ./toy
以下是输出:
; ModuleID = 'my compiler'
define i32 @foo(<2 x i32>* %a) {
entry:
%a1 = getelementptr i32, <2 x i32>* %a, i32 1
%load = load i32, i32* %a1
ret i32 %load
}
将数据写入内存位置
LLVM 使用 store 指令将数据写入内存位置。store 指令有两个参数:要存储的值和存储它的地址。store 指令没有返回值。假设我们想要将数据写入两个整数的向量中的第二个元素。store 指令看起来像 store i32 3, i32* %a1。要发出 store 指令,我们可以使用 LLVM 提供的以下 API:
void getStore(IRBuilder<> &Builder, Value *Address, Value *V) {
Builder.CreateStore(V, Address);
}
例如,我们将 <2 x i32> 向量的第二个元素乘以 16 并将其存储在相同的位置。
考虑以下代码:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 2);
Type *ptrTy = vecTy->getPointerTo(0);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), ptrTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateMul(L, R, "multmp");
}
Value *getGEP(IRBuilder<> &Builder, Value *Base, Value *Offset) {
return Builder.CreateGEP(Builder.getInt32Ty(), Base, Offset, "a1");
}
Value *getLoad(IRBuilder<> &Builder, Value *Address) {
return Builder.CreateLoad(Address, "load");
}
void getStore(IRBuilder<> &Builder, Value *Address, Value *V) {
Builder.CreateStore(V, Address);
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
Value *Base = fooFunc->arg_begin();
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *gep = getGEP(Builder, Base, Builder.getInt32(1));
Value *load = getLoad(Builder, gep);
Value *constant = Builder.getInt32(16);
Value *val = createArith(Builder, load, constant);
getStore(Builder, gep, val);
Builder.CreateRet(val);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译以下代码:
$ clang++ toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -fno-rtti -o toy
$ ./toy
生成的输出将如下:
; ModuleID = 'my compiler'
define i32 @foo(<2 x i32>* %a) {
entry:
%a1 = getelementptr i32, <2 x i32>* %a, i32 1
%load = load i32, i32* %a1
%multmp = mul i32 %load, 16
store i32 %multmp, i32* %a1
ret i32 %multmp
}
将标量插入到向量中
LLVM 还提供了发出指令的 API,该指令可以将标量插入到向量类型中。请注意,这种向量与数组不同。向量类型是一个简单的派生类型,表示元素向量。当使用 单指令多数据(SIMD)并行操作多个原始数据时,使用向量类型。向量类型需要一个大小(元素数量)和一个基础原始数据类型。例如,我们有一个 Vec 向量,它包含四个 i32 类型的整数 <4 x i32>。现在,我们想在向量的 0、1、2 和 3 索引处插入值 10、20、30 和 40。
insertelement 指令接受三个参数。第一个参数是向量类型的值。第二个操作数是一个标量值,其类型必须等于第一个操作数的元素类型。第三个操作数是一个索引,指示要插入值的位位置。结果值是相同类型的向量。
insertelement 指令看起来如下:
%vec0 = insertelement <4 x double> Vec, %val0, %idx
这可以通过以下要点进一步理解:
-
Vec是向量类型< 4 x i32 > -
val0是要插入的值 -
idx是要在向量中插入值的索引
考虑以下代码:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 4);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), vecTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
Value *getInsertElement(IRBuilder<> &Builder, Value *Vec, Value *Val,
Value *Index) {
return Builder.CreateInsertElement(Vec, Val, Index);
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *Vec = fooFunc->arg_begin();
for (unsigned int i = 0; i < 4; i++)
Value *V = getInsertElement(Builder, Vec, Builder.getInt32((i + 1) * 10), Builder.getInt32(i));
Builder.CreateRet(Builder.getInt32(0));
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译以下代码:
$ clang++ toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -fno-rtti -o toy
$ ./toy
结果输出如下:
; ModuleID = 'my compiler'
define i32 @foo(<4 x i32> %a) {
entry:
%0 = insertelement <4 x i32> %a, i32 10, i32 0
%1 = insertelement <4 x i32> %a, i32 20, i32 1
%2 = insertelement <4 x i32> %a, i32 30, i32 2
%3 = insertelement <4 x i32> %a, i32 40, i32 3
ret i32 0
}
向量 Vec 将具有 <10, 20, 30, 40> 的值。
从向量中提取标量
可以从向量中提取单个标量元素。LLVM 提供了 extractelement 指令来完成同样的操作。extractelement 指令的第一个操作数是向量类型的值。第二个操作数是一个索引,指示从哪个位置提取元素。
insertelement 指令看起来如下:
result = extractelement <4 x i32> %vec, i32 %idx
这可以通过以下要点进一步理解:
-
vec是一个向量 -
idx是要提取的数据所在的索引 -
result是标量类型,这里为i32
让我们举一个例子,我们想要将给定向量的所有元素相加并返回一个整数。
考虑以下代码:
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include <vector>
using namespace llvm;
static LLVMContext &Context = getGlobalContext();
static Module *ModuleOb = new Module("my compiler", Context);
static std::vector<std::string> FunArgs;
Function *createFunc(IRBuilder<> &Builder, std::string Name) {
Type *u32Ty = Type::getInt32Ty(Context);
Type *vecTy = VectorType::get(u32Ty, 4);
FunctionType *funcType =
FunctionType::get(Builder.getInt32Ty(), vecTy, false);
Function *fooFunc =
Function::Create(funcType, Function::ExternalLinkage, Name, ModuleOb);
return fooFunc;
}
void setFuncArgs(Function *fooFunc, std::vector<std::string> FunArgs) {
unsigned Idx = 0;
Function::arg_iterator AI, AE;
for (AI = fooFunc->arg_begin(), AE = fooFunc->arg_end(); AI != AE;
++AI, ++Idx)
AI->setName(FunArgs[Idx]);
}
BasicBlock *createBB(Function *fooFunc, std::string Name) {
return BasicBlock::Create(Context, Name, fooFunc);
}
Value *createArith(IRBuilder<> &Builder, Value *L, Value *R) {
return Builder.CreateAdd(L, R, "add");
}
Value *getExtractElement(IRBuilder<> &Builder, Value *Vec, Value *Index) {
return Builder.CreateExtractElement(Vec, Index);
}
int main(int argc, char *argv[]) {
FunArgs.push_back("a");
static IRBuilder<> Builder(Context);
Function *fooFunc = createFunc(Builder, "foo");
setFuncArgs(fooFunc, FunArgs);
BasicBlock *entry = createBB(fooFunc, "entry");
Builder.SetInsertPoint(entry);
Value *Vec = fooFunc->arg_begin();
SmallVector<Value *, 4> V;
for (unsigned int i = 0; i < 4; i++)
V[i] = getExtractElement(Builder, Vec, Builder.getInt32(i));
Value *add1 = createArith(Builder, V[0], V[1]);
Value *add2 = createArith(Builder, add1, V[2]);
Value *add = createArith(Builder, add2, V[3]);
Builder.CreateRet(add);
verifyFunction(*fooFunc);
ModuleOb->dump();
return 0;
}
编译以下代码:
$ clang++ toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -fno-rtti -o toy
$ ./toy
输出:
ModuleID = 'my compiler'
define i32 @foo(<4 x i32> %a) {
entry:
%0 = extractelement <4 x i32> %a, i32 0
%1 = extractelement <4 x i32> %a, i32 1
%2 = extractelement <4 x i32> %a, i32 2
%3 = extractelement <4 x i32> %a, i32 3
%add = add i32 %0, %1
%add1 = add i32 %add, %2
%add2 = add i32 %add1, %3
ret i32 %add2
}
摘要
内存操作对于大多数目标架构来说是一个重要的指令。一些架构具有复杂的指令来在内存中移动数据。一些甚至可以直接在内存操作数上执行二进制操作,而另一些则从内存中加载数据到寄存器,然后对其进行操作(CISC 对比 RISC)。许多加载/存储操作也由 LLVM 内置函数完成。例如,请参阅 llvm.org/docs/LangRef.html#masked-vector-load-and-store-intrinsics。
LLVM IR 为所有架构提供了一个共同的竞技场。它提供了在内存或聚合数据类型上执行数据操作的基本指令。在将 LLVM IR 降低到特定架构的过程中,架构可能会组合 IR 指令以生成它们特有的指令。在本章中,我们探讨了某些高级 IR 指令,并查看了一些示例。对于详细研究,请参考llvm.org/docs/LangRef.html,它提供了 LLVM IR 指令的权威资源。
在下一章中,你将学习如何优化 LLVM IR 以减少指令并生成干净的代码。
第四章. 基本 IR 转换
到目前为止,我们已经看到了 IR 如何独立于其目标,以及它如何被用来为特定的后端生成代码。为了为后端生成高效的代码,我们通过运行一系列分析和转换 Pass 来优化前端生成的 IR,使用 LLVM Pass 管理器。我们必须注意,编译器中发生的许多优化都发生在 IR 上,其中一个原因是 IR 是可重定位的,同一组优化对于多个目标都是有效的。这减少了为每个目标编写相同优化的工作量。也有一些特定于目标的优化;它们在选择 DAG 层级发生,我们将在后面看到。IR 成为优化目标的另一个原因是 LLVM IR 是 SSA 形式,这意味着每个变量只被分配一次,每个变量的新赋值本身就是一个新变量。这种表示的一个非常明显的优点是我们不必进行到达定义分析,其中某个变量被赋予另一个变量的值。SSA 表示法也有助于许多优化,如常量传播、死代码消除等。接下来,我们将看到一些重要的 LLVM 优化,LLVM Pass 基础设施的作用,以及我们如何使用 opt 工具执行不同的优化。
在本章中,我们将涵盖以下主题:
-
Opt 工具
-
Pass 和 Pass 管理器
-
在自己的 Pass 中使用其他 Pass 信息
-
IR 简化示例
-
IR 组合示例
Opt 工具
Opt 是在 LLVM IR 上运行的 LLVM 优化器和分析工具,用于优化 IR 或生成关于它的分析。我们在第一章中看到了对 opt 工具的非常基本的介绍,以及如何使用它来运行分析和转换 Pass。在本节中,我们将了解 opt 工具还能做什么。我们必须注意,opt 是一个开发者工具,它提供的所有优化都可以从前端调用。
使用 opt 工具,我们可以指定所需的优化级别,这意味着我们可以指定从 O0、O1、O2 到 O3(O0 是最不优化的代码,O3 是最优化代码)的优化级别。除了这些,还有一个优化级别 Os 或 Oz,它处理空间优化。调用这些优化之一的语法是:
$ opt -Ox -S input.ll
在这里,x 代表优化级别,其值可以是 0 到 3 或 s 或 z。这些优化级别与 Clang 前端指定的类似。-O0 表示没有优化,而 –O1 表示只启用少量优化。–O2 是一个适度的优化级别,而 –O3 是最高级别的优化,它与 –O2 类似,但它允许执行耗时更长或可能生成更大代码的优化(O3 级别并不保证代码是最优化和高效的,它只是说明编译器会尝试更多优化代码,在这个过程中可能会破坏某些东西)。–Os 表示针对大小的优化,基本上不运行会增加代码大小的优化(例如,它会移除 slp-vectorizer 优化)并执行减少代码大小的优化(例如,指令组合优化)。
我们可以直接将 opt 工具指向运行所需的具体 pass。这些 pass 可以是已经定义并列举在 llvm.org/docs/Passes.html 中的 pass,或者是我们自己编写的 pass。上述链接中列出的 pass 也在 -O1、-O2 和 -O3 的优化级别中运行。要查看在某个优化级别正在运行哪个 pass,请使用 -debug-pass=Structure 命令行选项与 opt 工具一起使用。
让我们通过一个例子来演示 O1 和 O2 优化级别的差异。O3 级别通常比 O2 级别多一个或两个遍历。所以,让我们举一个例子,看看 O2 优化级别如何优化代码。将测试代码写入 test.ll 文件:
define internal i32 @test(i32* %X, i32* %Y)
{
%A = load i32, i32* %X
%B = load i32, i32* %Y
%C = add i32 %A, %B
ret i32 %C
}
define internal i32 @caller(i32* %B)
{
%A = alloca i32
store i32 1, i32* %A
%C = call i32 @test(i32* %A, i32* %B)
ret i32 %C
}
define i32 @callercaller()
{
%B = alloca i32
store i32 2, i32* %B
%X = call i32 @caller(i32* %B)
ret i32 %X
}
在这个测试代码中,callercaller 函数调用 caller 函数,而 caller 函数又调用 test 函数,该函数执行两个数字的加法并返回值给其调用者,然后调用者将值返回给 callercaller 函数。
现在,运行 O1 和 O2 优化级别,如下所示:
$ opt -O1 -S test.ll > 1.ll
$ opt -O2 -S test.ll > 2.ll
以下截图显示了 O1 和 O2 优化级别优化代码的差异:

如我们所见,O2 优化了函数调用和 Add 操作,并直接从 callercaller 函数返回结果。这是由于 O2 优化运行了 always-inline pass,它内联了所有函数调用并将代码视为一个大的函数。然后,它还运行了 globaldce pass,该 pass 从代码中消除了不可达的内部部分。之后,它运行 constmerge,将重复的全局常量合并为单个常量。它还执行了一个全局值编号 pass,该 pass 消除了部分或完全冗余的指令,并消除了冗余的加载指令。
Pass 和 Pass Manager
LLVM 的 Pass 基础设施是 LLVM 系统的许多重要特性之一。有多个分析和优化遍历可以使用这个基础设施运行。LLVM 遍历的起点是 Pass 类,它是所有遍历的超类。我们需要从一些预定义的子类中继承,考虑到我们的遍历将要实现的功能。
-
ModulePass: 这是最高级的超类。通过继承这个类,我们可以一次性分析整个模块。模块内的函数可以不按特定顺序引用。要使用它,编写一个继承自
ModulePass子类的子类,并重载runOnModule函数。注意
在讨论其他
Pass类之前,让我们看看Pass类覆盖的三个虚拟方法:-
doInitialization: 这意味着执行不依赖于当前正在处理的函数的初始化操作。
-
runOn{Passtype}: 这是实现我们子类以实现遍历功能的方法。对于
FunctionPass,这将对应于runOnFunction,对于LoopPass,将对应于runOnLoop等。 -
doFinalization: 当
runOn{Passtype}为程序中的每个函数完成工作后,将调用此方法。
-
-
FunctionPass: 这些遍历操作在模块中的每个函数上执行,独立于模块中的其他函数。没有定义函数处理的顺序。它们不允许修改正在处理的函数以外的函数,也不允许从当前模块中添加或删除函数。要实现
FunctionPass,我们可能需要通过在runOnFunction方法中实现来重载前面提到的三个虚拟函数。 -
BasicBlockPass: 这些遍历操作一次处理一个基本块,独立于程序中存在的其他基本块。它们不允许添加或删除任何新的基本块或更改控制流图(CFG)。它们也不允许执行
FunctionPass不允许执行的操作。为了实现,它们可以重载FunctionPass的doInitialization和doFinalization方法,或者重载它们自己的虚拟方法,用于前面提到的两个方法和runOnBasicBlock方法。 -
LoopPass: 这些遍历操作针对函数中的每个循环进行处理,独立于函数内的其他所有循环。循环的处理方式是,最外层的循环最后执行。要实现
LoopPass,我们需要重载doInitialization、doFinalization和runOnLoop方法。
现在,让我们看看如何开始编写自定义遍历。让我们编写一个遍历,该遍历将打印所有函数的名称。
在开始编写遍历的实现之前,我们需要在代码的几个地方进行更改,以便遍历被识别并可以运行。
我们需要在 LLVM 树下创建一个目录。让我们创建一个目录,lib/Transforms/FnNamePrint。在这个目录中,我们需要创建一个 Makefile,内容如下,这将允许我们的 pass 被编译:
LEVEL = ../../..
LIBRARYNAME = FnNamePrint
LOADABLE_MODULE = 1
include $(LEVEL)/Makefile.common
这指定了所有 .cpp 文件都应该编译并链接成一个共享对象,该对象将在 build-folder 的 lib 文件夹中可用(build-folder/lib/FnNamePrint.so)。
现在,让我们开始编写实际的 pass 实现。我们需要在 lib/Transforms/FnNamePrint 中创建 pass 的源文件:让我们命名为 FnNamePrint.cpp。现在的第一步是选择正确的子类。在这种情况下,因为我们试图打印每个函数的名称,所以 FunctionPass 类将一次处理一个函数来满足我们的目的。此外,我们只打印函数的名称,而不修改其内部的内容,所以我们选择 FunctionPass 以保持简单。我们也可以使用 ModulePass,因为它是一个 Immutable Pass。
现在,让我们编写 pass 实现的源代码,它看起来是这样的:
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
struct FnNamePrint: public FunctionPass {
static char ID;
FnNamePrint () : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
errs() << "Function " << F.getName() << '\n';
return false;
}
};
}
char FnNamePrint::ID = 0;static RegisterPass< FnNamePrint > X("funcnameprint","Function Name Print", false, false);
在前面的代码中,我们首先 include 所需的头文件,并使用 llvm 命名空间:
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
我们将我们的 pass 声明为一个结构体,FnNamePrint,它是 FunctionPass 的子类。在 runOnFunction 中,我们实现了打印函数名的逻辑。最后返回的 bool 值表示我们是否在函数内进行了任何修改。如果进行了修改,则返回 True,否则返回 false。在我们的例子中,我们没有进行任何修改,所以返回 false。
struct FnNamePrint: public FunctionPass {
static char ID;
FnNamePrint () : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
errs() << "Function " << F.getName() << '\n';
return false;
}
};
}
然后,我们声明该 pass 的 ID,它用于识别该 pass:
char FnNamePrint::ID = 0;
最后,我们需要将 pass 注册到 Pass Manager 中。第一个参数是 opt 工具用于识别此 pass 的 Pass 名称。第二个参数是实际的 Pass 名称。第三个和第四个参数指定 pass 是否修改了 cfg 以及它是否是一个分析 pass。
static RegisterPass< FnNamePrint > X("funcnameprint","Function Name Print", false, false);
注意
pass 的实现已完成。现在,在我们使用它之前,我们需要使用 make 命令构建 LLVM,这将构建构建(build-folder/lib/FnNamePrint.so)文件夹中的共享对象。
现在,我们可以使用以下方式使用 opt 工具在测试用例上运行 pass:
$ opt -load path-to-llvm/build/lib/FnNamePrint.so -funcnameprint test.ll
load 命令行选项指定了从哪里获取 pass 的共享对象,–funcnameprint 是用于告诉 opt 工具运行我们编写的 pass 的选项。该 pass 将打印出测试用例中所有函数的名称。对于第一部分中的示例,它将打印出:
Function test
Function caller
Function callercaller
因此,我们开始了编写 Pass。现在,我们将看到 PassManager 类在 LLVM 中的重要性。
PassManager 类安排运行的 passes 以实现高效。PassManager 被所有运行 passes 的 LLVM 工具使用。确保 passes 之间的交互正确是 PassManager 的责任。因为它试图以优化的方式执行 passes,它必须了解 passes 如何相互交互以及 passes 之间的不同依赖关系。
一个 pass 可以指定对其他 passes 的依赖,即哪些 passes 需要在当前 pass 执行之前运行。它还可以指定由当前 pass 执行而失效的 passes。PassManager 在执行 pass 之前获取分析结果。我们将在后面看到 pass 如何指定此类依赖。
PassManager 的主要工作是避免反复计算分析结果。这是通过跟踪哪些分析可用、哪些已失效以及哪些分析是必需的来实现的。PassManager 跟踪分析结果的生命周期,并在不需要时释放持有分析结果的内存,从而实现最优的内存使用。
PassManager 将 passes 管道化以获得更好的内存和缓存结果,从而改善编译器的缓存行为。当给出一系列连续的 FunctionPass 时,它将在第一个函数上执行所有 FunctionPass,然后在第二个函数上执行所有 FunctionPass,依此类推。这改善了缓存行为,因为它只处理 LLVM 表示中的单个函数部分,而不是整个程序。
PassManager 还指定了 –debug-pass 选项,我们可以用它来查看一个 pass 如何与其他 pass 交互。我们可以使用 –debug-pass=Argument 选项查看所有运行的 passes。我们可以使用 –debug-pass=Structure 选项来查看 passes 的运行情况。它还会给出运行过的 passes 的名称。让我们以本章第一节的测试代码为例:
$ opt -O2 -S test.ll -debug-pass=Structure
$ opt -load /build-folder/lib/LLVMFnNamePrint.so test.ll -funcnameprint -debug-pass=Structure
Pass Arguments: -targetlibinfo -tti -funcnameprint -verify
Target Library Information
Target Transform Information
ModulePass Manager
FunctionPass Manager
Function Name Print
Module Verifier
Function test
Function caller
Function callercaller
在输出中,Pass Arguments 给出了运行的 passes,以下列表是运行每个 pass 所使用的结构。紧接在 ModulePass Manager 之后的 Passes 将显示每个模块运行的 passes(这里为空)。FunctionPass Manager 层级中的 passes 显示这些 passes 是按函数运行的(这里是 Function Name Print 和 Module Verifier),这是预期的结果。
PassManger 还提供了一些其他有用的标志,其中一些如下:
-
time-passes: 这提供了关于 pass 以及其他排队 passes 的时间信息。
-
stats: 这会打印每个 pass 的统计信息。
-
instcount: 这会收集所有指令的计数并报告它们。
–stats也必须传递给 opt 工具,以便instcount的结果可见。
在当前 Pass 中使用其他 Pass 信息
为了使遍历管理器最优运行,它需要知道遍历之间的依赖关系。每个遍历都可以自己声明其依赖关系:在执行此遍历之前需要执行的分析遍历以及当前遍历运行后将被使无效的遍历。为了指定这些依赖关系,一个遍历需要实现getAnalysisUsage方法。
virtual void getAnalysisUsage(AnalysisUsage &Info) const;
使用此方法,当前遍历可以通过在AnalysisUsage对象中填写详细信息来指定所需和无效化的集合。为了填写信息,遍历需要调用以下任何一种方法:
AnalysisUsage::addRequired<>方法
此方法安排在当前遍历之前执行一个遍历。一个例子是:对于内存复制优化,它需要别名分析的结果:
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.addRequired<AliasAnalysis>();
…
…
}
通过添加需要运行的遍历,确保在MemCpyOpt遍历之前运行Alias Analysis Pass。这也确保了如果Alias Analysis已被其他遍历使无效,它将在运行MemCpyOpt遍历之前运行。
AnalysisUsage::addRequiredTransitive<>方法
当分析链到其他分析以获取结果时,应使用此方法而不是addRequired方法。也就是说,当我们需要保留分析遍历的运行顺序时,我们使用此方法。例如:
void DependenceAnalysis::getAnalysisUsage(AnalysisUsage &AU) const {
…
AU.addRequiredTransitive<AliasAnalysis>();
AU.addRequiredTransitive<ScalarEvolution>();
AU.addRequiredTransitive<LoopInfo>();
}
在这里,DependenceAnalysis通过AliasAnalysis、ScalarEvolution和LoopInfo遍历链到结果。
AnalysisUsage::addPreserved<>方法
通过使用此方法,一个遍历可以指定它在运行时不会使其他遍历的分析无效:也就是说,如果存在,它将保留现有信息。这意味着后续需要该分析的遍历不需要再次运行。
例如,在前面看到的MemCpyOpt遍历的情况下,它需要AliasAnalysis的结果,并且还保留了它们。此外:
void getAnalysisUsage(AnalysisUsage &AU) const override {
……
AU.addPreserved<AliasAnalysis>();
…..
}
为了详细了解所有这些是如何相互关联和协同工作的,你可以选择任何转换遍历,查看源代码,你就会知道它们是如何从其他遍历获取信息以及如何使用这些信息的。
指令简化示例
在本节中,我们将看到如何在 LLVM 中将指令折叠成更简单的形式。在这里,不会创建新的指令。指令简化包括常量折叠:
sub i32 2, 1 -> 1
即,它将sub指令简化为常量值1。
它也可以处理非常量操作数:
or i32 %x, 0 -> %x
它返回变量%x的值
and i32 %x %x -> %x
在这种情况下,它返回一个已存在的值。
简化指令的方法实现位于lib/Analysis/InstructionSimplify.cpp。
处理指令简化的某些重要方法包括:
-
SimplifyBinOp 方法:此方法用于简化二进制运算,如加法、减法和乘法等。它的函数签名如下:
static Value *SimplifyBinOp(unsigned Opcode, Value *LHS, Value *RHS, const Query &Q, unsigned MaxRecurse)
在这里,我们通过 Opcode 指的是我们试图简化的操作符指令。LHS 和 RHS 是操作符两边的操作数。MaxRecurse 是我们指定的递归级别,在此之后,例程必须停止尝试简化指令。
在这个方法中,我们对 Opcode 有一个 switch 案例处理:
switch (Opcode) {
使用这个 Opcode,该方法决定需要调用哪个函数进行简化。以下是一些方法:
-
SimplifyAddInst:此方法尝试在操作数已知时折叠
Add操作符的结果。以下是一些折叠的例子:X + undef -> undef X + 0 -> X X + (Y - X) -> Y or (Y - X) + X -> Y
函数 static Value *SimplifyAddInst(Value *Op0, Value *Op1, bool isNSW, bool isNUW, const Query &Q, unsigned MaxRecurse ) 中最后简化的代码看起来像这样:
if (match(Op1, m_Sub(m_Value(Y), m_Specific(Op0))) ||
match(Op0, m_Sub(m_Value(Y), m_Specific(Op1))))
return Y;
在这里,第一个条件匹配表达式中的 (Y-X) 值,因为 Operand1: m_Value(Y) 表示 Y 的值,而 m_Specific(Op0) 表示 X。一旦匹配成功,它将表达式折叠为常量值 Y 并返回它。对于条件的第二部分也是类似的情况:
-
SimplifySubInst:此方法尝试在操作符已知时折叠
subtract操作符的结果。以下是一些相同示例:X - undef -> undef X - X -> 0 X - 0 -> X X - (X - Y) -> Y
指令匹配和折叠的执行方式类似于 SimplifyAddInst 中所示:
-
SimplifyAndInst:与前面两种方法类似,它尝试折叠逻辑运算符 And 的结果。以下是一些示例:
A & ~A = ~A & A = 0
在该方法中,代码看起来像这样:
if (match(Op0, m_Not(m_Specific(Op1))) ||
match(Op1, m_Not(m_Specific(Op0))))
return Constant::getNullValue(Op0->getType());
在这里,它尝试匹配 A 和 ~A,并在匹配到条件时返回一个 Null 值,即 0。
因此,我们已经看到了一些指令简化的例子。那么,如果我们可以用一组更有效的指令替换一组指令,我们该怎么办呢?
指令组合
指令组合是 LLVM 传递和编译技术,其中我们用更有效且在更少的机器周期内执行相同结果的指令替换一系列指令。指令组合不会改变程序的 CFG,主要用于代数简化。指令组合与指令简化的主要区别在于,在指令简化中我们不能生成新的指令,而在指令组合中是可能的。此传递通过指定 opt 工具的 –instcombine 参数来运行,并在 lib/transforms/instcombine 文件夹中实现。instcombine 传递组合
%Y = add i32 %X, 1
%Z = add i32 %Y, 1
into:
%Z = add i32 %X, 2
它已删除一个冗余的 add 指令,因此将两个 add 指令合并为一个。
LLVM 页面指出,此传递确保在程序上执行以下规范化的操作:
-
二元运算符的常数操作数被移动到 RHS。
-
带有常数操作数的位运算符与位移运算一起分组,首先执行位移运算,然后是 'or' 操作,'and' 操作,最后是 'xor' 操作。
-
如果可能,比较运算符从 <,>,<=,>= 转换为 == 或 !=。
-
所有操作布尔值的
cmp指令都被替换为逻辑操作。 -
添加 X,X 表示为 X*2,即 X<<1
-
常数为 2 的幂的乘法器被转换为位移操作。
这个过程从 bool InstCombiner::runOnFunction(Function &F) 开始,该函数位于 InstructionCombining.cpp 文件中。在 lib/Transform/InstCombine 文件夹下有不同文件,用于组合与不同指令相关的指令。在尝试组合指令之前,这些方法试图简化它们。其中一些用于简化 instcombine 模块的简化方法包括:
-
SimplifyAssociativeOrCommutative 函数:它对具有结合律或交换律的运算符进行简化。对于交换律运算符,它按复杂度递增的顺序从右到左对操作数进行排序。对于形式为 "
(X op Y) op Z" 的结合律运算,如果 (Y op Z) 可以简化,则将其转换为 "X op (Y op Z)"。 -
tryFactorization 函数:该方法尝试通过使用运算符的交换律和分配律提取公共项来简化二进制运算。例如,
(A*B)+(A*C)被简化为A*(B+C)。
现在,让我们看看指令组合。如前所述,不同的功能在不同的文件中实现。让我们以一个示例测试代码为例,看看在哪里添加代码,以便为我们的测试代码执行指令组合。
让我们在 test.ll 中编写测试代码,以匹配模式 (A | (B ^ C)) ^ ((A ^ C) ^ B),该模式可以简化为 (A & (B ^ C)):
define i32 @testfunc(i32 %x, i32 %y, i32 %z) {
%xor1 = xor i32 %y, %z
%or = or i32 %x, %xor1
%xor2 = xor i32 %x, %z
%xor3 = xor i32 %xor2, %y
%res = xor i32 %or, %xor3
ret i32 %res
}
LLVM 中处理 "And"、"Or" 和 "Xor" 等运算符的代码位于 lib/Transforms/InstCombine/InstCombineAndOrXor.cpp 文件中。
在 InstCombineAndOrXor.cpp 文件中,在 InstCombiner::visitXor(BinaryOperator &I) 函数中,转到 if 条件 If (Op0I && Op1I) 并添加以下代码片段:
If (match(Op01, m_Or(m_Xor(m_Value(B), m_Value(C)), m_Value(A)))
&& match(Op1I, m_Xor( m_Xor(m_Specific(A), m_Specific(C)), m_Specific(B)))) {
return BinaryOperator::CreateAnd(A, Builder->CreateXor(B,C));
}
如此明显,添加的代码是为了匹配模式 (A | (B ^ C)) ^ ((A ^ C) ^ B) 并在匹配时返回 (A & (B ^ C))。
要测试代码,构建 LLVM 并使用此测试代码运行 instcombine Pass,查看输出。
$ opt –instcombine –S test.ll
define i32 @testfunc(i32 %x, i32 %y, i32 %z) {
%1 = xor i32 %y, %z
%res = and i32 %1, %x
ret i32 %res
}
因此,输出显示现在只需要一个 xor 和一个 and 操作,而不是之前所需的四个 xor 和一个 or 操作。
要理解和添加更多转换,您可以查看 InstCombine 文件夹中的源代码。
摘要
因此,在本章中,我们探讨了如何将简单的转换应用于 IR。我们探讨了 opt 工具、LLVM Pass 基础设施、Passmanager 以及如何在 Pass 之间使用信息。我们以指令简化和指令组合的示例结束本章。在下一章中,我们将看到一些更高级的优化,如循环优化、标量演化等,在这些优化中,我们将操作代码块而不是单个指令。
第五章:高级 IR 块转换
在上一章中,我们已经介绍了一些优化,这些优化主要在指令级别。在本章中,我们将探讨块级别的优化,我们将优化一段代码到一个更简单的形式,这使得代码更加高效。我们将首先探讨在 LLVM 中如何表示循环,使用支配关系和 CFG 来优化循环。我们将使用循环简化(LoopSimplify)和循环不变式代码移动优化来进行循环处理。然后我们将看到标量值在程序执行过程中的变化,以及如何将这种标量演化优化的结果用于其他优化。然后我们将探讨 LLVM 如何表示其内建函数,称为 LLVM 内联函数。最后,我们将探讨 LLVM 通过理解其向量化方法来处理并行概念。
在本章中,我们将探讨以下主题:
-
循环处理
-
标量演化
-
LLVM 内联函数
-
向量化
循环处理
在开始循环处理和优化之前,我们必须对 CFG 和支配信息的概念有一个初步的了解。CFG 是程序的控制流图,它展示了程序如何通过各种基本块被执行。通过支配信息,我们了解到 CFG 中各个基本块之间的关系。
在 CFG 中,我们说节点 d 支配节点 n,如果通过 n 的每条路径(从输入到输出的路径)都必须通过 d。这表示为 d -> n。图 G = (V, E),其中 V 是基本块的集合,E 是定义在 V 上的支配关系,被称为支配树。
让我们通过一个示例来展示程序的控制流图(CFG)和相应的支配树。
在此处放置示例代码:
void fun() {
int iter, a, b;
for (iter = 0; iter < 10; iter++) {
a = 5;
if (iter == a)
b = 2;
else
b = 5;
}
}
前述代码的 CFG 看起来如下:

从你所学的支配关系和支配树,前一个 CFG 的支配树看起来如下:

第一幅图显示了前述代码的 CFG,下一幅图显示了相同 CFG 的支配树。我们已经对 CFG 的各个组件进行了编号,我们可以看到在 CFG 中,2 支配着 3,2 也支配着 4、5 和 6。3 支配着 4、5 和 6,并且是这些节点的直接支配者。4 和 5 之间没有支配关系。6 不是 5 的支配者,因为存在通过 4 的另一个路径,同样地,由于同样的原因,4 也不支配 6。
LLVM 中的所有循环优化和转换都源自于位于 lib/Analysis 目录下的 LoopPass.cpp 文件中实现的 LoopPass 类。LPPassManager 类负责处理所有的 LoopPasses。
开始处理循环最重要的类是LoopInfo类,它用于识别代码中的自然循环以及了解 CFG 中各种节点的深度。自然循环是 CFG 中的循环结构。为了在 CFG 中定义一个自然循环,我们必须知道什么是回边:它是在 CFG 中源节点支配目标节点的边。一个自然循环可以通过一个回边a->d来定义,它定义了 CFG 的一个子图,其中d是头节点,它包含所有可以到达a而不必到达d的其他基本块。
我们可以在前面的图中看到回边6->2形成了一个由节点2、3、4、5和6组成的自然循环。
下一个重要步骤是将循环简化为规范形式,这包括向循环中插入一个预头节点,这反过来又确保从循环外部只有一个入口边到循环头。它还插入循环退出块,确保所有从循环退出的块只有来自循环内部的先导。这些预头节点和退出块的插入有助于后续的循环优化,例如循环独立代码移动。
循环简化还确保循环只有一个回边,即如果循环头有超过两个先导(从预头节点块和多个锁存器到循环),我们只调整这个循环锁存器。一种实现方式是插入一个新块,这个新块是所有回边的目标,并使这个新块跳转到循环头。让我们看看循环简化遍历后循环看起来如何。我们将能够看到插入了一个预头节点,创建了新的退出块,并且只有一个回边。

现在,在从LoopInfo获取所需信息并将循环简化为规范形式之后,我们将探讨一些循环优化。
主要的循环优化之一是循环不变代码移动(LICM)优化。这个遍历尝试尽可能从循环体中移除代码。移除代码的条件是这段代码在循环内是不变的,即这部分代码的输出不依赖于循环执行,并且它将在循环的每次迭代中保持相同。这是通过将这段代码移动到预头节点块或将代码移动到退出块来实现的。这个遍历在lib/TransformsScalar/LICM.cpp文件中实现。如果我们查看循环的代码,我们会看到它需要在运行之前运行LoopInfo和LoopSimplify遍历。它还需要AliasAnalysis信息。别名分析是必要的,以便将循环不变加载和调用移出循环。如果没有加载和调用在循环内部与存储的任何内容别名,我们可以将这些移出循环。这也帮助了内存的标量提升。
让我们通过一个例子来看看 LICM 是如何完成的。
让我们在文件 licm.ll 中编写这个测试用例:
$ cat licm.ll
define void @func(i32 %i) {
Entry:
br label %Loop
Loop:
%j = phi i32 [ 0, %Entry ], [ %Val, %Loop ]
%loopinvar = mul i32 %i, 17
%Val = add i32 %j, %loopinvar
%cond = icmp eq i32 %Val, 0
br i1 %cond, label %Exit, label %Loop
Exit:
ret void
}
这个 testcase 在测试代码中有一个由 Loop 块表示的循环,循环条件是 br i1 %cond,label %Exit,label %Loop(循环的 Latch 部分)。我们可以看到 %j 值,它是作为归纳变量被使用的,是在使用 phi 指令之后推导出来的。基本上,它告诉如果控制来自 Entry 块,则选择值 0,如果控制来自 Loop 块,则选择 %Val。在这里,不变代码可以看作是 %loopinvar = mul i32 %i, 17,因为 %loopinvar 的值不依赖于循环的迭代次数,只依赖于函数参数。所以当我们运行 LICM 过滤器时,我们期望这个值能够从循环中提升出来,从而防止在循环的每次迭代中计算它。
让我们运行 licm 过滤器并查看输出:
$ opt -licm licm.ll -o licm.bc
$ llvm-dis licm.bc -o licm_opt.ll
$ cat licm_opt.ll
; ModuleID = 'licm.bc'
define void @func(i32 %i) {
Entry:
%loopinvar = mul i32 %i, 17
br label %Loop
Loop:
; preds = %Loop, %Entry
%j = phi i32 [ 0, %Entry ], [ %Val, %Loop ]
%Val = add i32 %j, %loopinvar
%cond = icmp eq i32 %Val, 0
br i1 %cond, label %Exit, label %Loop
Exit:
; preds = %Loop
ret void
}
正如我们在输出中看到的,计算 %loopinvar = mul i32 %i, 17 已经从循环中提升出来,这正是我们期望的输出。
我们还有许多其他的循环优化,例如循环旋转、循环交换、循环展开等等。这些优化的源代码可以在 LLVM 文件夹 lib/Transforms/Scalar 下找到,以获得对这些优化的更多理解。在下一节中,我们将看到标量演化的概念。
标量演化
通过标量演化,我们指的是一个标量值在程序中随着代码执行而变化的情况。我们查看一个特定的标量值,并观察它是如何被推导出来的,它依赖于哪些其他元素,这些元素是否在编译时已知,以及执行了哪些操作。我们需要查看代码块而不是单个指令。一个标量值由两个元素组成,一个变量和一个常数步长的操作。构建这个标量值的变量元素在编译时是未知的,其值只能在运行时知道。另一个元素是常数部分。这些元素本身可能可以递归地分解成其他元素,例如一个常数、一个未知值或一个算术操作。
这里的主要思想是在编译时查看包含未知部分的完整标量值,并观察这个值在执行过程中的变化,并尝试利用这一点进行优化。一个例子是移除一个与程序中其他某个值具有相似标量演化的冗余值。
在 LLVM 中,我们可以使用标量演化来分析包含常见整数算术运算的代码。
在 LLVM 中,ScalarEvolution 类在 include/llvm/Analysis 中实现,这是一个 LLVM 通过,可以用来分析循环中的标量表达式。它能够识别通用归纳变量(循环中值是迭代次数函数的变量)并使用 SCEV 类的对象来表示它们,SCEV 类用于表示程序中分析的表达式。使用这种分析可以获得迭代次数和其他重要分析。这种标量演化分析主要用于归纳变量替换和循环强度降低。
现在我们举一个例子,并运行标量演化通过它,看看它生成什么输出。
编写一个包含循环和一些循环内标量值的测试用例scalevl.ll。
$ cat scalevl.ll
define void @fun() {
entry:
br label %header
header:
%i = phi i32 [ 1, %entry ], [ %i.next, %body ]
%cond = icmp eq i32 %i, 10
br i1 %cond, label %exit, label %body
body:
%a = mul i32 %i, 5
%b = or i32 %a, 1
%i.next = add i32 %i, 1
br label %header
exit:
ret void
}
在这个测试用例中,我们有一个由头和体块组成的循环,其中%a和%b是感兴趣的循环体内的标量。让我们运行标量演化通过这个,看看输出结果:
$ opt -analyze -scalar-evolution scalevl.ll
Printing analysis 'Scalar Evolution Analysis' for function 'fun':
Classifying expressions for: @fun
%i = phi i32 [ 1, %entry ], [ %i.next, %body ]
--> {1,+,1}<%header> U: [1,11) S: [1,11) Exits: 10
%a = mul i32 %i, 5
--> {5,+,5}<%header> U: [5,51) S: [5,51) Exits: 50
%b = or i32 %a, 1
--> %b U: [1,0) S: full-set Exits: 51
%i.next = add i32 %i, 1
--> {2,+,1}<%header> U: [2,12) S: [2,12) Exits: 11
Determining loop execution counts for: @fun
Loop %header: backedge-taken count is 9
Loop %header: max backedge-taken count is 9
如我们所见,标量演化通过输出的范围显示了特定变量(U代表无符号范围,S代表有符号范围,这里两者相同)的值域以及退出值,即循环运行其最后一次迭代时该变量的值。例如,%i的值域为[1,11),这意味着起始迭代值为1,当%i的值变为11时,条件%cond = icmp eq i32 %i, 10变为假,循环中断。因此,当%i退出循环时的值为10,这在输出中用Exits: 10表示。
{x,+,y}形式的值,例如{2,+,1},表示加法递归,即在循环执行期间改变值的表达式,其中 x 代表第 0 次迭代的基值,y 代表在每次后续迭代中添加到它上面的值。
输出还显示了循环在第一次运行后的迭代次数。这里,它显示了9的值,表示回边取用,即循环总共运行了10次。最大回边取用值是永远不会小于回边取用值的最小值,这里为9。
这是这个示例的输出,你可以尝试一些其他的测试用例,看看这个通过会输出什么。
LLVM 内置函数
内置函数是编译器内部构建的函数。编译器知道如何以最优化方式实现这些函数的功能,并为特定后端替换成一组机器指令。通常,函数的代码会内联插入,从而避免函数调用的开销(在许多情况下,我们确实调用了库函数。例如,对于列在llvm.org/docs/LangRef.html#standard-c-library-intrinsics中的函数,我们调用libc)。这些在其他编译器中也被称为内置函数。
在 LLVM 中,这些内建函数在 IR 级别的代码优化期间引入(程序代码中编写的内建函数可以通过前端直接发出)。这些函数名将以前缀"llvm."开头,这是 LLVM 中的一个保留词。这些函数始终是外部的,用户不能在其代码中指定这些函数的主体。在我们的代码中,我们只能调用这些内建函数。
在本节中,我们不会深入探讨细节。我们将通过一个示例来了解 LLVM 如何使用其自身的内建函数优化代码的某些部分。
让我们编写一段简单的代码:
$ cat intrinsic.cpp
int func()
{
int a[5];
for (int i = 0; i != 5; ++i)
a[i] = 0;
return a[0];
}
现在,使用 Clang 生成 IR 文件。使用以下命令,我们将得到包含未优化 IR 且没有任何内建函数的intrinsic.ll文件。
$ clang -emit-llvm -S intrinsic.cpp
现在,使用 opt 工具以 O1 优化级别优化 IR。
$ opt -O1 intrinsic.ll -S -o -
; ModuleID = 'intrinsic.ll'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
; Function Attrs: nounwind readnone uwtable
define i32 @_Z4funcv() #0 {
%a = alloca [5 x i32], align 16
%a2 = bitcast [5 x i32]* %a to i8*
call void @llvm.memset.p0i8.i64(i8* %a2, i8 0, i64 20, i32 16, i1 false)
%1 = getelementptr inbounds [5 x i32], [5 x i32]* %a, i64 0, i64 0
%2 = load i32, i32* %1, align 16
ret i32 %2
}
; Function Attrs: nounwind argmemonly
declare void @llvm.memset.p0i8.i64(i8* nocapture, i8, i64, i32, i1) #1
这里需要注意的重要优化是调用 LLVM 内建函数llvm.memset.p0i8.i64以填充数组值为0。内建函数可以用于在代码中实现向量化和并行化,从而生成更好的代码。它可能会调用libc库中最优化的memset调用版本,并且如果没有使用此函数,可能会选择完全省略此函数。
调用中的第一个参数指定了数组"a",即需要填充值的目标数组。第二个参数指定了要填充的值。调用中的第三个参数指定了要填充的字节数。第四个参数指定了目标值的对齐方式。最后一个参数用于确定这是一个易失性操作还是非易失性操作。
在 LLVM 中有一系列这样的内建函数,其列表可以在llvm.org/docs/LangRef.html#intrinsic-functions找到。
向量化
向量化是编译器的重要优化手段,我们可以将代码向量化以一次执行多个数据集上的指令。高级目标架构通常具有向量寄存器和向量指令——其中广泛的数据类型(通常是 128/256 位)可以加载到向量寄存器中,并且可以在这些寄存器集上执行操作,同时执行两个、四个,有时甚至八个操作,其成本与一个标量操作相当。
在 LLVM 中有两种向量化类型——超字级并行(SLP)和循环向量化。循环向量化处理循环中的向量化机会,而 SLP 向量化处理基本块中的直接代码的向量化。
向量指令执行单指令多数据(SIMD)操作;在多个数据通道上并行执行相同的操作。

让我们看看如何在 LLVM 基础设施中实现 SLP 向量化。
如代码本身所描述,LLVM 中 SLP 矢量化实现的灵感来源于 Ira Rosen、Dorit Nuzman 和 Ayal Zaks 在论文 GCC 中的循环感知 SLP 中描述的工作。LLVM SLP 矢量化传递实现自底向上的 SLP 矢量化器。它检测可以组合成向量存储的连续存储操作。接下来,它尝试将存储操作组合成向量存储。然后,它尝试使用 use-def 链构造可矢量化树。如果找到了有利的树,SLP 矢量化器就会在树上执行矢量化。
SLP 矢量化有三个阶段:
-
识别模式并确定它是否是有效的矢量化模式
-
确定矢量化代码是否有利可图
-
如果步骤 1 和 2 都成立,那么就矢量化代码
让我们来看一个例子:
考虑将两个数组的 4 个连续元素添加到第三个数组中。
int a[4], b[4], c[4];
void addsub() {
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
a[3] = b[3] + c[3];
}
前一种表达式的 IR 将看起来像这样:
; ModuleID = 'addsub.c'
@a = global [4 x i32] zeroinitializer, align 4
@b = global [4 x i32] zeroinitializer, align 4
@c = global [4 x i32] zeroinitializer, align 4
; Function Attrs: nounwind
define void @addsub() {
entry:
%0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0)
%1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0)
%add = add nsw i32 %1, %0
store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0)
%2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1)
%3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1)
%add1 = add nsw i32 %3, %2
store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1)
%4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2)
%5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2)
%add2 = add nsw i32 %5, %4
store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2)
%6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)
%7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)
%add3 = add nsw i32 %7, %6
store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)
ret void
}
前一个模式的表达式树可以可视化为一系列的存储和加载操作:

对于前面的表达式树,自底向上的 SLP 矢量化首先构建一个以存储指令开始的链:
// Use the bottom up slp vectorizer to construct chains that start
// with store instructions.
BoUpSLP R(&F, SE, TTI, TLI, AA, LI, DT, AC);
然后,它扫描前面代码中已经构建的树中的所有给定基本块中的存储操作:
// Scan the blocks in the function in post order.
for (auto BB : post_order(&F.getEntryBlock())) {
// Vectorize trees that end at stores.
if (unsigned count = collectStores(BB, R)) {
(void)count;
DEBUG(dbgs() << "SLP: Found " << count << " stores to vectorize.\n");
Changed |= vectorizeStoreChains(R);
}
// Vectorize trees that end at reductions.
Changed |= vectorizeChainsInBlock(BB, R);
}
collectStores() 函数收集所有存储引用。
unsigned SLPVectorizer::collectStores(BasicBlock *BB, BoUpSLP &R) {
unsigned count = 0;
StoreRefs.clear();
const DataLayout &DL = BB->getModule()->getDataLayout();
for (Instruction &I : *BB) {
StoreInst *SI = dyn_cast<StoreInst>(&I);
if (!SI)
continue;
// Don't touch volatile stores.
if (!SI->isSimple())
continue;
// Check that the pointer points to scalars.
Type *Ty = SI->getValueOperand()->getType();
if (!isValidElementType(Ty))
continue;
// Find the base pointer.
Value *Ptr = GetUnderlyingObject(SI->getPointerOperand(), DL);
// Save the store locations.
StoreRefs[Ptr].push_back(SI);
count++;
}
return count;
}
SLPVectorizer::vectorizeStoreChains() 函数有三个步骤和对每个步骤的函数调用:
bool SLPVectorizer::vectorizeStoreChain(ArrayRef<Value *> Chain,
int CostThreshold, BoUpSLP &R,
unsigned VecRegSize) {
…
…
R.buildTree(Operands);
int Cost = R.getTreeCost();
DEBUG(dbgs() << "SLP: Found cost=" << Cost << " for VF=" << VF << "\n");
if (Cost < CostThreshold) {
DEBUG(dbgs() << "SLP: Decided to vectorize cost=" << Cost << "\n");
R.vectorizeTree();
…
…
}
第一步是识别模式。函数 buildTree() 随后递归地构建树,如前面的可视化所示。
void BoUpSLP::buildTree(ArrayRef<Value *> Roots,
ArrayRef<Value *> UserIgnoreLst) {
…
…
buildTree_rec(Roots, 0);
…
…
}
对于我们的给定例子,它将确定所有存储操作的操作数都是二进制加法操作:
void BoUpSLP::buildTree_rec(ArrayRef<Value *> VL, unsigned Depth) {
…
…
case Instruction::Add:
newTreeEntry(VL, true);
DEBUG(dbgs() << "SLP: added a vector of bin op.\n");
// Sort operands of the instructions so that each side is more
// likely to have the sam opcode
if (isa<BinaryOperator>(VL0) && VL0->isCommutative()) {
ValueList Left, Right;
reorderInputsAccordingToOpcode(VL, Left, Right);
buildTree_rec(Left, Depth + 1);
buildTree_rec(Right, Depth + 1);
return;
}
…
…
}
当遇到二进制运算符 ADD 时,它会在 ADD 操作的左右操作数(在我们的情况下都是 Load)上递归地构建树(调用相同的函数):
case Instruction::Load: {
// Check that a vectorized load would load the same memory as a // scalar load.
// For example we don't want vectorize loads that are smaller than 8 bit.
// Even though we have a packed struct {<i2, i2, i2, i2>} LLVM treats
// loading/storing it as an i8 struct. If we vectorize loads/stores from
// such a struct we read/write packed bits disagreeing with the
// unvectorized version.
const DataLayout &DL = F->getParent()->getDataLayout();
Type *ScalarTy = VL[0]->getType();
if (DL.getTypeSizeInBits(ScalarTy) != DL.getTypeAllocSizeInBits(ScalarTy)) {
BS.cancelScheduling(VL);
newTreeEntry(VL, false);
DEBUG(dbgs() << "SLP: Gathering loads of non-packed type.\n");
return;
}
// Check if the loads are consecutive or of we need to swizzle them.
for (unsigned i = 0, e = VL.size() - 1; i < e; ++i) {
LoadInst *L = cast<LoadInst>(VL[i]);
if (!L->isSimple()) {
BS.cancelScheduling(VL);
newTreeEntry(VL, false);
DEBUG(dbgs() << "SLP: Gathering non-simple loads.\n");
return;
}
if (!isConsecutiveAccess(VL[i], VL[i + 1], DL)) {
if (VL.size() == 2 && isConsecutiveAccess(VL[1], VL[0], DL)) {
++NumLoadsWantToChangeOrder;
}
BS.cancelScheduling(VL);
newTreeEntry(VL, false);
DEBUG(dbgs() << "SLP: Gathering non-consecutive loads.\n");
return;
}
}
++NumLoadsWantToKeepOrder;
newTreeEntry(VL, true);
DEBUG(dbgs() << "SLP: added a vector of loads.\n");
return;
}
在构建树的过程中,有几个检查来验证树是否可以矢量化。例如,在前面的例子中,当在树之间遇到加载操作时,会检查它们是否是连续加载。在我们的表达式树中,LHS 中的树之间的加载操作(b[0]、b[1]、b[2] 和 b[3])正在访问连续的内存位置。同样,RHS 中的树之间的加载操作(c[0]、c[1]、c[2] 和 c[3])也在访问连续的内存位置。如果给定的操作中的任何检查失败,则树的构建将被终止,代码不会被矢量化。
在识别了模式和构建了向量树之后,下一步是获取构建的树的矢量化成本。这实际上是指如果矢量化,与当前标量形式的树的成本相比的成本。如果矢量化成本低于标量成本,则矢量化树是有益的:
int BoUpSLP::getTreeCost() {
int Cost = 0;
DEBUG(dbgs() << "SLP: Calculating cost for tree of size "
<< VectorizableTree.size() << ".\n");
// We only vectorize tiny trees if it is fully vectorizable.
if (VectorizableTree.size() < 3 && !isFullyVectorizableTinyTree()) {
if (VectorizableTree.empty()) {
assert(!ExternalUses.size() && "We should not have any external users");
}
return INT_MAX;
}
unsigned BundleWidth = VectorizableTree[0].Scalars.size();
for (unsigned i = 0, e = VectorizableTree.size(); i != e; ++i) {
int C = getEntryCost(&VectorizableTree[i]);
DEBUG(dbgs() << "SLP: Adding cost " << C << " for bundle that starts with " << *VectorizableTree[i].Scalars [0] << " . \n" );
Cost += C;
}
SmallSet<Value *, 16> ExtractCostCalculated;
int ExtractCost = 0;
for (UserList::iterator I = ExternalUses.begin(), E = ExternalUses.end();
I != E; ++I) {
// We only add extract cost once for the same scalar.
if (!ExtractCostCalculated.insert(I->Scalar).second)
continue;
// Uses by ephemeral values are free (because the ephemeral value will be
// removed prior to code generation, and so the extraction will be
// removed as well).
if (EphValues.count(I->User))
continue;
VectorType *VecTy = VectorType::get(I->Scalar->getType(), BundleWidth);
ExtractCost +=
TTI->getVectorInstrCost(Instruction::ExtractElement, VecTy, I->Lane);
}
Cost += getSpillCost();
DEBUG(dbgs() << "SLP: Total Cost " << Cost + ExtractCost << ".\n");
return Cost + ExtractCost;
}
在这里需要关注的一个重要接口是TargetTransformInfo(TTI),它提供了访问用于 IR 级别转换的代码生成接口。在我们的 SLP 向量化中,TTI 用于获取构建的向量树中向量指令的成本:
int BoUpSLP::getEntryCost(TreeEntry *E) {
…
…
case Instruction::Store: {
// We know that we can merge the stores. Calculate the cost.
int ScalarStCost = VecTy->getNumElements() *
TTI->getMemoryOpCost(Instruction::Store, ScalarTy, 1, 0);
int VecStCost = TTI->getMemoryOpCost(Instruction::Store, VecTy, 1, 0);
return VecStCost - ScalarStCost;
}
…
…
}
在同一个函数中,也计算了向量加法的成本:
case Instruction::Add: {
// Calculate the cost of this instruction.
int ScalarCost = 0;
int VecCost = 0;
if (Opcode == Instruction::FCmp || Opcode == Instruction::ICmp ||
Opcode == Instruction::Select) {
VectorType *MaskTy = VectorType::get(Builder.getInt1Ty(), VL.size());
ScalarCost =
VecTy->getNumElements() *
TTI->getCmpSelInstrCost(Opcode, ScalarTy, Builder.getInt1Ty());
VecCost = TTI->getCmpSelInstrCost(Opcode, VecTy, MaskTy);
} else {
// Certain instructions can be cheaper to vectorize if they have
// a constant second vector operand.
TargetTransformInfo::OperandValueKind Op1VK =
TargetTransformInfo::OK_AnyValue;
TargetTransformInfo::OperandValueKind Op2VK =
TargetTransformInfo::OK_UniformConstantValue;
TargetTransformInfo::OperandValueProperties Op1VP =
TargetTransformInfo::OP_None;
TargetTransformInfo::OperandValueProperties Op2VP =
TargetTransformInfo::OP_None;
// If all operands are exactly the same ConstantInt then set the
// operand kind to OK_UniformConstantValue.
// If instead not all operands are constants, then set the operand kind
// to OK_AnyValue. If all operands are constants but not the
// same, then set the operand kind to OK_NonUniformConstantValue.
ConstantInt *CInt = nullptr;
for (unsigned i = 0; i < VL.size(); ++i) {
const Instruction *I = cast<Instruction>(VL[i]);
if (!isa<ConstantInt>(I->getOperand(1))) {
Op2VK = TargetTransformInfo::OK_AnyValue;
break;
}
if (i == 0) {
CInt = cast<ConstantInt>(I->getOperand(1));
continue;
}
if (Op2VK == TargetTransformInfo::OK_UniformConstantValue &&
CInt != cast<ConstantInt>(I->getOperand(1)))
Op2VK = TargetTransformInfo::OK_NonUniformConstantValue;
}
// FIXME: Currently cost of model modification for division by
// power of 2 is handled only for X86\. Add support for other
// targets.
if (Op2VK == TargetTransformInfo::OK_UniformConstantValue && CInt &&
CInt->getValue().isPowerOf2())
Op2VP = TargetTransformInfo::OP_PowerOf2;
ScalarCost = VecTy->getNumElements() *
TTI->getArithmeticInstrCost(Opcode, ScalarTy, Op1VK, Op2VK, Op1VP, Op2VP);
VecCost = TTI->getArithmeticInstrCost(Opcode, VecTy, Op1VK, Op2VK, Op1VP, Op2VP);
}
return VecCost - ScalarCost;
}
在我们的例子中,整个表达式树的总成本为-12,这表明向量化这棵树是有利可图的。
最后,通过在树上调用函数R.vectorizeTree()来对树进行向量化:
Value *BoUpSLP::vectorizeTree() {
…
…
vectorizeTree(&VectorizableTree[0]);
…
…
}
让我们看看向量化过程遵循的所有步骤。注意,这需要'opt'工具的'Debug'构建版本。
$ opt -S -basicaa -slp-vectorizer -mtriple=aarch64-unknown-linuxgnu -mcpu=cortex-a57 addsub.ll –debug
Features:
CPU:cortex-a57
SLP: Analyzing blocks in addsub.
SLP: Found 4 stores to vectorize.
SLP: Analyzing a store chain of length 4.
SLP: Analyzing a store chain of length 4
SLP: Analyzing 4 stores at offset 0
SLP: bundle: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0)
SLP: initialize schedule region to store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0)
SLP: extend schedule region end to store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1)
SLP: extend schedule region end to store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2)
SLP: extend schedule region end to store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)
SLP: try schedule bundle [ store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0); store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1); store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2); store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)] in block entry
SLP: update deps of [ store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0); store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1); store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2); store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)]
SLP: update deps of / store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1)
SLP: update deps of / store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2)
SLP: update deps of / store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)
SLP: gets ready on update: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0)
SLP: We are able to schedule this bundle.
SLP: added a vector of stores.
SLP: bundle: %add = add nsw i32 %1, %0
SLP: extend schedule region start to %add = add nsw i32 %1, %0
SLP: try schedule bundle [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6] in block entry
SLP: update deps of [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6]
SLP: update deps of / %add1 = add nsw i32 %3, %2
SLP: update deps of / %add2 = add nsw i32 %5, %4
SLP: update deps of / %add3 = add nsw i32 %7, %6
SLP: schedule [ store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0); store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1); store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2); store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)]
SLP: gets ready (def): [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6]
SLP: We are able to schedule this bundle.
SLP: added a vector of bin op.
SLP: bundle: %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0)
SLP: extend schedule region start to %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0)
SLP: try schedule bundle [ %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0); %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1); %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2); %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)] in block entry
SLP: update deps of [ %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0); %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1); %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2); %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)]
SLP: update deps of / %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1)
SLP: update deps of / %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2)
SLP: update deps of / %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)
SLP: schedule [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6]
SLP: gets ready (def): [ %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0); %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1); %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2); %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)]
SLP: We are able to schedule this bundle.
SLP: added a vector of loads.
SLP: bundle: %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0)
SLP: extend schedule region start to %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0)
SLP: try schedule bundle [ %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0); %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1); %4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2); %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)] in block entry
SLP: update deps of [ %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0); %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1); %4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2); %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)]
SLP: update deps of / %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1)
SLP: update deps of / %4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2)
SLP: update deps of / %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)
SLP: gets ready on update: %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0)
SLP: We are able to schedule this bundle.
SLP: added a vector of loads.
SLP: Checking user: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0).
SLP: Internal user will be removed: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0).
SLP: Checking user: store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1).
SLP: Internal user will be removed: store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1).
SLP: Checking user: store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2).
SLP: Internal user will be removed: store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2).
SLP: Checking user: store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3).
SLP: Internal user will be removed: store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3).
SLP: Checking user: %add = add nsw i32 %1, %0.
SLP: Internal user will be removed: %add = add nsw i32 %1, %0.
SLP: Checking user: %add1 = add nsw i32 %3, %2.
SLP: Internal user will be removed: %add1 = add nsw i32 %3, %2.
SLP: Checking user: %add2 = add nsw i32 %5, %4.
SLP: Internal user will be removed: %add2 = add nsw i32 %5, %4.
SLP: Checking user: %add3 = add nsw i32 %7, %6.
SLP: Internal user will be removed: %add3 = add nsw i32 %7, %6.
SLP: Checking user: %add = add nsw i32 %1, %0.
SLP: Internal user will be removed: %add = add nsw i32 %1, %0.
SLP: Checking user: %add1 = add nsw i32 %3, %2.
SLP: Internal user will be removed: %add1 = add nsw i32 %3, %2.
SLP: Checking user: %add2 = add nsw i32 %5, %4.
SLP: Internal user will be removed: %add2 = add nsw i32 %5, %4.
SLP: Checking user: %add3 = add nsw i32 %7, %6.
SLP: Internal user will be removed: %add3 = add nsw i32 %7, %6.
SLP: Calculating cost for tree of size 4.
SLP: Adding cost -3 for bundle that starts with store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0) .
SLP: Adding cost -3 for bundle that starts with %add = add nsw i32 %1, %0 .
SLP: Adding cost -3 for bundle that starts with %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0) .
SLP: Adding cost -3 for bundle that starts with %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0) .
SLP: #LV: 0, Looking at %add = add nsw i32 %1, %0
SLP: #LV: 1 add, Looking at %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0)
SLP: #LV: 2 , Looking at %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0)
SLP: SpillCost=0
SLP: Total Cost -12.
SLP: Found cost=-12 for VF=4
SLP: Decided to vectorize cost=-12
SLP: schedule block entry
SLP: initially in ready list: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0)
SLP: schedule [ store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0); store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1); store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2); store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3)]
SLP: gets ready (def): [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6]
SLP: schedule [ %add = add nsw i32 %1, %0; %add1 = add nsw i32 %3, %2; %add2 = add nsw i32 %5, %4; %add3 = add nsw i32 %7, %6]
SLP: gets ready (def): [ %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0); %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1); %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2); %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)]
SLP: gets ready (def): [ %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0); %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1); %4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2); %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)]
SLP: schedule [ %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0); %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1); %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2); %4 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3)]
SLP: schedule [ %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0); %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1); %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2); %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3)]
SLP: Extracting 0 values .
SLP: Erasing scalar: store i32 %add, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 0).
SLP: Erasing scalar: store i32 %add1, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 1).
SLP: Erasing scalar: store i32 %add2, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 2).
SLP: Erasing scalar: store i32 %add3, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @a, i32 0, i32 3).
SLP: Erasing scalar: %add = add nsw i32 %8, %3.
SLP: Erasing scalar: %add1 = add nsw i32 %7, %2.
SLP: Erasing scalar: %add2 = add nsw i32 %6, %1.
SLP: Erasing scalar: %add3 = add nsw i32 %5, %0.
SLP: Erasing scalar: %8 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 0).
SLP: Erasing scalar: %7 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 1).
SLP: Erasing scalar: %6 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 2).
SLP: Erasing scalar: %5 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @c, i32 0, i32 3).
SLP: Erasing scalar: %3 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 0).
SLP: Erasing scalar: %2 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 1).
SLP: Erasing scalar: %1 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 2).
SLP: Erasing scalar: %0 = load i32, i32* getelementptr inbounds ([4 x i32], [4 x i32]* @b, i32 0, i32 3).
SLP: Optimizing 0 gather sequences instructions.
SLP: vectorized "addsub"
最终的向量化输出是:
; ModuleID = 'addsub.ll'
target triple = "aarch64-unknown-linuxgnu"
@a = global [4 x i32] zeroinitializer, align 4
@b = global [4 x i32] zeroinitializer, align 4
@c = global [4 x i32] zeroinitializer, align 4
define void @addsub() {
entry:
%0 = load <4 x i32>, <4 x i32>* bitcast ([4 x i32]* @b to <4 x i32>*), align 4
%1 = load <4 x i32>, <4 x i32>* bitcast ([4 x i32]* @c to <4 x i32>*), align 4
%2 = add nsw <4 x i32> %1, %0
store <4 x i32> %2, <4 x i32>* bitcast ([4 x i32]* @a to <4 x i32>*), align 4
ret void
}
摘要
在本章中,我们总结了编译器的优化部分,其中我们看到了块级优化。我们讨论了循环优化、标量演化、向量化和 LLVM 内建函数的例子。我们还了解了在 LLVM 中如何处理 SLP 向量化。然而,还有许多其他这样的优化你可以去了解并掌握。
在下一章中,我们将看到如何将这个中间表示(IR)转换为有向无环图。在selectionDAG级别也有一些优化,我们将对其进行探讨。
第六章:IR 到 Selection DAG 阶段
到上一章为止,我们看到了如何将前端语言转换为 LLVM IR。我们还看到了如何将 IR 转换为更优化的代码。经过一系列分析和转换过程后,最终的 IR 是最优化的机器无关代码。然而,IR 仍然是实际机器代码的抽象表示。编译器必须为目标架构生成执行代码。
LLVM 使用 DAG(有向无环图)来表示代码生成。其思路是将 IR 转换为SelectionDAG,然后经过一系列阶段——DAG 合并、合法化、指令选择、指令调度等,最终分配寄存器并生成机器代码。请注意,寄存器分配和指令调度是交织进行的。
在本章中,我们将介绍以下主题:
-
将 IR 转换为 selectionDAG
-
合法化 selectionDAG
-
优化 selectionDAG
-
指令选择
-
调度和生成机器指令
-
寄存器分配
-
代码生成
将 IR 转换为 selectionDAG
一个 IR 指令可以由一个 SDAG 节点表示。因此,整个指令集形成一个相互连接的有向无环图,每个节点对应一个 IR 指令。
例如,考虑以下 LLVM IR:
$ cat test.ll
define i32 @test(i32 %a, i32 %b, i32 %c) {
%add = add nsw i32 %a, %b
%div = sdiv i32 %add, %c
ret i32 %div
}
LLVM 提供了一个SelectionDAGBuilder接口来创建与 IR 指令对应的 DAG 节点。考虑以下二进制运算:
%add = add nsw i32 %a, %b
当遇到给定的 IR 时,将调用以下函数:
void SelectionDAGBuilder::visit(unsigned Opcode, const User &I) {
// Note: this doesn't use InstVisitor, because it has to work with
// ConstantExpr's in addition to instructions.
switch (Opcode) {
default: llvm_unreachable("Unknown instruction type encountered!");
// Build the switch statement using the Instruction.def file.
#define HANDLE_INST(NUM, OPCODE, CLASS) \
case Instruction::OPCODE: visit##OPCODE((const CLASS&)I); break;
#include "llvm/IR/Instruction.def"
}
}
根据操作码(此处为Add),将调用相应的访问函数。在这种情况下,调用visitAdd(),它进一步调用visitBinary()函数。visitBinary()函数如下:
void SelectionDAGBuilder::visitBinary(const User &I, unsigned OpCode) {
SDValue Op1 = getValue(I.getOperand(0));
SDValue Op2 = getValue(I.getOperand(1));
bool nuw = false;
bool nsw = false;
bool exact = false;
FastMathFlags FMF;
if (const OverflowingBinaryOperator *OFBinOp =
dyn_cast<const OverflowingBinaryOperator>(&I)) {
nuw = OFBinOp->hasNoUnsignedWrap();
nsw = OFBinOp->hasNoSignedWrap();
}
if (const PossiblyExactOperator *ExactOp =
dyn_cast<const PossiblyExactOperator>(&I))
exact = ExactOp->isExact();
if (const FPMathOperator *FPOp = dyn_cast<const FPMathOperator>(&I))
FMF = FPOp->getFastMathFlags();
SDNodeFlags Flags;
Flags.setExact(exact);
Flags.setNoSignedWrap(nsw);
Flags.setNoUnsignedWrap(nuw);
if (EnableFMFInDAG) {
Flags.setAllowReciprocal(FMF.allowReciprocal());
Flags.setNoInfs(FMF.noInfs());
Flags.setNoNaNs(FMF.noNaNs());
Flags.setNoSignedZeros(FMF.noSignedZeros());
Flags.setUnsafeAlgebra(FMF.unsafeAlgebra());
}
SDValue BinNodeValue = DAG.getNode(OpCode, getCurSDLoc(), Op1.getValueType(), Op1, Op2, &Flags);
setValue(&I, BinNodeValue);
}
此函数从 IR 的二进制运算符中获取两个操作数并将它们存储到SDValue类型中。然后它使用二进制运算符的操作码调用DAG.getNode()函数。这导致形成一个 DAG 节点,其外观大致如下:

操作数0和1是加载 DAG 节点。
考虑以下中间表示(IR):
%div = sdiv i32 %add, %c
遇到sdiv指令时,将调用visitSDiv()函数。
void SelectionDAGBuilder::visitSDiv(const User &I) {
SDValue Op1 = getValue(I.getOperand(0));
SDValue Op2 = getValue(I.getOperand(1));
SDNodeFlags Flags;
Flags.setExact(isa<PossiblyExactOperator>(&I) &&
cast<PossiblyExactOperator>(&I)->isExact());
setValue(&I, DAG.getNode(ISD::SDIV, getCurSDLoc(), Op1.getValueType(), Op1, Op2, &Flags));
}
与visitBinary()类似,此函数也将两个操作数存储到SDValue中,并获取一个具有ISD::SDIV作为其运算符的 DAG 节点。节点的外观如下:

在我们的 IR 中,操作数 0 是%add。操作数1是%c,它作为参数传递给函数,在将 IR 转换为SelectionDAG时转换为加载节点。关于加载 DAG 节点的实现,请查阅lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp文件中的visitLoad()函数。
在访问所有前面提到的 IR 指令后,最终将 IR 转换为以下SelectionDAG:

在前面的图中,请注意以下几点:
-
黑色箭头表示数据流依赖关系
-
红色箭头表示粘合依赖
-
蓝色虚线箭头表示链式依赖
Glue 防止两个节点在调度过程中被拆分。链式依赖防止具有副作用节点。数据依赖表示指令依赖于先前指令的结果。
合法化 SelectionDAG
在前一个主题中,我们看到了如何将 IR 转换为 SelectionDAG。整个过程没有涉及任何关于我们试图为其生成代码的目标架构的知识。DAG 节点可能对给定的目标架构是非法的。例如,X86 架构不支持 sdiv 指令。相反,它支持 sdivrem 指令。这种特定于目标的信息通过 TargetLowering 接口传达给 SelectionDAG 阶段。目标实现此接口来描述如何将 LLVM IR 指令降低为合法的 SelectionDAG 操作。
在我们的 IR 情况中,我们需要将 sdiv 指令扩展为 'sdivrem' 指令。在函数 void SelectionDAGLegalize::LegalizeOp(SDNode *Node) 中,遇到了 TargetLowering::Expand 情况,这会在该特定节点上调用 ExpandNode() 函数调用。
void SelectionDAGLegalize::LegalizeOp(SDNode *Node){
…
…
case TargetLowering::Expand:
ExpandNode(Node);
return;
…
…
}
此函数将 SDIV 扩展到 SDIVREM 节点:
case ISD::SDIV: {
bool isSigned = Node->getOpcode() == ISD::SDIV;
unsigned DivRemOpc = isSigned ? ISD::SDIVREM : ISD::UDIVREM;
EVT VT = Node->getValueType(0);
SDVTList VTs = DAG.getVTList(VT, VT);
if (TLI.isOperationLegalOrCustom(DivRemOpc, VT) ||
(isDivRemLibcallAvailable(Node, isSigned, TLI) &&
useDivRem(Node, isSigned, true)))
Tmp1 = DAG.getNode(DivRemOpc, dl, VTs, Node->getOperand(0),
Node->getOperand(1));
else if (isSigned)
Tmp1 = ExpandIntLibCall(Node, true,
RTLIB::SDIV_I8,
RTLIB::SDIV_I16, RTLIB::SDIV_I32,
RTLIB::SDIV_I64, RTLIB::SDIV_I128);
else
Tmp1 = ExpandIntLibCall(Node, false,
RTLIB::UDIV_I8,
RTLIB::UDIV_I16, RTLIB::UDIV_I32,
RTLIB::UDIV_I64, RTLIB::UDIV_I128);
Results.push_back(Tmp1);
break;
}
最后,在合法化之后,节点变为 ISD::SDIVREM:

因此,上述指令已被 'legalized' 映射到目标架构上支持的指令。我们上面看到的是一个扩展合法化的示例。还有两种其他类型的合法化——提升和自定义。提升将一种类型提升到更大的类型。自定义合法化涉及特定于目标的钩子(可能是一个自定义操作——通常与 IR 内置函数一起看到)。我们将这些留给读者在 CodeGen 阶段进一步探索。
优化 SelectionDAG
在将 IR 转换为 SelectionDAG 之后,可能会出现许多优化 DAG 本身的机会。这些优化发生在 DAGCombiner 阶段。这些机会可能由于一组特定于架构的指令而出现。
让我们举一个例子:
#include <arm_neon.h>
unsigned hadd(uint32x4_t a) {
return a[0] + a[1] + a[2] + a[3];
}
IR 中的前一个示例看起来如下:
define i32 @hadd(<4 x i32> %a) nounwind {
%vecext = extractelement <4 x i32> %a, i32 3
%vecext1 = extractelement <4 x i32> %a, i32 2
%add = add i32 %vecext, %vecext1
%vecext2 = extractelement <4 x i32> %a, i32 1
%add3 = add i32 %add, %vecext2
%vecext4 = extractelement <4 x i32> %a, i32 0
%add5 = add i32 %add3, %vecext4
ret i32 %add5
}
示例基本上是从 <4xi32> 向量中提取单个元素,并将向量的每个元素相加得到一个标量结果。
高级架构,如 ARM,有一个单独的指令来完成前面的操作——对单个向量进行加法。SDAG 需要通过在 SelectionDAG 中识别前面的模式来合并成一个 DAG 节点。
这可以在 AArch64DAGToDAGISel 中选择指令时完成。
SDNode *AArch64DAGToDAGISel::Select(SDNode *Node) {
…
…
case ISD::ADD: {
if (SDNode *I = SelectMLAV64LaneV128(Node))
return I;
if (SDNode *I = SelectADDV(Node))
return I;
break;
}
}
我们定义 SelectADDV() 函数如下:
SDNode *AArch64DAGToDAGISel::SelectADDV(SDNode *N) {
if (N->getValueType(0) != MVT::i32)
return nullptr;
SDValue SecondAdd;
SDValue FirstExtr;
if (!checkVectorElemAdd(N, SecondAdd, FirstExtr))
return nullptr;
SDValue Vector = FirstExtr.getOperand(0);
if (Vector.getValueType() != MVT::v4i32)
return nullptr;
uint64_t LaneMask = 0;
ConstantSDNode *LaneNode = cast<ConstantSDNode>(FirstExtr->getOperand(1));
LaneMask |= 1 << LaneNode->getZExtValue();
SDValue ThirdAdd;
SDValue SecondExtr;
if (!checkVectorElemAdd(SecondAdd.getNode(), ThirdAdd, SecondExtr))
return nullptr;
if (Vector != SecondExtr.getOperand(0))
return nullptr;
ConstantSDNode *LaneNode2 = cast<ConstantSDNode>(SecondExtr->getOperand(1));
LaneMask |= 1 << LaneNode2->getZExtValue();
SDValue LHS = ThirdAdd.getOperand(0);
SDValue RHS = ThirdAdd.getOperand(1);
if (LHS.getOpcode() != ISD::EXTRACT_VECTOR_ELT ||
RHS.getOpcode() != ISD::EXTRACT_VECTOR_ELT ||
LHS.getOperand(0) != Vector ||
RHS.getOperand(0) != Vector)
return nullptr;
ConstantSDNode *LaneNode3 = cast<ConstantSDNode>(LHS->getOperand(1));
LaneMask |= 1 << LaneNode3->getZExtValue();
ConstantSDNode *LaneNode4 = cast<ConstantSDNode>(RHS->getOperand(1));
LaneMask |= 1 << LaneNode4->getZExtValue();
if (LaneMask != 0x0F)
return nullptr;
return CurDAG->getMachineNode(AArch64::ADDVv4i32v, SDLoc(N), MVT::i32,
Vector);
}
注意,我们之前已经定义了一个辅助函数 checkVectorElemAdd() 来检查加法选择 DAG 节点的链。
static bool checkVectorElemAdd(SDNode *N, SDValue &Add, SDValue &Extr) {
SDValue Op0 = N->getOperand(0);
SDValue Op1 = N->getOperand(1);
const unsigned Opc0 = Op0->getOpcode();
const unsigned Opc1 = Op1->getOpcode();
const bool AddLeft = (Opc0 == ISD::ADD && Opc1 == ISD::EXTRACT_VECTOR_ELT);
const bool AddRight = (Opc0 == ISD::EXTRACT_VECTOR_ELT && Opc1 == ISD::ADD);
if (!(AddLeft || AddRight))
return false;
Add = AddLeft ? Op0 : Op1;
Extr = AddLeft ? Op1 : Op0;
return true;
}
让我们看看这如何影响代码生成:
$ llc -mtriple=aarch64-linux-gnu -verify-machineinstrs hadd.ll
在前面的代码之前,生成的最终代码将如下所示:
mov w8, v0.s[3]
mov w9, v0.s[2]
add w8, w8, w9
mov w9, v0.s[1]
add w8, w8, w9
fmov w9, s0
add w0, w8, w9
ret
显然,前面的代码是标量代码。在添加前面的补丁并编译后,生成的代码将如下所示:
addv s0, v0.4s
fmov w0, s0
ret
指令选择
在这个阶段,SelectionDAG 已优化并合法化。然而,指令仍然不是机器代码形式。这些指令需要在 SelectionDAG 本身中映射到特定于架构的指令。TableGen 类帮助选择特定于目标的指令。
CodeGenAndEmitDAG() 函数调用 DoInstructionSelection() 函数,该函数遍历每个 DAG 节点并为每个节点调用 Select() 函数。Select() 函数是 targets 实现以选择节点的主要钩子。Select() 函数是一个由 targets 实现的虚拟方法。
为了考虑,假设我们的目标架构是 X86。X86DAGToDAGISel::Select() 函数拦截一些节点进行手动匹配,但将大部分工作委托给 X86DAGToDAGISel::SelectCode() 函数。X86DAGToDAGISel::SelectCode() 函数由 TableGen 自动生成。它包含匹配器表,然后调用通用的 SelectionDAGISel::SelectCodeCommon() 函数,并传递该表。
SDNode *ResNode = SelectCode(Node);
例如,考虑以下:
$ cat test.ll
define i32 @test(i32 %a, i32 %b, i32 %c) {
%add = add nsw i32 %a, %b
%div = sdiv i32 %add, %c
ret i32 %div
}
在指令选择之前,SDAG 看起来如下:
$ llc –view-isel-dags test.ll

在指令选择之后,SDAG 看起来如下:
$ llc –view-sched-dags test.ll

调度和发出机器指令
到目前为止,我们一直在 DAG 上执行操作。现在,为了机器能够执行,我们需要将 DAG 转换为机器可以执行的指令。朝着这个方向迈出的一步是将指令列表输出到 MachineBasicBlock。这是通过 Scheduler 完成的,其目标是线性化 DAG。调度依赖于目标架构,因为某些 Targets 将具有影响调度的特定于目标的钩子。
类 InstrEmitter::EmitMachineNode 将 SDNode *Node 作为输入参数之一,它将为该参数发出 MachineInstr 类的机器指令。这些指令被输出到 MachineBasicBlock。
该函数分别调用 EmitSubregNode、EmitCopyToRegClassNode 和 EmitRegSequence 来处理 subreg 插入/提取、COPY_TO_REGCLASS 和 REG_SEQUENCE。
调用 MachineInstrBuilder MIB = BuildMI(*MF, Node->getDebugLoc(), II); 用于构建机器指令。调用 CreateVirtualRegisters 函数以添加由该指令创建的结果寄存器值。
for 循环发出指令的操作数:
for (unsigned i = NumSkip; i != NodeOperands; ++i)
AddOperand(MIB, Node->getOperand(i), i-NumSkip+NumDefs, &II,
VRBaseMap, /*IsDebug=*/false, IsClone, IsCloned);
MBB->insert(InsertPos, MIB);
它将指令插入到 MachineBasicBlock 中的位置。
以下代码标记了未使用的寄存器为死亡:
if (!UsedRegs.empty() || II.getImplicitDefs())
MIB->setPhysRegsDeadExcept(UsedRegs, *TRI);
如我们之前讨论的那样,特定于目标的钩子会影响调度,该函数中的代码如下:
if (II.hasPostISelHook())
TLI->AdjustInstrPostInstrSelection(MIB, Node);
AdjustInstrPostInstrSelection 是由 Targets 实现的一个虚拟函数。
让我们通过一个例子来看看这一步生成的机器指令。为此,我们需要将命令行选项-print-machineinstrs传递给llc工具。让我们使用之前相同的testcode:
$ cat test.ll
define i32 @test(i32 %a, i32 %b, i32 %c) {
%add = add nsw i32 %a, %b
%div = sdiv i32 %add, %c
ret i32 %div
}
现在,调用llc命令并将–print-machineinstrs传递给它。将test.ll作为输入文件,并将输出存储在输出文件中:
llc -print-machineinstrs test.ll > outfile 2>&1
outfile很大,包含除调度之外许多其他代码生成阶段的输出。我们需要查看输出文件中# After Instruction Selection:后面的部分,如下所示:
# After Instruction Selection:
# Machine code for function test: SSA
Function Live Ins: %EDI in %vreg0, %ESI in %vreg1, %EDX in %vreg2
BB#0: derived from LLVM BB %0
Live Ins: %EDI %ESI %EDX
%vreg2<def> = COPY %EDX; GR32:%vreg2
%vreg1<def> = COPY %ESI; GR32:%vreg1
%vreg0<def> = COPY %EDI; GR32:%vreg0
%vreg3<def,tied1> = ADD32rr %vreg0<tied0>, %vreg1, %EFLAGS<imp-def,dead>; GR32:%vreg3,%vreg0,%vreg1
%EAX<def> = COPY %vreg3; GR32:%vreg3
CDQ %EAX<imp-def>, %EDX<imp-def>, %EAX<imp-use>
IDIV32r %vreg2, %EAX<imp-def>, %EDX<imp-def,dead>, %EFLAGS<imp-def,dead>, %EAX<imp-use>, %EDX<imp-use>; GR32:%vreg2
%vreg4<def> = COPY %EAX; GR32:%vreg4
%EAX<def> = COPY %vreg4; GR32:%vreg4
RETQ %EAX
# End machine code for function test.
我们可以在输出中看到某些位置被物理寄存器占用,而一些则由虚拟寄存器占用。我们还可以在输出中看到机器指令IDIV32r。在下一节中,我们将看到物理寄存器是如何分配给代码中存在的这些虚拟寄存器的。
寄存器分配
代码生成器的下一步是寄存器分配。正如我们在前面的例子中所看到的,一些正在使用的寄存器是虚拟寄存器。寄存器分配是将物理寄存器分配给这些虚拟寄存器的任务。在 LLVM 中,虚拟寄存器的数量可以是无限的,但物理寄存器的数量取决于目标,是有限的。因此,通过寄存器分配,我们旨在最大化分配给虚拟寄存器的物理寄存器数量。我们必须注意,物理寄存器的数量是有限的,因此并不是所有的虚拟寄存器都可以分配到物理寄存器。如果在某个时刻没有可用的物理寄存器,而我们需要一个物理寄存器来存储变量,我们可能会将存在于物理寄存器中的变量移动到主存储器中,从而将释放的寄存器分配给我们要存储的变量。将变量从物理寄存器移动到内存的过程称为溢出。有各种算法来计算应该从寄存器溢出到内存中的变量。
寄存器分配器扮演的另一个重要角色是 SSA 形式的分解。到目前为止,机器指令中存在的 phi 指令需要被替换为常规指令。传统的方法是将它替换为复制指令。
必须注意的是,一些机器片段已经分配了寄存器。这是由于目标要求,它希望某些寄存器固定到某些操作上。除了这些固定的寄存器之外,寄存器分配器负责处理其余的非固定寄存器。
将虚拟寄存器映射到物理寄存器的寄存器分配可以通过以下两种方式完成:
-
直接映射:它使用
TargetRegisterInfo类和MachineOperand类。在这种情况下,开发者需要提供插入加载和存储指令的位置,以从内存中获取值并将值存储在内存中。 -
间接映射:在这里,
VirtRegMap类负责插入加载和存储操作。它也从内存中获取值并将值存储到内存中。我们需要使用VirtRegMap::assignVirt2Phys(vreg, preg)函数来将虚拟寄存器映射到物理寄存器。
LLVM 有四种寄存器分配技术。我们将简要地看看它们是什么,而不深入到算法的细节。这四个分配器如下:
-
基本寄存器分配器:这是所有技术中最基本的寄存器分配技术。它可以作为实现其他寄存器分配技术的起点。该算法使用溢出权重来优先处理虚拟寄存器。权重最小的虚拟寄存器将分配到寄存器。当没有物理寄存器可用时,虚拟寄存器将被溢出到内存中。
-
快速寄存器分配器:这种分配是在基本块级别上进行的,并试图通过延长寄存器中值的保留时间来重用寄存器中的值。
-
PBQP 寄存器分配器:如该寄存器分配器的源代码文件(
llvm/lib/CodeGen/RegAllocPBQP.cpp)中所述,这个分配器通过将寄存器分配器表示为 PBQP 问题,然后使用 PBQP 求解器来解决它。 -
贪婪寄存器分配器:这是 LLVM 中效率较高的分配器之一,它跨函数工作。其分配是通过分割活动范围和最小化溢出成本来完成的。
让我们用一个例子来看看之前测试代码 test.ll 的寄存器分配,看看 vregs 是如何被实际寄存器替换的。让我们以贪婪分配器为例。你也可以选择其他任何分配器。使用的目标机器是 x86-64 机器。
$ llc test.ll –regalloc=greedy –o test1.s
$ cat test1.s
.text
.file "test.ll"
.globl test
.align 16, 0x90
.type test,@function
test: # @test
.cfi_startproc
# BB#0:
movl %edx, %ecx
leal (%rdi,%rsi), %eax
cltd
idivl %ecx
retq
.Lfunc_end0:
.size test, .Lfunc_end0-test
.cfi_endproc
.section ".note.GNU-stack","",@progbits
我们可以看到现在所有的 vregs 都已经消失了,并被实际寄存器所替换。这里使用的机器是 x86-64。你可以尝试使用 pbqp 分配器进行寄存器分配,看看分配的差异。leal (%rdi,%rsi), %eax 指令将被以下指令替换:
movl %esi, %edx
movl %edi, %eax
leal (%rax, %rdx), %eax.
代码生成
我们在第一部分从 LLVM IR 开始,将其转换为 SelectioDAG,然后转换为 MachineInstr。现在,我们需要生成这段代码。目前,我们有 LLVM JIT 和 MC 来完成这个任务。LLVM JIT 是在内存中直接生成目标对象代码的传统方式。我们更感兴趣的是 LLVM MC 层。
MC 层负责从之前步骤传递给它的 MachineInstr 生成汇编文件/对象文件。在 MC 层中,指令以 MCInst 的形式表示,它们是轻量级的,也就是说,它们不携带关于程序的信息,如 MachineInstr。
代码发射从AsmPrinter类开始,该类被目标特定的AsmPrinter类重载。这个类通过使用目标特定的MCInstLowering接口(对于 x86 是lib/Target/x86/X86MCInstLower.cpp文件中的X86MCInstLower类)将MachineFunction函数转换为 MC 标签结构,处理一般的降低过程。
现在,我们有了MCInst指令,这些指令被传递给MCStreamer类,以进行生成汇编文件或目标代码的下一步。根据MCStreamer的选择,它使用其子类MCAsmStreamer生成汇编代码,并使用MCObjectStreamer生成目标代码。
目标特定的MCInstPrinter由MCAsmStreamer调用以打印汇编指令。要生成二进制代码,MCObjectStreamer通过MCObjectStreamer调用 LLVM 对象代码汇编器。汇编器反过来调用MCCodeEmitter::EncodeInstruction()以生成二进制指令。
我们必须注意,MC 层是 LLVM 和 GCC 之间的一大区别。GCC 总是输出汇编,然后需要一个外部汇编器将这个汇编转换成目标文件,而对于 LLVM,使用它自己的汇编器,我们可以轻松地以二进制形式打印指令,并通过在它们周围添加一些包装来直接生成目标文件。这不仅保证了以文本或二进制形式输出的内容将相同,而且通过移除对外部过程的调用,比 GCC 节省了时间。
现在,让我们通过使用llc工具查看与汇编对应的 MC 指令的例子。我们使用本章前面使用的相同测试代码test.ll文件。
要查看 MC 指令,我们需要将命令行选项–asm-show-inst传递给llc。它将以汇编文件注释的形式显示 MC 指令。
llc test.ll -asm-show-inst -o -
.text
.file "test.ll"
.globl test
.align 16, 0x90
.type test,@function
test: # @test
.cfi_startproc
# BB#0:
movl %edx, %ecx # <MCInst #1674 MOV32rr
# <MCOperand Reg:22>
# <MCOperand Reg:24>>
leal (%rdi,%rsi), %eax # <MCInst #1282 LEA64_32r
# <MCOperand Reg:19>
# <MCOperand Reg:39>
# <MCOperand Imm:1>
# <MCOperand Reg:43>
# <MCOperand Imm:0>
# <MCOperand Reg:0>>
cltd # <MCInst #388 CDQ>
idivl %ecx # <MCInst #903 IDIV32r
# <MCOperand Reg:22>>
retq # <MCInst #2465 RETQ
# <MCOperand Reg:19>>
.Lfunc_end0:
.size test, .Lfunc_end0-test
.cfi_endproc
.section ".note.GNU-stack","",@progbits
我们在汇编注释中看到MCInst和MCOperands。我们还可以通过将选项–show-mc-encoding传递给llc来在汇编注释中查看二进制编码。
$ llc test.ll -show-mc-encoding -o -
.text
.file "test.ll"
.globl test
.align 16, 0x90
.type test,@function
test: # @test
.cfi_startproc
# BB#0:
movl %edx, %ecx # encoding: [0x89,0xd1]
leal (%rdi,%rsi), %eax # encoding: [0x8d,0x04,0x37]
cltd # encoding: [0x99]
idivl %ecx # encoding: [0xf7,0xf9]
retq # encoding: [0xc3]
.Lfunc_end0:
.size test, .Lfunc_end0-test
.cfi_endproc
.section ".note.GNU-stack","",@progbits
摘要
在本章中,我们了解了如何将 LLVM IR 转换为 SelectionDAG。然后 SDAG 会经历各种转换。指令被合法化,数据类型也是如此。SelectionDAG 还会经过优化阶段,其中 DAG 节点被组合成最优节点,这些节点可能是特定于目标的。在 DAG 组合之后,它进入指令选择阶段,将目标架构指令映射到 DAG 节点。之后,DAGs 以线性顺序排列,以便 CPU 执行,这些 DAGs 转换为 MachineInstr,并且 DAGs 被销毁。在下一步中,对代码中出现的所有虚拟寄存器进行物理寄存器的分配。之后,MC 层出现并处理目标代码和汇编代码的生成。在下一章中,我们将看到如何定义一个目标;通过使用表描述文件和 TableGen,LLVM 如何表示目标的各个方面。
第七章:生成目标架构代码
编译器生成的代码最终必须在目标机器上执行。LLVM IR 的抽象形式有助于为各种架构生成代码。目标机器可以是任何东西——CPU、GPU、DSP 等。目标机器有一些定义性的方面,如寄存器集、指令集、函数的调用约定和指令流水线。这些方面或属性是通过tablegen工具生成的,以便在编写机器代码生成程序时易于使用。
LLVM 后端有一个流水线结构,其中指令通过阶段从 LLVM IR 到SelectionDAG,然后到MachineDAG,然后到MachineInstr,最后到MCInst。IR 被转换为 SelectionDAG。然后 SelectionDAG 经过合法化和优化。在此阶段之后,DAG 节点被映射到目标指令(指令选择)。然后 DAG 经过指令调度,生成指令的线性序列。虚拟寄存器随后被分配到目标机器寄存器,这涉及到最优化的寄存器分配以最小化内存溢出。
本章描述了如何表示目标架构。它还描述了如何生成汇编代码。
本章讨论的主题如下:
-
定义寄存器和寄存器集
-
定义调用约定
-
定义指令集
-
实现帧降低
-
选择指令
-
打印指令
-
注册目标
示例后端
为了理解目标代码生成,我们定义了一个简单的 RISC 型架构 TOY 机器,具有最少的寄存器,例如r0-r3,一个栈指针SP,一个链接寄存器LR(用于存储返回地址);以及一个CPSR——当前状态程序寄存器。这个玩具后端的调用约定类似于 ARM thumb-like 架构——传递给函数的参数将存储在寄存器集r0-r1中,返回值将存储在r0中。
定义寄存器和寄存器集
寄存器集是通过 tablegen 工具定义的。Tablegen 有助于维护大量特定领域的信息记录。它提取了这些记录的共同特征。这有助于减少描述中的重复,并形成表示领域信息的一种结构化方式。请访问llvm.org/docs/TableGen/以详细了解 tablegen。TableGen文件由TableGen 二进制:llvm-tblgen解释。
我们在前一段落中描述了我们的示例后端,它有四个寄存器(r0-r3),一个栈寄存器(SP)和一个链接寄存器(LR)。这些可以在TOYRegisterInfo.td文件中指定。tablegen函数提供了Register类,可以扩展以指定寄存器。创建一个名为TOYRegisterInfo.td的新文件。
寄存器可以通过扩展 Register 类来定义。
class TOYReg<bits<16> Enc, string n> : Register<n> {
let HWEncoding = Enc;
let Namespace = "TOY";
}
寄存器 r0-r3 属于通用 Register 类。这可以通过扩展 RegisterClass 来指定。
foreach i = 0-3 in {
def R#i : R<i, "r"#i >;
}
def GRRegs : RegisterClass<"TOY", [i32], 32,
(add R0, R1, R2, R3, SP)>;
剩余的,寄存器 SP、LR 和 CPSR 可以如下定义:
def SP : TOYReg<13, "sp">;
def LR : TOYReg<14, "lr">;
def CPSR : TOYReg<16, "cpsr">;
当所有这些放在一起时,TOYRegisterInfo.td 看起来如下所示:
class TOYReg<bits<16> Enc, string n> : Register<n> {
let HWEncoding = Enc;
let Namespace = "TOY";
}
foreach i = 0-3 in {
def R#i : R<i, "r"#i >;
}
def SP : TOYReg<13, "sp">;
def LR : TOYReg<14, "lr">;
def GRRegs : RegisterClass<"TOY", [i32], 32,
(add R0, R1, R2, R3, SP)>;
我们可以将此文件放在名为 TOY 的新文件夹中,该文件夹位于名为 Target 的父文件夹中,位于 llvm 的根目录下,即 llvm_root_directory/lib/Target/TOY/ TOYRegisterInfo.td。
表生成工具 llvm-tablegen 处理这个 .td 文件以生成 .inc 文件,该文件通常为这些寄存器生成枚举。这些枚举可以在 .cpp 文件中使用,其中寄存器可以引用为 TOY::R0。
定义调用约定
调用约定指定了值如何传递到和从函数调用返回。我们的 TOY 架构指定两个参数通过两个寄存器 r0 和 r1 传递,其余的传递到栈上。定义的调用约定随后通过引用函数指针在指令选择阶段使用。
在定义调用约定时,我们必须表示两个部分——一个用于定义约定返回值,另一个用于定义参数传递的约定。父类 CallingConv 被继承以定义调用约定。
在我们的 TOY 架构中,返回值存储在 r0 寄存器中。如果有更多参数,整数值将存储在大小为 4 字节且 4 字节对齐的栈槽中。这可以在 TOYCallingConv.td 中如下声明:
def RetCC_TOY : CallingConv<[
CCIfType<[i32], CCAssignToReg<[R0]>>,
CCIfType<[i32], CCAssignToStack<4, 4>>
]>;
参数传递约定可以定义为以下内容:
def CC_TOY : CallingConv<[
CCIfType<[i8, i16], CCPromoteToType<i32>>,
CCIfType<[i32], CCAssignToReg<[R0, R1]>>,
CCIfType<[i32], CCAssignToStack<4, 4>>
]>;
前面的声明说明了以下三个内容:
-
如果参数的数据类型是
i8或i16,它将被提升为i32 -
前两个参数将存储在寄存器
r0和r1中 -
如果有更多参数,它们将存储在
Stack
我们还定义了调用者保留寄存器,因为调用者保留寄存器用于存储应在调用之间保留的长生存期值。
def CC_Save : CalleeSavedRegs<(add R2, R3)>;
在构建项目后,llvm-tablegen 工具生成一个 TOYCallingConv.inc 文件,该文件将在 TOYISelLowering.cpp 文件中的指令选择阶段被包含。
定义指令集
架构具有丰富的指令集来表示目标机器支持的各项操作。在表示指令时,通常需要在目标描述文件中定义以下三个内容:
-
操作数
-
汇编字符串
-
指令模式
规范包含一个定义或输出的列表,以及一个使用或输入的列表。可以有不同类型的操作数类,例如 Register 类,以及立即数和更复杂的 register+imm 操作数。
例如,我们可以在 TOYInstrInfo.td 中如下定义我们的玩具机器的寄存器到寄存器的加法:
def ADDrr : InstTOY<(outs GRRegs:$dst),
(ins GRRegs:$src1, GRRegs:$src2),
"add $dst, $src1,z$src2",
[(set i32:$dst, (add i32:$src1, i32:$src2))]>;
在上述声明中,'ins' 有两个属于通用寄存器类的寄存器 $src1 和 $src2,它们持有两个操作数。操作的结果将被放入 'outs',这是一个属于通用 Register 类的 $dst 寄存器。汇编字符串是 "add $dst, $src1,z$src2"。$src1、$src2 和 $dst 的值将在寄存器分配时确定。因此,将生成两个寄存器之间 add 操作的汇编,如下所示:
add r0, r0, r1
我们在上面看到,一个简单的指令可以使用 tablegen 来表示。类似于 add register to register 指令,可以定义一个 subtract register from a register 指令。我们留给读者去尝试。更详细地表示复杂指令可以从项目代码中的 ARM 或 X86 架构规范中找到。
实现帧降低
帧降低涉及发出函数的前置和后置代码。前置代码发生在函数的开始处,它设置了被调用函数的栈帧。后置代码在函数的最后执行,它恢复调用(父)函数的栈帧。
在程序执行过程中,"栈" 扮演着几个角色,如下所述:
-
在调用函数时跟踪返回地址
-
在函数调用上下文中存储局部变量
-
从调用者传递参数给被调用者。
因此,在实现帧降低时,需要定义两个主要功能 - emitPrologue() 和 emitEpilogue()。
emitPrologue() 函数可以定义为以下内容:
void TOYFrameLowering::emitPrologue(MachineFunction &MF) const {
const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo();
MachineBasicBlock &MBB = MF.front();
MachineBasicBlock::iterator MBBI = MBB.begin();
uint64_t StackSize = computeStackSize(MF);
if (!StackSize) {
return;
}
unsigned StackReg = TOY::SP;
unsigned OffsetReg = materializeOffset(MF, MBB, MBBI, (unsigned)StackSize);
if (OffsetReg) {
BuildMI(MBB, MBBI, dl, TII.get(TOY::SUBrr), StackReg)
.addReg(StackReg)
.addReg(OffsetReg)
.setMIFlag(MachineInstr::FrameSetup);
} else {
BuildMI(MBB, MBBI, dl, TII.get(TOY::SUBri), StackReg)
.addReg(StackReg)
.addImm(StackSize)
.setMIFlag(MachineInstr::FrameSetup);
}
}
上面的函数遍历 Machine Basic Block。它为函数计算栈大小,计算栈大小的偏移量,并发出使用栈寄存器设置帧的指令。
同样,emitEpilogue() 函数可以定义为以下内容:
void TOYFrameLowering::emitEpilogue(MachineFunction &MF,
MachineBasicBlock &MBB) const {
const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo();
MachineBasicBlock::iterator MBBI = MBB.getLastNonDebugInstr();
DebugLoc dl = MBBI->getDebugLoc();
uint64_t StackSize = computeStackSize(MF);
if (!StackSize) {
return;
}
unsigned StackReg = TOY::SP;
unsigned OffsetReg = materializeOffset(MF, MBB, MBBI, (unsigned)StackSize);
if (OffsetReg) {
BuildMI(MBB, MBBI, dl, TII.get(TOY::ADDrr), StackReg)
.addReg(StackReg)
.addReg(OffsetReg)
.setMIFlag(MachineInstr::FrameSetup);
} else {
BuildMI(MBB, MBBI, dl, TII.get(TOY::ADDri), StackReg)
.addReg(StackReg)
.addImm(StackSize)
.setMIFlag(MachineInstr::FrameSetup);
}
}
前面的函数还计算栈大小,遍历机器基本块,并在函数返回时设置函数帧。请注意,这里的栈是递减的。
emitPrologue() 函数首先计算栈大小以确定是否需要前置代码。然后它通过计算偏移量来调整栈指针。对于 emitEpilogue(),它首先检查是否需要后置代码。然后它将栈指针恢复到函数开始时的状态。
例如,考虑这个输入 IR:
%p = alloca i32, align 4
store i32 2, i32* %p
%b = load i32* %p, align 4
%c = add nsw i32 %a, %b
生成的 TOY 汇编将看起来像这样:
sub sp, sp, #4 ; prologue
movw r1, #2
str r1, [sp]
add r0, r0, #2
add sp, sp, #4 ; epilogue
降低指令
在本章中,我们将看到三个方面的实现 - 函数调用约定、形式参数调用约定和返回值调用约定。我们创建一个文件 TOYISelLowering.cpp,并在其中实现指令降低。
首先,让我们看看如何实现调用约定。
SDValue TOYTar-getLoweing::LowerCall(TargetLowering::CallLoweringInfo &CLI, SmallVectorImpl<SDValue> &InVals)
const {
SelectionDAG &DAG = CLI.DAG;
SDLoc &Loc = CLI.DL;
SmallVectorImpl<ISD::OutputArg> &Outs = CLI.Outs;
SmallVectorImpl<SDValue> &OutVals = CLI.OutVals;
SmallVectorImpl<ISD::InputArg> &Ins = CLI.Ins;
SDValue Chain = CLI.Chain;
SDValue Callee = CLI.Callee;
CallingConv::ID CallConv = CLI.CallConv;
const bool isVarArg = CLI.IsVarArg;
CLI.IsTailCall = false;
if (isVarArg) {
llvm_unreachable("Unimplemented");
}
// Analyze operands of the call, assigning locations to each
// operand.
SmallVector<CCValAssign, 16> ArgLocs;
CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(), ArgLocs, *DAG.getContext());
CCInfo.AnalyzeCallOperands(Outs, CC_TOY);
// Get the size of the outgoing arguments stack space
// requirement.
const unsigned NumBytes = CCInfo.getNextStackOffset();
Chain = DAG.getCALLSEQ_START(Chain,
DAG.getIntPtrConstant(NumBytes, Loc, true), Loc);
SmallVector<std::pair<unsigned, SDValue>, 8> RegsToPass;
SmallVector<SDValue, 8> MemOpChains;
// Walk the register/memloc assignments, inserting copies/loads.
for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
CCValAssign &VA = ArgLocs[i];
SDValue Arg = OutVals[i];
// We only handle fully promoted arguments.
assert(VA.getLocInfo() == CCValAssign::Full && "Unhandled loc
info");
if (VA.isRegLoc()) {
RegsToPass.push_back(std::make_pair(VA.getLocReg(), Arg));
continue;
}
assert(VA.isMemLoc() &&
"Only support passing arguments through registers or
via the stack");
SDValue StackPtr = DAG.getRegister(TOY::SP, MVT::i32);
SDValue PtrOff = DAG.getIntPtrConstant(VA.getLocMemOffset(),
Loc);
PtrOff = DAG.getNode(ISD::ADD, Loc, MVT::i32, StackPtr,
PtrOff);
MemOpChains.push_back(DAG.getStore(Chain, Loc, Arg, PtrOff,
MachinePointerInfo(), false, false, 0));
}
// Emit all stores, make sure they occur before the call.
if (!MemOpChains.empty()) {
Chain = DAG.getNode(ISD::TokenFactor, Loc, MVT::Other, MemOpChains);
}
// Build a sequence of copy-to-reg nodes chained together with
// token chain
// and flag operands which copy the outgoing args into the
// appropriate regs.
SDValue InFlag;
for (auto &Reg : RegsToPass) {
Chain = DAG.getCopyToReg(Chain, Loc, Reg.first, Reg.second, InFlag);
InFlag = Chain.getValue(1);
}
// We only support calling global addresses.
GlobalAddressSDNode *G = dyn_cast<GlobalAddressSDNode>(Callee);
assert(G && "We only support the calling of global address-es");
EVT PtrVT = getPointerTy(DAG.getDataLayout());
Callee = DAG.getGlobalAddress(G->getGlobal(), Loc, PtrVT, 0);
std::vector<SDValue> Ops;
Ops.push_back(Chain);
Ops.push_back(Callee);
// Add argument registers to the end of the list so that they
// are known live into the call.
for (auto &Reg : RegsToPass) {
Ops.push_back(DAG.getRegister(Reg.first, Reg.second.getValueType()));
}
// Add a register mask operand representing the call-preserved
// registers.
const uint32_t *Mask;
const TargetRegisterInfo *TRI = DAG.getSubtarget().getRegisterInfo();
Mask = TRI->getCallPreservedMask(DAG.getMachineFunction(), CallConv);
assert(Mask && "Missing call preserved mask for calling
convention");
Ops.push_back(DAG.getRegisterMask(Mask));
if (InFlag.getNode()) {
Ops.push_back(InFlag);
}
SDVTList NodeTys = DAG.getVTList(MVT::Other, MVT::Glue);
// Returns a chain and a flag for retval copy to use.
Chain = DAG.getNode(TOYISD::CALL, Loc, NodeTys, Ops);
InFlag = Chain.getValue(1);
Chain = DAG.getCALLSEQ_END(Chain, DAG.getIntPtrConstant(NumBytes, Loc, true),
DAG.getIntPtrConstant(0, Loc, true), InFlag, Loc);
if (!Ins.empty()) {
InFlag = Chain.getValue(1);
}
// Handle result values, copying them out of physregs into vregs
// that we return.
return LowerCallResult(Chain, InFlag, CallConv, isVarArg, Ins,
Loc, DAG, InVals);
}
在上述函数中,我们首先分析了调用的操作数,为每个操作数分配位置,并计算了参数栈空间的大小。然后,我们扫描register/memloc分配,并插入copies和loads。对于我们的示例目标,我们支持通过寄存器或通过栈传递参数(记住上一节中定义的调用约定)。然后,我们发出所有存储操作,确保它们在调用之前发生。我们构建一系列copy-to-reg节点,将输出参数复制到适当的寄存器中。然后,我们添加一个表示调用保留寄存器的寄存器掩码操作数。我们返回一个链和标志,用于返回值复制,并最终处理结果值,将它们从physregs复制到我们返回的vregs中。
我们现在将查看正式参数调用约定的实现。
SDValue TOYTargetLowering::LowerFormalArguments(
SDValue Chain, CallingConv::ID CallConv, bool isVarArg,
const SmallVectorImpl<ISD::InputArg> &Ins, SDLoc dl, SelectionDAG &DAG,
SmallVectorImpl<SDValue> &InVals) const {
MachineFunction &MF = DAG.getMachineFunction();
MachineRegisterInfo &RegInfo = MF.getRegInfo();
assert(!isVarArg && "VarArg not supported");
// Assign locations to all of the incoming arguments.
SmallVector<CCValAssign, 16> ArgLocs;
CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(), ArgLocs, *DAG.getContext());
CCInfo.AnalyzeFormalArguments(Ins, CC_TOY);
for (auto &VA : ArgLocs) {
if (VA.isRegLoc()) {
// Arguments passed in registers
EVT RegVT = VA.getLocVT();
assert(RegVT.getSimpleVT().SimpleTy == MVT::i32 &&
"Only support MVT::i32 register passing");
const unsigned VReg =
RegInfo.createVirtualRegister(&TOY::GRRegsRegClass);
RegInfo.addLiveIn(VA.getLocReg(), VReg);
SDValue ArgIn = DAG.getCopyFromReg(Chain, dl, VReg, RegVT);
InVals.push_back(ArgIn);
continue;
}
assert(VA.isMemLoc() &&
"Can only pass arguments as either registers or via the
stack");
const unsigned Offset = VA.getLocMemOffset();
const int FI = MF.getFrameInfo()->CreateFixedObject(4, Offset,
true);
EVT PtrTy = getPointerTy(DAG.getDataLayout());
SDValue FIPtr = DAG.getFrameIndex(FI, PtrTy);
assert(VA.getValVT() == MVT::i32 &&
"Only support passing arguments as i32");
SDValue Load = DAG.getLoad(VA.getValVT(), dl, Chain, FIPtr,
MachinePointerInfo(), false, false, false, 0);
InVals.push_back(Load);
}
return Chain;
}
在上述正式参数调用约定的实现中,我们为所有传入的参数分配了位置。我们只处理通过寄存器或栈传递的参数。我们现在将查看返回值调用约定的实现。
bool TOYTargetLowering::CanLowerReturn(
CallingConv::ID CallConv, MachineFunction &MF, bool isVarArg,
const SmallVectorImpl<ISD::OutputArg> &Outs, LLVMContext &Context) const {
SmallVector<CCValAssign, 16> RVLocs;
CCState CCInfo(CallConv, isVarArg, MF, RVLocs, Context);
if (!CCInfo.CheckReturn(Outs, RetCC_TOY)) {
return false;
}
if (CCInfo.getNextStackOffset() != 0 && isVarArg) {
return false;
}
return true;
}
SDValue
TOYTargetLowering::LowerReturn(SDValue Chain, CallingConv::ID CallConv, bool isVarArg, const SmallVec torImpl<ISD::OutputArg> & Outs, const SmallVectorImpl<SDValue> const SmallVec torImpl<ISD::OutputArg> & Outs,
if (isVarArg) {
report_fatal_error("VarArg not supported");
}
// CCValAssign - represent the assignment of
// the return value to a location
SmallVector<CCValAssign, 16> RVLocs;
// CCState - Info about the registers and stack slot.
CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(), RVLocs,
*DAG.getContext());
CCInfo.AnalyzeReturn(Outs, RetCC_TOY);
SDValue Flag;
SmallVector<SDValue, 4> RetOps(1, Chain);
// Copy the result values into the output registers.
for (unsigned i = 0, e = RVLocs.size(); i < e; ++i) {
CCValAssign &VA = RVLocs[i];
assert(VA.isRegLoc() && "Can only return in registers!");
Chain = DAG.getCopyToReg(Chain, dl, VA.getLocReg(), OutVals[i], Flag);
Flag = Chain.getValue(1);
RetOps.push_back(DAG.getRegister(VA.getLocReg(), VA.getLocVT()));
}
RetOps[0] = Chain; // Update chain.
// Add the flag if we have it.
if (Flag.getNode()) {
RetOps.push_back(Flag);
}
return DAG.getNode(TOYISD::RET_FLAG, dl, MVT::Other, RetOps);
}
我们首先检查是否可以降低返回值。然后收集有关寄存器和栈槽位的信息。我们将结果值复制到输出寄存器中,并最终返回一个表示返回值的 DAG 节点。
打印指令
打印汇编指令是生成目标代码的重要步骤。定义了各种类,它们作为流式传输的网关。
首先,我们在TOYInstrFormats.td文件中初始化指令类,分配操作数、汇编字符串、模式、输出变量等:
class InstTOY<dag outs, dag ins, string asmstr, list<dag> pattern>
: Instruction {
field bits<32> Inst;
let Namespace = "TOY";
dag OutOperandList = outs;
dag InOperandList = ins;
let AsmString = asmstr;
let Pattern = pattern;
let Size = 4;
}
然后,我们在TOYInstPrinter.cpp中定义了打印操作数的函数。
void TOYInstPrinter::printOperand(const MCInst *MI, unsigned OpNo,
raw_ostream &O) {
const MCOperand &Op = MI->getOperand(OpNo);
if (Op.isReg()) {
printRegName(O, Op.getReg());
return;
}
if (Op.isImm()) {
O << "#" << Op.getImm();
return;
}
assert(Op.isExpr() && "unknown operand kind in printOperand");
printExpr(Op.getExpr(), O);
}
此函数简单地打印操作数、寄存器或立即值,视情况而定。
我们还在同一文件中定义了一个打印寄存器名称的函数:
void TOYInstPrinter::printRegName(raw_ostream &OS, unsigned RegNo) const {
OS << StringRef(getRegisterName(RegNo)).lower();
}
接下来,我们定义了一个打印指令的函数:
void TOYInstPrinter::printInst(const MCInst *MI, raw_ostream &O,
StringRef Annot) {
printInstruction(MI, O);
printAnnotation(O, Annot);
}
接下来,我们如下声明和定义汇编信息:
我们创建一个TOYMCAsmInfo.h并声明一个ASMInfo类:
#ifndef TOYTARGETASMINFO_H
#define TOYTARGETASMINFO_H
#include "llvm/MC/MCAsmInfoELF.h"
namespace llvm {
class StringRef;
class Target;
class TOYMCAsmInfo : public MCAsmInfoELF {
virtual void anchor();
public:
explicit TOYMCAsmInfo(StringRef TT);
};
} // namespace llvm
#endif
构造函数可以在TOYMCAsmInfo.cpp中定义如下:
#include "TOYMCAsmInfo.h"
#include "llvm/ADT/StringRef.h"
using namespace llvm;
void TOYMCAsmInfo::anchor() {}
TOYMCAsmInfo::TOYMCAsmInfo(StringRef TT) {
SupportsDebugInformation = true;
Data16bitsDirective = "\t.short\t";
Data32bitsDirective = "\t.long\t";
Data64bitsDirective = 0;
ZeroDirective = "\t.space\t";
CommentString = "#";
AscizDirective = ".asciiz";
HiddenVisibilityAttr = MCSA_Invalid;
HiddenDeclarationVisibilityAttr = MCSA_Invalid;
ProtectedVisibilityAttr = MCSA_Invalid;
}
对于编译,我们如下定义LLVMBuild.txt:
[component_0]
type = Library
name = TOYAsmPrinter
parent = TOY
required_libraries = MC Support
add_to_library_groups = TOY
此外,我们定义了CMakeLists.txt文件如下:
add_llvm_library(LLVMTOYAsmPrinter
TOYInstPrinter.cpp
)
当最终编译发生时,llc工具(一个静态编译器)将生成TOY架构的汇编代码(在将TOY架构注册到llc工具之后)。
要将我们的TOY目标注册到静态编译器llc,请遵循以下步骤:
-
首先,将
TOY后端条目添加到llvm_root_dir/CMakeLists.txt:set(LLVM_ALL_TARGETS AArch64 ARM … … TOY ) -
然后,将
toy条目添加到llvm_root_dir/include/llvm/ADT/Triple.h:class Triple { public: enum ArchType { UnknownArch, arm, // ARM (little endian): arm, armv.*, xscale armeb, // ARM (big endian): armeb aarch64, // AArch64 (little endian): aarch64 … … toy // TOY: toy }; -
将
toy条目添加到llvm_root_dir/include/llvm/MC/MCExpr.h:class MCSymbolRefExpr : public MCExpr { public: enum VariantKind { ... VK_TOY_LO, VK_TOY_HI, }; -
将
toy条目添加到llvm_root_dir/include/llvm/Support/ELF.h:enum { EM_NONE = 0, // No machine EM_M32 = 1, // AT&T WE 32100 … … EM_TOY = 220 // whatever is the next number }; -
然后,将
toy条目添加到lib/MC/MCExpr.cpp:StringRef MCSymbolRefExpr::getVariantKindName(VariantKind Kind) { switch (Kind) { … … case VK_TOY_LO: return "TOY_LO"; case VK_TOY_HI: return "TOY_HI"; } … } -
接下来,将
toy条目添加到lib/Support/Triple.cpp:const char *Triple::getArchTypeName(ArchType Kind) { switch (Kind) { … … case toy: return "toy"; } const char *Triple::getArchTypePrefix(ArchType Kind) { switch (Kind) { … … case toy: return "toy"; } } Triple::ArchType Triple::getArchTypeForLLVMName(StringRef Name) { … … .Case("toy", toy) … } static Triple::ArchType parseArch(StringRef ArchName) { … … .Case("toy", Triple::toy) … } static unsigned getArchPointerBitWidth(llvm::Triple::ArchType Arch) { … … case llvm::Triple::toy: return 32; … … } Triple Triple::get32BitArchVariant() const { … … case Triple::toy: // Already 32-bit. break; … } Triple Triple::get64BitArchVariant() const { … … case Triple::toy: T.setArch(UnknownArch); break; … … } -
将
toy目录条目添加到lib/Target/LLVMBuild.txt:[common] subdirectories = ARM AArch64 CppBackend Hexagon MSP430 … … TOY -
在
lib/Target/TOY文件夹中创建一个名为TOY.h的新文件:#ifndef TARGET_TOY_H #define TARGET_TOY_H #include "MCTargetDesc/TOYMCTargetDesc.h" #include "llvm/Target/TargetMachine.h" namespace llvm { class TargetMachine; class TOYTargetMachine; FunctionPass *createTOYISelDag(TOYTargetMachine &TM, CodeGenOpt::Level OptLevel); } // end namespace llvm; #endif -
在
lib/Target/TOY文件夹中创建一个名为TargetInfo的新文件夹。在该文件夹内,创建一个名为TOYTargetInfo.cpp的新文件,如下所示:#include "TOY.h" #include "llvm/IR/Module.h" #include "llvm/Support/TargetRegistry.h" using namespace llvm; Target llvm::TheTOYTarget; extern "C" void LLVMInitializeTOYTargetInfo() { RegisterTarget<Triple::toy> X(TheTOYTarget, "toy", "TOY"); } -
在同一文件夹中创建
CMakeLists.txt文件:add_llvm_library(LLVMTOYInfo TOYTargetInfo.cpp) -
创建一个
LLVMBuild.txt文件:[component_0] type = Library name = TOYInfo parent = TOY required_libraries = Support add_to_library_groups = TOY -
在
lib/Target/TOY文件夹中创建一个名为TOYTargetMachine.cpp的文件:#include "TOYTargetMachine.h" #include "TOY.h" #include "TOYFrameLowering.h" #include "TOYInstrInfo.h" #include "TOYISelLowering.h " #include "TOYSelectionDAGInfo.h" #include "llvm/CodeGen/Passes.h" #include "llvm/IR/Module.h" #include "llvm/PassManager.h" #include "llvm/Support/TargetRegistry.h" using namespace llvm; TOYTargetMachine::TOYTargetMachine(const Target &T, StringRef TT, StringRef CPU, StringRef FS, const TargetOptions &Options, Reloc::Model RM, CodeModel::Model CM, CodeGenOpt::Level OL) : LLVMTargetMachine(T, TT, CPU, FS, Options, RM, CM, OL), Subtarget(TT, CPU, FS, *this) { initAsmInfo(); } namespace { class TOYPassConfig : public TargetPassConfig { public: TOYPassConfig(TOYTargetMachine *TM, PassManagerBase &PM) : TargetPassConfig(TM, PM) {} TOYTargetMachine &getTOYTargetMachine() const { return getTM<TOYTargetMachine>(); } virtual bool addPreISel(); virtual bool addInstSelector(); virtual bool addPreEmitPass(); }; } // namespace TargetPassConfig *TOYTargetMachine::createPassConfig (PassManagerBase &PM) { return new TOYPassConfig(this, PM); } bool TOYPassConfig::addPreISel() { return false; } bool TOYPassConfig::addInstSelector() { addPass(createTOYISelDag(getTOYTargetMachine(), getOptLevel())); return false; } bool TOYPassConfig::addPreEmitPass() { return false; } // Force static initialization. extern "C" void LLVMInitializeTOYTarget() { RegisterTargetMachine<TOYTargetMachine> X(TheTOYTarget); } void TOYTargetMachine::addAnalysisPasses(PassManagerBase &PM) {} -
创建一个名为
MCTargetDesc的新文件夹和一个名为TOYMCTargetDesc.h的新文件:#ifndef TOYMCTARGETDESC_H #define TOYMCTARGETDESC_H #include "llvm/Support/DataTypes.h" namespace llvm { class Target; class MCInstrInfo; class MCRegisterInfo; class MCSubtargetInfo; class MCContext; class MCCodeEmitter; class MCAsmInfo; class MCCodeGenInfo; class MCInstPrinter; class MCObjectWriter; class MCAsmBackend; class StringRef; class raw_ostream; extern Target TheTOYTarget; MCCodeEmitter *createTOYMCCodeEmitter(const MCInstrInfo &MCII, const MCRegisterInfo &MRI, const MCSubtargetInfo &STI, MCContext &Ctx); MCAsmBackend *createTOYAsmBackend(const Target &T, const MCRegisterInfo &MRI, StringRef TT, StringRef CPU); MCObjectWriter *createTOYELFObjectWriter(raw_ostream &OS, uint8_t OSABI); } // End llvm namespace #define GET_REGINFO_ENUM #include "TOYGenRegisterInfo.inc" #define GET_INSTRINFO_ENUM #include "TOYGenInstrInfo.inc" #define GET_SUBTARGETINFO_ENUM #include "TOYGenSubtargetInfo.inc" #endif -
在同一文件夹中再创建一个名为
TOYMCTargetDesc.cpp的文件:#include "TOYMCTargetDesc.h" #include "InstPrinter/TOYInstPrinter.h" #include "TOYMCAsmInfo.h" #include "llvm/MC/MCCodeGenInfo.h" #include "llvm/MC/MCInstrInfo.h" #include "llvm/MC/MCRegisterInfo.h" #include "llvm/MC/MCSubtargetInfo.h" #include "llvm/MC/MCStreamer.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormattedStream.h" #include "llvm/Support/TargetRegistry.h" #define GET_INSTRINFO_MC_DESC #include "TOYGenInstrInfo.inc" #define GET_SUBTARGETINFO_MC_DESC #include "TOYGenSubtargetInfo.inc" #define GET_REGINFO_MC_DESC #include "TOYGenRegisterInfo.inc" using namespace llvm; static MCInstrInfo *createTOYMCInstrInfo() { MCInstrInfo *X = new MCInstrInfo(); InitTOYMCInstrInfo(X); return X; } static MCRegisterInfo *createTOYMCRegisterInfo(StringRef TT) { MCRegisterInfo *X = new MCRegisterInfo(); InitTOYMCRegisterInfo(X, TOY::LR); return X; } static MCSubtargetInfo *createTOYMCSubtargetInfo(StringRef TT, StringRef CPU, StringRef FS) { MCSubtargetInfo *X = new MCSubtargetInfo(); InitTOYMCSubtargetInfo(X, TT, CPU, FS); return X; } static MCAsmInfo *createTOYMCAsmInfo(const MCRegisterInfo &MRI, StringRef TT) { MCAsmInfo *MAI = new TOYMCAsmInfo(TT); return MAI; } static MCCodeGenInfo *createTOYMCCodeGenInfo(StringRef TT, Reloc::Model RM, CodeModel::Model CM, CodeGenOpt::Level OL) { MCCodeGenInfo *X = new MCCodeGenInfo(); if (RM == Reloc::Default) { RM = Reloc::Static; } if (CM == CodeModel::Default) { CM = CodeModel::Small; } if (CM != CodeModel::Small && CM != CodeModel::Large) { report_fatal_error("Target only supports CodeModel Small or Large"); } X->InitMCCodeGenInfo(RM, CM, OL); return X; } static MCInstPrinter * createTOYMCInstPrinter(const Target &T, unsigned SyntaxVariant, const MCAsmInfo &MAI, const MCInstrInfo & MII, const MCRegisterInfo &MRI, const MCSubtargetInfo &STI) { return new TOYInstPrinter(MAI, MII, MRI); } static MCStreamer * createMCAsmStreamer(MCContext &Ctx, formatted_raw_ostream &OS, bool isVerboseAsm, bool useDwarfDirectory, MCInstPrinter *InstPrint, MCCodeEmitter *CE, MCAsmBackend *TAB, bool ShowInst) { return createAsmStreamer(Ctx, OS, isVerboseAsm, useD - warfDirectory, InstPrint, CE, TAB, ShowInst); } static MCStreamer *createMCStreamer(const Target &T, StringRef TT, MCContext &Ctx, MCAsmBackend &MAB, raw_ostream &OS, MCCodeEmitter *Emitter, const MCSubtargetInfo &STI, bool RelaxAll, bool NoExecStack) { return createELFStreamer(Ctx, MAB, OS, Emitter, false, NoExecStack); } // Force static initialization. extern "C" void LLVMInitializeTOYTargetMC() { // Register the MC asm info. RegisterMCAsmInfoFn X(TheTOYTarget, createTOYMCAsmInfo); // Register the MC codegen info. TargetRegistry::RegisterMCCodeGenInfo(TheTOYTarget, createTOYMCCodeGenInfo); // Register the MC instruction info. TargetRegistry::RegisterMCInstrInfo(TheTOYTarget, createTOYMCInstrInfo); // Register the MC register info. TargetRegistry::RegisterMCRegInfo(TheTOYTarget, createTOYMCRegisterInfo); // Register the MC subtarget info. TargetRegistry::RegisterMCSubtargetInfo(TheTOYTarget, createTOYMCSub targetInfo); // Register the MCInstPrinter TargetRegistry::RegisterMCInstPrinter(TheTOYTarget, createTOYMCInstPrinter); // Register the ASM Backend. TargetRegistry::RegisterMCAsmBackend(TheTOYTarget, createTOYAsmBackend); // Register the assembly streamer. TargetRegistry::RegisterAsmStreamer(TheTOYTarget, createMCAsmStreamer); // Register the object streamer. TargetRegistry::RegisterMCObjectStreamer(TheTOYTarget, createMCStreamer); // Register the MCCodeEmitter TargetRegistry::RegisterMCCodeEmitter(TheTOYTarget, createTOYMCCodeEmitter); } -
在同一文件夹中创建一个
LLVMBuild.txt文件:[component_0] type = Library name = TOYDesc parent = TOY required_libraries = MC Support TOYAsmPrinter TOYInfo add_to_library_groups = TOY -
创建一个
CMakeLists.txt文件:add_llvm_library(LLVMTOYDesc TOYMCTargetDesc.cpp)按如下方式构建整个 LLVM 项目:
$ cmake llvm_src_dir –DCMAKE_BUILD_TYPE=Release – DLLVM_TARGETS_TO_BUILD="TOY" $ make Here, we have specified that we are building the LLVM compiler for the toy target. After the build completes, check whether the TOY target appears with the llc command: $ llc –version … … Registered Targets : toy – TOY
以下 IR,当提供给llc工具时,将生成如下的汇编:
target datalayout = "e-m:e-p:32:32-i1:8:32-i8:8:32- i16:16:32-i64:32-f64:32-a:0:32-n32"
target triple = "toy"
define i32 @foo(i32 %a, i32 %b){
%c = add nsw i32 %a, %b
ret i32 %c
}
$ llc foo.ll
.text
.file "foo.ll"
.globl foo
.type foo,@function
foo: # @foo
# BB#0: # %entry
add r0, r0, r1
b lr
.Ltmp0:
.size foo, .Ltmp0-foo
要查看如何使用llc注册目标的详细信息,您可以访问llvm.org/docs/WritingAnLLVMBackend.html#target-registration和jonathan2251.github.io/lbd/llvmstructure.html#target-registration由陈中舒和 Anoushe Jamshidi 编写。
摘要
在本章中,我们简要讨论了如何在 LLVM 中表示目标架构机器。我们看到了使用 tablegen 组织数据(如寄存器集、指令集、调用约定等)的便捷性,对于给定的目标。然后llvm-tablegen将这些目标描述.td文件转换为枚举,这些枚举可以在程序逻辑(如帧降低、指令选择、指令打印等)中使用。更详细和复杂的架构,如 ARM 和 X86,可以提供对目标详细描述的见解。
在第一章中,我们尝试了一个基本练习,以熟悉 LLVM 基础设施提供的各种工具。在随后的章节中,即第二章,构建 LLVM IR和第三章,高级 LLVM IR中,我们使用了 LLVM 提供的 API 来生成 IR。读者可以在他们的前端使用这些 API 将他们的语言转换为 LLVM IR。在第五章,高级 IR 块变换中,我们习惯了 IR 优化的 Pass Pipeline,并经历了一些示例。在第六章,IR 到选择 DAG 阶段中,读者熟悉了将 IR 转换为选择 DAG 的过程,这是生成机器代码的一个步骤。在本章的最后,我们看到了如何使用 tablegen 表示示例架构并用于生成代码。
阅读完这本书后,我们希望读者能够熟悉 LLVM 基础设施,并准备好深入探索 LLVM,为自己的定制架构或定制语言创建编译器。编译愉快!


浙公网安备 33010602011771号