LLVM-秘籍-全-
LLVM 秘籍(全)
原文:
zh.annas-archive.org/md5/4bc347c0faa64e08d5a906f84ed40c46
译者:飞龙
前言
程序员可能在编程过程中某个时刻遇到过编译器。简单来说,编译器将人类可读的高级语言转换为机器可执行代码。但你是否曾想过编译器内部发生了什么?编译器在生成优化后的机器代码之前会进行大量的处理。编写一个好的编译器涉及到许多复杂的算法。
本书贯穿编译的所有阶段:前端处理、代码优化、代码生成等。为了使这次旅程变得简单,LLVM 是研究最简单的编译器基础设施。它是一个模块化、分层编译器基础设施,其中每个阶段都作为一个单独的配方提供。用面向对象的 C++ 编写的 LLVM 为程序员提供了一个简单的接口和大量的 API,以便他们编写自己的编译器。
作为作者,我们坚持认为,简单的解决方案通常比复杂的解决方案更有效;在这本书中,我们将探讨各种配方,这些配方将帮助您提高技能,让您考虑所有编译选项,并理解编译代码不仅仅是表面上的那么简单。
我们也相信,那些没有参与编译器开发的程序员也能从这本书中受益,因为了解编译器实现的知识将帮助他们下次编写代码时进行优化。
我们希望您会发现这本书中的配方美味可口,在尝试了所有配方之后,您将能够准备出自己的编译器大餐。饿了吗?让我们开始配方吧!
本书涵盖内容
第一章,LLVM 设计和使用,介绍了 LLVM 基础设施的模块化世界,您将学习如何下载和安装 LLVM 和 Clang。在本章中,我们将通过一些示例来熟悉 LLVM 的工作原理。我们还将看到各种前端的一些示例。
第二章,编写前端步骤,解释了编写一种语言前端所需的步骤。我们将为一种基本玩具语言编写一个裸机玩具编译器前端。我们还将看到如何将前端语言转换为 LLVM 中间表示(IR)。
第三章,扩展前端和添加 JIT 支持,探讨了玩具语言的更高级特性和向前端添加 JIT 支持的情况。我们实现了大多数现代编程语言中找到的一些语言强大功能。
第四章,准备优化,探讨了 LLVM IR 的传递基础设施。我们探索了各种优化级别,以及每个级别的优化技术。我们还看到了编写我们自己的 LLVM 传递的逐步方法。
第五章,实现优化,展示了我们如何在 LLVM IR 上实现各种常见的优化传递。我们还探索了一些在 LLVM 开源代码中尚未出现的向量化技术。
第六章,目标无关代码生成器,带我们穿越目标无关代码生成器的抽象基础设施。我们探索了 LLVM IR 如何转换为选择 DAGs,这些 DAGs 进一步处理以生成目标机器代码。
第七章,优化机器代码,探讨了如何优化选择 DAGs 以及如何将目标寄存器分配给变量。本章还描述了在 Selection DAGs 上的各种优化技术以及各种寄存器分配技术。
第八章,编写 LLVM 后端,带我们踏上描述目标架构的旅程。本章涵盖了如何描述寄存器、指令集、调用约定、编码、子目标特性等内容。
第九章,使用 LLVM 进行各种有用的项目,探讨了 LLVM IR 基础设施可以用于的各种其他项目。记住,LLVM 不仅仅是一个编译器;它是一个编译器基础设施。本章探讨了可以将各种项目应用于代码片段以从中获取有用信息的各种项目。
你需要这本书的内容
为了完成这本书中涵盖的大多数示例,你只需要一台 Linux 机器,最好是 Ubuntu。你还需要一个简单的文本或代码编辑器、互联网接入和浏览器。我们建议安装 meld 工具以比较两个文件;它在 Linux 平台上运行良好。
这本书面向的对象
这本书是为熟悉编译器概念的编译器程序员而写的,他们希望在他们的工作中以有意义的方式深入了解、探索和使用 LLVM 基础设施。
这本书也适合那些虽然没有直接参与编译器项目,但经常参与编写数千行代码的开发阶段的程序员。通过了解编译器的工作原理,他们将以最优的方式编码,并通过干净的代码提高性能。
部分
在这本书中,你会发现一些频繁出现的标题(准备、如何做、如何工作、更多内容、相关内容)。
为了清楚地说明如何完成食谱,我们使用这些部分。
准备工作
本节将告诉你在食谱中可以期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。
如何做…
本节包含遵循食谱所需的步骤。
如何工作…
这一部分通常包含对上一节发生情况的详细解释。
更多内容...
这一部分包含有关食谱的附加信息,以便你更了解食谱。
相关内容
这一部分提供了对其他有用信息的链接。
惯例
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号如下所示:“我们可以通过使用 include
指令包含其他上下文。”
代码块设置如下:
primary := identifier_expr
:=numeric_expr
:=paran_expr
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
primary := identifier_expr
:=numeric_expr
:=paran_expr
任何命令行输入或输出都如下所示:
$ cat testfile.ll
新术语和重要词汇以粗体显示。你会在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击下一步按钮将你带到下一屏幕。”
注意
警告或重要提示以如下方框形式出现。
小贴士
小技巧和技巧如下所示。
读者反馈
我们读者的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
如要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>
,并在邮件主题中提及书籍标题。
如果你在一个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件,网址为 www.packtpub.com
。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给你。
下载本书的彩色图像
我们还为你提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助你更好地理解输出的变化。你可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/5981OS_ColorImages.pdf
。
错误清单
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support
,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>
与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>
联系我们,我们将尽力解决问题。
第一章. LLVM 设计和使用
在本章中,我们将涵盖以下主题:
-
理解模块化设计
-
交叉编译 Clang/LLVM
-
将 C 源代码转换为 LLVM 汇编
-
将 IR 转换为 LLVM 位码
-
将 LLVM 位码转换为目标机器汇编
-
将 LLVM 位码转换回 LLVM 汇编
-
转换 LLVM IR
-
链接 LLVM 位码
-
执行 LLVM 位码
-
使用 C 前端 Clang
-
使用 GO 前端
-
使用 DragonEgg
简介
在这个菜谱中,你将了解LLVM,它的设计和如何利用它提供的各种工具进行多种用途。你还将了解如何将简单的 C 代码转换为 LLVM 中间表示,以及如何将其转换为各种形式。你还将学习代码如何在 LLVM 源树中组织,以及如何使用它来编写你自己的编译器。
理解模块化设计
LLVM 被设计成一套库,与其他编译器(如GNU 编译器集合(GCC))不同。在这个菜谱中,我们将使用 LLVM 优化器来理解这个设计。由于 LLVM 优化器的设计是基于库的,它允许你以指定的顺序排列要运行的传递。此外,这种设计允许你选择可以运行的优化传递——也就是说,可能有一些优化对于你正在设计的系统类型可能没有用,只有少数优化是特定于该系统的。在查看传统的编译器优化器时,它们被构建成一个紧密相连的代码块,这使得很难将其分解成你可以轻松理解和使用的较小部分。在 LLVM 中,你不需要了解整个系统的工作原理,就可以了解一个特定的优化器。你可以只选择一个优化器并使用它,而无需担心与之相连的其他组件。
在我们继续深入了解这个菜谱之前,我们还必须了解一些关于 LLVM 汇编语言的知识。LLVM 代码以三种形式表示:内存中的编译器中间表示(IR)、磁盘上的位码表示,以及人类可读的汇编。LLVM 是一个基于静态单赋值(SSA)的表示,它提供了类型安全、低级操作、灵活性和表示所有高级语言的清洁能力。这种表示在整个 LLVM 编译策略的所有阶段都得到使用。LLVM 表示旨在成为一个通用的 IR,因为它处于足够低的级别,使得高级思想可以干净地映射到它。此外,LLVM 汇编语言是格式良好的。如果你对菜谱中提到的 LLVM 汇编有任何疑问,请参考菜谱末尾的也见部分提供的链接。
准备工作
我们必须在我们的主机机器上安装 LLVM 工具链。具体来说,我们需要opt
工具。
如何做...
我们将对相同的代码运行两种不同的优化,一个接一个,看看它如何根据我们选择的优化来修改代码。
-
首先,让我们编写一个可以输入这些优化的代码。这里我们将将其写入一个名为
testfile.ll:
的文件中。$ cat testfile.ll define i32 @test1(i32 %A) { %B = add i32 %A, 0 ret i32 %B } define internal i32 @test(i32 %X, i32 %dead) { ret i32 %X } define i32 @caller() { %A = call i32 @test(i32 123, i32 456) ret i32 %A }
-
现在,运行
opt
工具进行其中一个优化——即,用于合并指令:$ opt –S –instcombine testfile.ll –o output1.ll
-
查看输出以了解
instcombine
的工作情况:$ cat output1.ll ; ModuleID = 'testfile.ll' define i32 @test1(i32 %A) { ret i32 %A } define internal i32 @test(i32 %X, i32 %dead) { ret i32 %X } define i32 @caller() { %A = call i32 @test(i32 123, i32 456) ret i32 %A }
-
运行
opt
命令进行无效参数消除优化:$ opt –S –deadargelim testfile.ll –o output2.ll
-
查看输出,以了解
deadargelim
的工作情况:$ cat output2.ll ; ModuleID = testfile.ll' define i32 @test1(i32 %A) { %B = add i32 %A, 0 ret i32 %B } define internal i32 @test(i32 %X) { ret i32 %X } define i32 @caller() { %A = call i32 @test(i32 123) ret i32 %A }
工作原理...
在前面的例子中,我们可以看到,对于第一个命令,运行了 instcombine
过滤器,它合并了指令,因此将 %B = add i32 %A, 0; ret i32 %B
优化为 ret i32 %A
,而不影响代码。
在第二种情况下,当运行 deadargelim 过滤器
时,我们可以看到第一个函数没有修改,但上次未修改的代码部分现在被修改了,未使用的函数参数被消除。
LLVM 优化器是提供给用户所有不同过滤步骤的工具。这些过滤步骤都是用类似风格编写的。对于这些过滤步骤中的每一个,都有一个编译后的目标文件。不同过滤步骤的目标文件被存档到一个库中。库中的过滤步骤不是强连接的,LLVM PassManager 拥有关于过滤步骤之间依赖关系的信息,它在执行过滤步骤时解决这些依赖关系。以下图像显示了每个过滤步骤如何链接到特定库中的特定目标文件。在以下图中,PassA 引用了 LLVMPasses.a 中的 PassA.o,而自定义过滤步骤则引用了不同的库 MyPasses.a 中的 MyPass.o 目标文件。
小贴士
下载示例代码
您可以从您在 www.packtpub.com
的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
更多内容...
与优化器类似,LLVM 代码生成器也利用其模块化设计,将代码生成问题分解为单个过滤步骤:指令选择、寄存器分配、调度、代码布局优化和汇编输出。此外,还有许多默认运行的内置过滤步骤。用户可以选择运行哪些过滤步骤。
相关链接
-
在接下来的章节中,我们将看到如何编写我们自己的自定义过滤步骤,其中我们可以选择运行哪些优化过滤步骤以及它们的顺序。此外,为了更深入的了解,请参阅
www.aosabook.org/en/llvm.html
。 -
要了解有关 LLVM 汇编语言的更多信息,请参阅
llvm.org/docs/LangRef.html
。
交叉编译 Clang/LLVM
通过交叉编译,我们指的是在一个平台上(例如,x86)构建一个将在另一个平台上(例如,ARM)运行的二进制文件。我们构建二进制文件的机器称为宿主,而生成二进制文件将要运行的机器称为目标。为运行在其上的同一平台构建代码的编译器(宿主和目标平台相同)称为原生汇编器,而为宿主平台不同的目标平台构建代码的编译器称为交叉编译器。
在这个配方中,将展示为不同于宿主平台的平台交叉编译 LLVM,这样您就可以使用为所需目标平台构建的二进制文件。在这里,将通过一个示例来展示交叉编译,即从宿主平台 x86_64 交叉编译到目标平台 ARM。生成的二进制文件可以在具有 ARM 架构的平台中使用。
准备工作
以下软件包需要在您的系统(宿主平台)上安装:
-
cmake
-
ninja-build
(来自 Ubuntu 的 backports) -
gcc-4.x-arm-linux-gnueabihf
-
gcc-4.x-multilib-arm-linux-gnueabihf
-
binutils-arm-linux-gnueabihf
-
libgcc1-armhf-cross
-
libsfgcc1-armhf-cross
-
libstdc++6-armhf-cross
-
libstdc++6-4.x-dev-armhf-cross
-
在您的宿主平台上安装 llvm
如何操作...
要从宿主架构(即此处为 X86_64)编译 ARM 目标,需要执行以下步骤:
-
将以下
cmake
标志添加到 LLVM 的正常cmake
构建中:-DCMAKE_CROSSCOMPILING=True -DCMAKE_INSTALL_PREFIX= path-where-you-want-the-toolchain(optional) -DLLVM_TABLEGEN=<path-to-host-installed-llvm-toolchain-bin>/llvm-tblgen -DCLANG_TABLEGEN=< path-to-host-installed-llvm-toolchain-bin >/clang-tblgen -DLLVM_DEFAULT_TARGET_TRIPLE=arm-linux-gnueabihf -DLLVM_TARGET_ARCH=ARM -DLLVM_TARGETS_TO_BUILD=ARM -DCMAKE_CXX_FLAGS='-target armv7a-linux-gnueabihf -mcpu=cortex-a9 -I/usr/arm-linux-gnueabihf/include/c++/4.x.x/arm-linux-gnueabihf/ -I/usr/arm-linux-gnueabihf/include/ -mfloat-abi=hard -ccc-gcc-name arm-linux-gnueabihf-gcc'
-
如果使用你的平台编译器,请运行:
$ cmake -G Ninja <llvm-source-dir> <options above>
如果使用 Clang 作为交叉编译器,请运行:
$ CC='clang' CXX='clang++' cmake -G Ninja <source-dir> <options above>
如果你的路径上有 clang/Clang++,它应该可以正常工作。
-
要构建 LLVM,只需输入:
$ ninja
-
在 LLVM/Clang 构建成功后,使用以下命令安装它:
$ ninja install
如果您已指定 DCMAKE_INSTALL_PREFIX
选项,这将创建 install-dir
位置的 sysroot
。
它是如何工作的...
cmake
软件包通过使用传递给 cmake
的选项标志来构建所需平台的工具链,并且使用 tblgen
工具将目标描述文件转换为 C++ 代码。因此,通过使用它,可以获得有关目标的信息,例如——目标上可用的指令、寄存器数量等等。
注意
如果使用 Clang 作为交叉编译器,LLVM ARM 后端存在一个问题,在位置无关代码(PIC)上产生绝对重定位,因此作为解决方案,暂时禁用 PIC。
ARM 库在宿主系统上不可用。因此,要么下载它们的副本,要么在您的系统上构建它们。
将 C 源代码转换为 LLVM 汇编代码
在这里,我们将使用 C 前端 Clang 将 C 代码转换为 LLVM 的中间表示。
准备工作
Clang 必须安装到 PATH 中。
如何操作...
-
让我们在
multiply.c
文件中创建一个 C 代码,它看起来可能如下所示:$ cat multiply.c int mult() { int a =5; int b = 3; int c = a * b; return c; }
-
使用以下命令从 C 代码生成 LLVM IR:
$ clang -emit-llvm -S multiply.c -o multiply.ll
-
查看生成的 IR:
$ cat multiply.ll ; ModuleID = 'multiply.c' target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; Function Attrs: nounwind uwtable define i32 @mult() #0 { %a = alloca i32, align 4 %b = alloca i32, align 4 %c = alloca i32, align 4 store i32 5, i32* %a, align 4 store i32 3, i32* %b, align 4 %1 = load i32* %a, align 4 %2 = load i32* %b, align 4 %3 = mul nsw i32 %1, %2 store i32 %3, i32* %c, align 4 %4 = load i32* %c, align 4 ret i32 %4 }
我们也可以使用
cc1
生成 IR:$ clang -cc1 -emit-llvm testfile.c -o testfile.ll
它是如何工作的...
C 代码转换为 IR 的过程始于词法分析过程,其中 C 代码被分解成一个标记流,每个标记代表一个标识符、字面量、运算符等。这个标记流被送入解析器,在语言的帮助下使用上下文无关文法(CFG)构建一个抽象语法树。之后进行语义分析以检查代码是否语义正确,然后我们生成代码到 IR。
这里我们使用 Clang 前端从 C 代码生成 IR 文件。
参见
- 在下一章中,我们将看到词法分析和解析器是如何工作的,以及代码生成是如何进行的。要了解 LLVM IR 的基础知识,可以参考
llvm.org/docs/LangRef.html
。
将 IR 转换为 LLVM 位码
在这个菜谱中,你将学习如何从 IR 生成 LLVM 位码。LLVM 位码文件格式(也称为字节码)实际上是两件事:一个位流容器格式和将 LLVM IR 编码到容器格式中的编码。
准备工作
llvm-as
工具必须安装到 PATH 中。
如何做...
执行以下步骤:
-
首先创建一个将被用作
llvm-as
输入的 IR 代码:$ cat test.ll define i32 @mult(i32 %a, i32 %b) #0 { %1 = mul nsw i32 %a, %b ret i32 %1 }
-
要将
test.ll
中的 LLVM IR 转换为位码格式,你需要使用以下命令:llvm-as test.ll –o test.bc
-
输出生成在
test.bc
文件中,该文件为位流格式;因此,当我们想以文本格式查看输出时,我们得到如下截图所示:由于这是一个位码文件,查看其内容最好的方式是使用
hexdump
工具。以下截图显示了hexdump
的输出:
它是如何工作的...
llvm-as
是 LLVM 汇编器。它将 LLVM 汇编文件(即 LLVM IR)转换为 LLVM 位码。在上面的命令中,它以 test.ll
文件作为输入和输出,并以 test.bc
作为位码文件。
更多内容...
为了将 LLVM IR 编码为位码,使用了块和记录的概念。块代表位流的区域,例如——函数体、符号表等。每个块都有一个特定于其内容的 ID(例如,LLVM IR 中的函数体由 ID 12 表示)。记录由一个记录代码和一个整数值组成,它们描述了文件中的实体,如指令、全局变量描述符、类型描述等。
LLVM IR 的位码文件可能被一个简单的包装结构所封装。这个结构包含一个简单的头部,指示嵌入的 BC 文件的偏移量和大小。
参见
- 要详细了解 LLVM 位流文件格式,请参阅
llvm.org/docs/BitCodeFormat.html#abstract
将 LLVM 位码转换为目标机器汇编
在本食谱中,您将学习如何将 LLVM 位码文件转换为特定目标的汇编代码。
准备工作
The LLVM 静态编译器 llc
应该是从 LLVM 工具链中安装的。
如何操作...
执行以下步骤:
-
在前面的食谱中创建的位码文件
test.bc
,可以用作llc
的输入。使用以下命令,我们可以将 LLVM 位码转换为汇编代码:$ llc test.bc –o test.s
-
输出文件生成在
test.s
文件中,这是汇编代码。要查看它,请使用以下命令行:$ cat test.s .text .file "test.bc" .globl mult .align 16, 0x90 .type mult,@function mult: # @mult .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 imull %esi, %edi movl %edi, %eax popq %rbp retq .Ltmp3: .size mult, .Ltmp3-mult .cfi_endproc
-
您还可以使用 Clang 从位码文件格式中转储汇编代码。通过传递
–S
选项给 Clang,当test.bc
文件处于位流文件格式时,我们得到test.s
的汇编格式:$ clang -S test.bc -o test.s –fomit-frame-pointer # using the clang front end
输出的
test.s
文件与前面的示例相同。我们使用额外的选项fomit-frame-pointer
,因为 Clang 默认不消除帧指针,而llc
默认消除它。
它是如何工作的...
llc
命令将指定架构的 LLVM 输入编译成汇编语言。如果我们没有在前面命令中提及任何架构,汇编代码将为 llc
命令正在使用的宿主机器生成。要从汇编文件生成可执行文件,您可以使用汇编器和链接器。
还有更多...
通过在前面命令中指定 -march=architecture
标志,您可以指定需要生成汇编代码的目标架构。使用 -mcpu=cpu
标志设置,您可以指定架构内的 CPU 以生成代码。还可以通过指定 -regalloc=basic/greedy/fast/pbqp
来指定要使用的寄存器分配类型。
将 LLVM 位码转换回 LLVM 汇编
在本食谱中,您将把 LLVM 位码转换回 LLVM IR。实际上,这是通过使用名为 llvm-dis
的 LLVM 反汇编工具来实现的。
准备工作
要完成此操作,您需要安装 llvm-dis
工具。
如何操作...
要查看位码文件如何转换为 IR,请使用在“将 IR 转换为 LLVM Bitcode”食谱中生成的 test.bc
文件。test.bc
文件作为输入提供给 llvm-dis
工具。现在按照以下步骤进行:
-
使用以下命令显示如何将位码文件转换为我们在 IR 文件中创建的文件:
$ llvm-dis test.bc –o test.ll
-
以下是如何生成 LLVM IR:
| $ cat test.ll ; ModuleID = 'test.bc' define i32 @mult(i32 %a, i32 %b) #0 { %1 = mul nsw i32 %a, %b ret i32 %1 }
输出文件
test.ll
与我们在“将 IR 转换为 LLVM Bitcode”食谱中创建的文件相同。
它是如何工作的...
llvm-dis
命令是 LLVM 反汇编器。它接受一个 LLVM 位码文件并将其转换为 LLVM 汇编语言。
在这里,输入文件是 test.bc
,它通过 llvm-dis
转换为 test.ll
。
如果省略了文件名,llvm-dis
将从标准输入读取其输入。
转换 LLVM IR
在本配方中,我们将看到如何使用 opt 工具将 IR 从一种形式转换为另一种形式。我们将看到应用于 IR 代码的不同优化。
准备工作
您需要安装 opt 工具。
如何做到这一点...
opt
工具按照以下命令运行变换传递:
$opt –passname input.ll –o output.ll
-
让我们用一个实际的例子来。我们创建与配方将 C 源代码转换为 LLVM 汇编中使用的 C 代码等价的 LLVM IR:
$ cat multiply.c int mult() { int a =5; int b = 3; int c = a * b; return c; }
-
转换并输出后,我们得到未优化的输出:
$ clang -emit-llvm -S multiply.c -o multiply.ll $ cat multiply.ll ; ModuleID = 'multiply.c' target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; Function Attrs: nounwind uwtable define i32 @mult() #0 { %a = alloca i32, align 4 %b = alloca i32, align 4 %c = alloca i32, align 4 store i32 5, i32* %a, align 4 store i32 3, i32* %b, align 4 %1 = load i32* %a, align 4 %2 = load i32* %b, align 4 %3 = mul nsw i32 %1, %2 store i32 %3, i32* %c, align 4 %4 = load i32* %c, align 4 ret i32 %4 }
-
现在使用 opt 工具将其转换为将内存提升到寄存器的形式:
$ opt -mem2reg -S multiply.ll -o multiply1.ll $ cat multiply1.ll ; ModuleID = 'multiply.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 uwtable define i32 @mult(i32 %a, i32 %b) #0 { %1 = mul nsw i32 %a, %b ret i32 %1 }
它是如何工作的...
opt
、LLVM 优化器和分析工具将input.ll
文件作为输入,并在其上运行passname
传递。运行传递后的输出保存在包含变换后 IR 代码的output.ll
文件中。opt 工具可以传递多个传递。
更多...
当传递-analyze
选项给 opt 时,它对输入源执行各种分析,并将结果通常打印到标准输出或标准错误。此外,当输出要被馈送到另一个程序时,可以将输出重定向到文件。
当没有传递-analyze
选项给 opt 时,它运行旨在优化输入文件的变换传递。
以下是一些重要的变换,可以作为标志传递给 opt 工具:
-
adce
:激进死代码消除 -
bb-vectorize
:基本块向量化 -
constprop
:简单的常量传播 -
dce
:删除死代码 -
deadargelim
:删除死参数 -
globaldce
:删除死全局变量 -
globalopt
:全局变量优化器 -
gvn
:全局值编号 -
inline
:函数集成/内联 -
instcombine
:合并冗余指令 -
licm
:循环不变代码移动 -
loop
:unswitch:取消切换循环 -
loweratomic
:将原子内联函数降低为非原子形式 -
lowerinvoke
:降低调用到调用,用于无回滚代码生成器 -
lowerswitch
:将 SwitchInsts 降低为分支 -
mem2reg
:提升内存到寄存器 -
memcpyopt
:MemCpy 优化 -
simplifycfg
:简化 CFG -
sink
:代码下沉 -
tailcallelim
:尾调用消除
至少运行前面的一些传递,以了解它们是如何工作的。要到达可能适用于这些传递的适当源代码,请转到llvm/test/Transforms
目录。对于上述提到的每个传递,您都可以看到测试代码。应用相关的传递,并查看测试代码是如何被修改的。
注意
要查看 C 代码如何转换为 IR 的映射,在将 C 代码转换为 IR 后,如在前面的配方将 C 源代码转换为 LLVM 汇编中讨论的那样,运行mem2reg
传递。然后它将帮助您了解 C 指令是如何映射到 IR 指令的。
链接 LLVM 位代码
在本节中,您将链接之前生成的.bc
文件,以获得包含所有所需引用的单个位代码文件。
准备工作
要链接 .bc
文件,你需要 llvm-link
工具。
如何操作...
执行以下步骤:
-
要展示
llvm-link
的工作原理,首先在两个不同的文件中编写两个代码,其中一个引用另一个:$ cat test1.c int func(int a) { a = a*2; return a; } $ cat test2.c #include<stdio.h> extern int func(int a); int main() { int num = 5; num = func(num); printf("number is %d\n", num); return num; }
-
使用以下格式将此 C 代码转换为位流文件格式,首先转换为
.ll
文件,然后从.ll
文件转换为.bc
文件:$ clang -emit-llvm -S test1.c -o test1.ll $ clang -emit-llvm -S test2.c -o test2.ll $ llvm-as test1.ll -o test1.bc $ llvm-as test2.ll -o test2.bc
我们使用
test1.bc
和test2.bc
,其中test2.bc
引用了test1.bc
文件中的func
语法。 -
以以下方式调用
llvm-link
命令来链接两个 LLVM 位码文件:$ llvm-link test1.bc test2.bc –o output.bc
我们向 llvm-link
工具提供多个位码文件,它将它们链接在一起以生成单个位码文件。这里,output.bc
是生成的输出文件。我们将在下一个步骤 执行 LLVM 位码 中执行此位码文件。
它是如何工作的...
llvm-link
使用链接器的基本功能——也就是说,如果一个文件中引用的函数或变量在另一个文件中定义,那么链接器的任务是解决文件中所有引用和在另一个文件中定义的内容。但请注意,这并不是传统链接器,它将各种目标文件链接起来生成二进制文件。llvm-link
工具仅链接位码文件。
在先前的场景中,它是将 test1.bc
和 test2.bc
文件链接起来生成 output.bc
文件,其中引用已解决。
注意
在链接位码文件后,我们可以通过给 llvm-link
工具提供 –S
选项来生成输出作为 IR 文件。
执行 LLVM 位码
在本步骤中,你将执行先前步骤中生成的 LLVM 位码。
准备工作
要执行 LLVM 位码,你需要 lli
工具。
如何操作...
在先前的步骤中,我们看到了如何通过将两个 .bc
文件链接起来并定义 func
来创建单个位流文件。通过以下方式调用 lli
命令,我们可以执行生成的 output.bc
文件。它将在标准输出上显示输出:
| $ lli output.bc
number is 10
The output.bc
文件是 lli
的输入,它将执行位码文件,并在标准输出上显示任何输出。在这里,输出生成了数字 10
,这是在先前的步骤中将 test1.c
和 test2.c
链接形成的 output.bc
文件执行的结果。test2.c
文件中的主函数以整数 5 作为参数调用 test1.c
文件中的 func
函数。func
函数将输入参数加倍,并将结果返回给主函数,主函数将其输出到标准输出。
它是如何工作的...
lli
工具命令执行以 LLVM 位码格式存在的程序。它接受以 LLVM 位码格式输入,并使用即时编译器执行它,如果架构有可用的即时编译器,或者使用解释器。
如果 lli
正在使用即时编译器,那么它实际上将所有代码生成器选项视为 llc
的选项。
参见
- 在第三章的扩展前端和添加 JIT 支持的为语言添加 JIT 支持菜谱中。
使用 C 前端 Clang
在这个菜谱中,你将了解如何使用 Clang 前端实现不同的目的。
准备工作
你将需要 Clang 工具。
如何做...
Clang 可以用作高级编译器驱动程序。让我们用一个例子来展示它:
-
创建一个
hello world
C 代码,test.c
:$ cat test.c #include<stdio.h> int main() { printf("hello world\n"); return 0; }
-
使用 Clang 作为编译器驱动程序生成可执行文件
a.out
,执行时给出预期的输出:$ clang test.c $ ./a.out hello world
在这里创建了包含 C 代码的
test.c
文件。使用 Clang 编译它并生成一个可执行文件,执行时给出期望的结果。 -
可以通过提供
–E
标志来仅使用 Clang 的预处理器模式。在以下示例中,创建一个包含#define
指令定义 MAX 值的 C 代码,并使用此 MAX 作为你将要创建的数组的大小:$ cat test.c #define MAX 100 void func() { int a[MAX]; }
-
使用以下命令运行预处理器,该命令在标准输出上显示输出:
$ clang test.c -E # 1 "test.c" # 1 "<built-in>" 1 # 1 "<built-in>" 3 # 308 "<built-in>" 3 # 1 "<command line>" 1 # 1 "<built-in>" 2 # 1 "test.c" 2 void func() { int a[100]; }
在本菜谱的所有后续部分中都将使用的
test.c
文件中,MAX 被定义为100
,在预处理过程中被替换为a[MAX]
中的 MAX,变为a[100]
。 -
你可以使用以下命令从先前的示例中打印
test.c
文件的 AST,该命令在标准输出上显示输出:| $ clang -cc1 test.c -ast-dump TranslationUnitDecl 0x3f72c50 <<invalid sloc>> <invalid sloc>|-TypedefDecl 0x3f73148 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'|-TypedefDecl 0x3f731a8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'|-TypedefDecl 0x3f73518 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag [1]'`-FunctionDecl 0x3f735b8 <test.c:3:1, line:5:1> line:3:6 func 'void ()'`-CompoundStmt 0x3f73790 <col:13, line:5:1>`-DeclStmt 0x3f73778 <line:4:1, col:11>`-VarDecl 0x3f73718 <col:1, col:10> col:5 a 'int [100]'
在这里,
–cc1
选项确保只运行编译器前端,而不是驱动程序,并打印与test.c
文件代码对应的 AST。 -
你可以使用以下命令为先前示例中的
test.c
文件生成 LLVM 汇编代码:|$ clang test.c -S -emit-llvm -o - |; ModuleID = 'test.c' |target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" |target triple = "x86_64-unknown-linux-gnu" | |; Function Attrs: nounwind uwtable |define void @func() #0 { |%a = alloca [100 x i32], align 16 |ret void |}
–S
和–emit-llvm
标志确保为test.c
代码生成 LLVM 汇编。 -
要获取相同
test.c
测试代码的机器代码,将–S
标志传递给 Clang。由于–o –
选项,它将在标准输出上生成输出:|$ clang -S test.c -o - | .text | .file "test.c" | .globl func | .align 16, 0x90 | .type func,@function |func: # @func | .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 | popq %rbp | retq |.Ltmp3: | .size func, .Ltmp3-func | .cfi_endproc
当单独使用–S
标志时,编译器的代码生成过程将生成机器代码。在这里,运行命令时,由于使用了–o –
选项,机器代码将在标准输出上输出。
它是如何工作的...
在先前的示例中,Clang 作为预处理器、编译器驱动程序、前端和代码生成器工作,因此根据给定的输入标志给出期望的输出。
参见
- 这是对如何使用 Clang 的基本介绍。还有许多其他可以传递给 Clang 的标志,使其执行不同的操作。要查看列表,使用 Clang
–help
。
使用 GO 前端
llgo
编译器是仅用 Go 语言编写的基于 LLVM 的 Go 语言前端。使用此前端,我们可以从用 Go 编写的程序中生成 LLVM 汇编代码。
准备工作
你需要下载llgo
的二进制文件或从源代码构建llgo
,并将二进制文件添加到配置的PATH
文件位置。
如何做...
执行以下步骤:
-
创建一个 Go 源文件,例如,该文件将用于使用
llgo
生成 LLVM 汇编。创建test.go
:|$ cat test.go |package main |import "fmt" |func main() { | fmt.Println("Test Message") |}
-
现在,使用
llgo
获取 LLVM 汇编:$llgo -dump test.go ; ModuleID = 'main' target datalayout = "e-p:64:64:64..." target triple = "x86_64-unknown-linux" %0 = type { i8*, i8* } ....
工作原理…
llgo
编译器是 Go 语言的接口;它将 test.go
程序作为其输入并输出 LLVM IR。
参见
- 关于如何获取和安装
llgo
的信息,请参阅github.com/go-llvm/llgo
使用 DragonEgg
Dragonegg 是一个 gcc 插件,允许 gcc 使用 LLVM 优化器和代码生成器,而不是使用 gcc 自身的优化器和代码生成器。
准备工作
您需要具备 gcc 4.5 或更高版本,目标机器为 x86-32/x86-64
和 ARM 处理器。此外,您还需要下载 Dragonegg 源代码并构建 dragonegg.so
文件。
如何操作…
执行以下步骤:
-
创建一个简单的
hello world
程序:$ cat testprog.c #include<stdio.h> int main() { printf("hello world"); }
-
使用您的 gcc 编译此程序;这里我们使用 gcc-4.5:
$ gcc testprog.c -S -O1 -o - .file " testprog.c" .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "Hello world!" .text .globl main .type main, @function main: subq $8, %rsp movl $.LC0, %edi call puts movl $0, %eax addq $8, %rsp ret .size main, .-main
-
在 gcc 的命令行中使用
-fplugin=path/dragonegg.so
标志使 gcc 使用 LLVM 的优化器和 LLVM 代码生成器:$ gcc testprog.c -S -O1 -o - -fplugin=./dragonegg.so .file " testprog.c" # Start of file scope inline assembly .ident "GCC: (GNU) 4.5.0 20090928 (experimental) LLVM: 82450:82981" # End of file scope inline assembly .text .align 16 .globl main .type main,@function main: subq $8, %rsp movl $.L.str, %edi call puts xorl %eax, %eax addq $8, %rsp ret .size main, .-main .type .L.str,@object .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "Hello world!" .size .L.str, 13 .section .note.GNU-stack,"",@progbits
参见
- 关于如何获取源代码和安装过程的信息,请参阅
dragonegg.llvm.org/
第二章。编写前端步骤
在本章中,我们将涵盖以下配方:
-
定义一个 TOY 语言
-
实现一个词法分析器
-
定义抽象语法树
-
实现一个解析器
-
解析简单表达式
-
解析二进制表达式
-
调用解析驱动程序
-
在我们的 TOY 语言上运行词法分析和解析器
-
为每个 AST 类定义 IR 代码生成方法
-
为表达式生成 IR 代码
-
为函数生成 IR 代码
-
添加 IR 优化支持
简介
在本章中,你将了解如何编写一种语言的前端。通过使用自定义的 TOY 语言,你将获得如何编写词法分析和解析器以及如何从前端生成的抽象语法树(AST)生成 IR 代码的配方。
定义一个 TOY 语言
在实现词法分析和解析器之前,需要首先确定语言的语法和语法规则。在本章中,使用 TOY 语言来演示如何实现词法分析和解析器。本配方的目的是展示如何浏览一种语言。为此,要使用的 TOY 语言简单但有意义。
一种语言通常有一些变量、一些函数调用、一些常量等等。为了保持简单,我们考虑的 TOY 语言只有 32 位整型常量 A,一个不需要声明其类型(如 Python,与 C/C++/Java 不同,后者需要类型声明)的变量。
如何做到这一点...
语法可以定义为如下(生产规则定义如下,非终结符在左侧(LHS)上,终结符和非终结符的组合在右侧(RHS)上;当遇到 LHS 时,它将产生生产规则中定义的适当 RHS):
-
一个数值表达式将给出一个常数:
numeric_expr := number
-
括号表达式将在一个开括号和一个闭括号之间有一个表达式:
paran_expr := '(' expression ')'
-
标识符表达式将产生一个标识符或一个函数调用:
identifier_expr := identifier := identifier '('expr_list ')'
-
如果标识符
_expr
是一个函数调用,它将没有参数或由逗号分隔的参数列表:expr_list := (empty) := expression (',' expression)*
-
将有一些原始表达式,语法的起点,它可能产生一个标识符表达式、一个数值表达式或一个括号表达式:
primary := identifier_expr :=numeric_expr :=paran_expr
-
一个表达式可以导致一个二进制表达式:
expression := primary binoprhs
-
右侧的二进制运算可以产生二进制运算符和表达式的组合:
binoprhs := ( binoperator primary )* binoperators := '+'/'-'/'*'/'/'
-
函数声明可以有如下语法:
func_decl := identifier '(' identifier_list ')' identifier_list := (empty) := (identifier)*
-
函数定义通过一个
def
关键字后跟一个函数声明和一个定义其体的表达式来区分:function_defn := 'def' func_decl expression
-
最后,将有一个顶层表达式,它将产生一个表达式:
toplevel_expr := expression
基于先前定义的语法的 TOY 语言的一个示例可以写成如下:
def foo (x , y)
x +y * 16
由于我们已经定义了语法,下一步是为其编写词法分析和解析器。
实现一个词法分析器
Lexer 是程序编译的第一个阶段的组成部分。Lexer 将程序中的输入流进行标记化。然后 parser 消费这些标记以构建 AST。标记化的语言通常是上下文无关语言。标记是一组一个或多个字符的字符串,这些字符作为一个整体是有意义的。从字符输入流中形成标记的过程称为标记化。某些分隔符用于识别单词组作为标记。存在一些 lexer 工具来自动进行词法分析,例如 LEX。在以下过程中演示的 TOY lexer 是一个使用 C++ 编写的 hand-written lexer。
准备中
我们必须对配方中定义的 TOY 语言有一个基本理解。创建一个名为 toy.cpp
的文件,如下所示:
$ vim toy.cpp
接下来的所有代码将包含 lexer、parser 和代码生成逻辑。
如何做到这一点...
在实现 lexer 时,定义标记类型以对输入字符串流进行分类(类似于自动机的状态)。这可以通过使用 枚举 (enum) 类型来完成:
-
按如下方式打开
toy.cpp
文件:$ vim toy.cpp
-
在
toy.cpp
文件中按如下方式编写enum
:enum Token_Type { EOF_TOKEN = 0, NUMERIC_TOKEN, IDENTIFIER_TOKEN, PARAN_TOKEN, DEF_TOKEN };
以下是为前一个示例定义的术语列表:
-
EOF_TOKEN
: 它表示文件结束 -
NUMERIC_TOKEN:
当前标记是数值类型 -
IDENTIFIER_TOKEN:
当前标记是标识符 -
PARAN_TOKEN:
当前标记是括号 -
DEF_TOKEN
: 当前标记def
表示随后的内容是一个函数定义
-
-
要存储数值,可以在
toy.cpp
文件中定义一个静态变量,如下所示:static int Numeric_Val;
-
要存储
Identifier
字符串名称,可以在toy.cpp
文件中定义一个静态变量,如下所示:static std::string Identifier_string;
-
现在可以在
toy.cpp
文件中使用库函数如isspace()
、isalpha()
和fgetc()
定义 lexer 函数,如下所示:static int get_token() { static int LastChar = ' '; while(isspace(LastChar)) LastChar = fgetc(file); if(isalpha(LastChar)) { Identifier_string = LastChar; while(isalnum((LastChar = fgetc(file)))) Identifier_string += LastChar; if(Identifier_string == "def") return DEF_TOKEN; return IDENTIFIER_TOKEN; } if(isdigit(LastChar)) { std::string NumStr; do { NumStr += LastChar; LastChar = fgetc(file); } while(isdigit(LastChar)); Numeric_Val = strtod(NumStr.c_str(), 0); return NUMERIC_TOKEN; } if(LastChar == '#') { do LastChar = fgetc(file); while(LastChar != EOF && LastChar != '\n' && LastChar != '\r'); if(LastChar != EOF) return get_token(); } if(LastChar == EOF) return EOF_TOKEN; int ThisChar = LastChar; LastChar = fgetc(file); return ThisChar; }
它是如何工作的...
之前定义的示例 TOY 语言如下:
def foo (x , y)
x + y * 16
lexer 将获取前面的程序作为输入。它将遇到 def
关键字并确定随后的内容是一个定义标记,因此返回枚举值 DEF_TOKEN
。之后,它将遇到函数定义及其参数。然后,有一个涉及两个二元运算符、两个变量和一个数值常数的表达式。以下食谱演示了这些是如何存储在数据结构中的。
参见
- 在
clang.llvm.org/doxygen/Lexer_8cpp_source.html
查看为 C++ 语言编写的更复杂和详细的 hand-written lexer。
定义抽象语法树
AST 是编程语言源代码的抽象语法结构的树表示。编程结构(如表达式、流程控制语句等)的 AST 被分组为操作符和操作数。AST 表示编程结构之间的关系,而不是它们由语法生成的途径。AST 忽略了诸如标点符号和分隔符等不重要的编程元素。AST 通常包含每个元素的附加属性,这些属性在后续编译阶段很有用。源代码的位置是这样一个属性,当根据语法确定源代码的正确性时,如果遇到错误,可以使用它来抛出错误行号(位置、行号、列号等,以及其他相关属性存储在 Clang 前端 C++的SourceManager
类对象中)。
在语义分析期间,AST 被大量使用,编译器检查程序元素和语言的正确使用。编译器在语义分析期间还基于 AST 生成符号表。对树的完整遍历允许验证程序的正确性。验证正确性后,AST 作为代码生成的基。
准备工作
到现在为止,我们必须已经运行了词法分析器以获取用于生成 AST 的标记。我们打算解析的语言包括表达式、函数定义和函数声明。我们再次有各种类型的表达式——变量、二元运算符、数值表达式等。
如何实现...
要定义 AST 结构,请按照以下步骤进行:
-
按如下方式打开
toy.cpp
文件:$ vi toy.cpp
在词法分析代码下方定义 AST。
-
解析表达式的
base
类可以定义为以下内容:class BaseAST { public : virtual ~BaseAST(); };
然后,为要解析的每种表达式类型定义几个派生类。
-
变量表达式的 AST 类可以定义为以下内容:
class VariableAST : public BaseAST{ std::string Var_Name; // string object to store name of // the variable. public: VariableAST (std::string &name) : Var_Name(name) {} // ..// parameterized constructor of variable AST class to be initialized with the string passed to the constructor. };
-
语言有一些数值表达式。此类数值表达式的
AST
类可以定义为以下内容:class NumericAST : public BaseAST { int numeric_val; public : NumericAST (intval) :numeric_val(val) {} };
-
对于涉及二元运算的表达式,
AST
类可以定义为以下内容:Class BinaryAST : public BaseAST { std::string Bin_Operator; // string object to store // binary operator BaseAST *LHS, *RHS; // Objects used to store LHS and // RHS of a binary Expression. The LHS and RHS binary // operation can be of any type, hence a BaseAST object // is used to store them. public: BinaryAST (std::string op, BaseAST *lhs, BaseAST *rhs ) : Bin_Operator(op), LHS(lhs), RHS(rhs) {} // Constructor //to initialize binary operator, lhs and rhs of the binary //expression. };
-
函数声明的
AST
类可以定义为以下内容:class FunctionDeclAST { std::string Func_Name; std::vector<std::string> Arguments; public: FunctionDeclAST(const std::string &name, const std::vector<std::string> &args) : Func_Name(name), Arguments(args) {}; };
-
函数定义的
AST
类可以定义为以下内容:class FunctionDefnAST { FunctionDeclAST *Func_Decl; BaseAST* Body; public: FunctionDefnAST(FunctionDeclAST *proto, BaseAST *body) : Func_Decl(proto), Body(body) {} };
-
函数调用的
AST
类可以定义为以下内容:class FunctionCallAST : public BaseAST { std::string Function_Callee; std::vector<BaseAST*> Function_Arguments; public: FunctionCallAST(const std::string &callee, std::vector<BaseAST*> &args) : Function_Callee(callee), Function_Arguments(args) {} };
AST 的基本骨架现在已准备好使用。
它是如何工作的…
AST 充当存储由词法分析器提供的各种信息的数据结构。这些信息在解析器逻辑中生成,并根据正在解析的标记类型填充 AST。
参见
- 生成 AST 后,我们将实现解析器,然后我们将看到同时调用词法分析和解析器的示例。有关 Clang 中 C++ 的更详细 AST 结构,请参阅:
clang.llvm.org/docs/IntroductionToTheClangAST.html
。
实现解析器
解析器根据语言的语法规则对代码进行语法分析。解析阶段确定输入代码是否可以根据定义的语法形成标记串。在此阶段构建一个解析树。解析器定义函数将语言组织成称为 AST 的数据结构。本食谱中定义的解析器使用递归下降解析技术,这是一种自顶向下的解析器,并使用相互递归的函数来构建 AST。
准备工作
我们必须拥有自定义的语言,即在本例中的 TOY 语言,以及由词法分析器生成的标记流。
如何操作...
在我们的 TOY 解析器中定义一些基本值持有者,如下所示:
-
按照以下步骤打开
toy.cpp
文件:$ vi toy.cpp
-
定义一个全局静态变量以保存来自词法分析器的当前标记,如下所示:
static int Current_token;
-
按照以下方式定义一个从输入流中获取下一个标记的函数:
static void next_token() { Current_token = get_token(); }
-
下一步是定义使用上一节中定义的 AST 数据结构进行表达式解析的函数。
-
定义一个通用函数,根据词法分析器确定的标记类型调用特定的解析函数,如下所示:
static BaseAST* Base_Parser() { switch (Current_token) { default: return 0; case IDENTIFIER_TOKEN : return identifier_parser(); case NUMERIC_TOKEN : return numeric_parser(); case '(' : return paran_parser(); } }
它是如何工作的...
输入流被标记化并传递给解析器。Current_token
保存要处理的标记。在此阶段已知标记的类型,并调用相应的解析函数以初始化 AST。
参见
- 在接下来的几个食谱中,你将学习如何解析不同的表达式。有关 Clang 中实现的 C++ 语言的更详细解析,请参阅:
clang.llvm.org/doxygen/classclang_1_1Parser.html
。
解析简单表达式
在本食谱中,你将学习如何解析一个简单的表达式。一个简单的表达式可能包括数值、标识符、函数调用、函数声明和函数定义。对于每种类型的表达式,都需要定义单独的解析逻辑。
准备工作
我们必须拥有自定义的语言——在本例中即为 TOY 语言,以及由词法分析器生成的标记流。我们已经在上面定义了抽象语法树(AST)。进一步地,我们将解析表达式并调用每种类型表达式的 AST 构造函数。
如何操作...
要解析简单表达式,按照以下代码流程进行:
-
按照以下步骤打开
toy.cpp
文件:$ vi toy.cpp
我们已经在
toy.cpp
文件中实现了词法分析器逻辑。接下来的代码需要附加在toy.cpp
文件中的词法分析器代码之后。 -
按照以下方式定义用于数值表达式的
parser
函数:static BaseAST *numeric_parser() { BaseAST *Result = new NumericAST(Numeric_Val); next_token(); return Result; }
-
定义标识符表达式的
parser
函数。请注意,标识符可以是变量引用或函数调用。它们通过检查下一个标记是否为(
来区分。这如下实现:static BaseAST* identifier_parser() { std::string IdName = Identifier_string; next_token(); if(Current_token != '(') return new VariableAST(IdName); next_token(); std::vector<BaseAST*> Args; if(Current_token != ')') { while(1) { BaseAST* Arg = expression_parser(); if(!Arg) return 0; Args.push_back(Arg); if(Current_token == ')') break; if(Current_token != ',') return 0; next_token(); } } next_token(); return new FunctionCallAST(IdName, Args); }
-
定义函数声明
parser
函数如下:static FunctionDeclAST *func_decl_parser() { if(Current_token != IDENTIFIER_TOKEN) return 0; std::string FnName = Identifier_string; next_token(); if(Current_token != '(') return 0; std::vector<std::string> Function_Argument_Names; while(next_token() == IDENTIFIER_TOKEN) Function_Argument_Names.push_back(Identifier_string); if(Current_token != ')') return 0; next_token(); return new FunctionDeclAST(FnName, Function_Argument_Names); }
-
定义函数定义的
parser
函数如下:static FunctionDefnAST *func_defn_parser() { next_token(); FunctionDeclAST *Decl = func_decl_parser(); if(Decl == 0) return 0; if(BaseAST* Body = expression_parser()) return new FunctionDefnAST(Decl, Body); return 0; }
注意,在前面代码中使用的名为
expression_parser
的函数用于解析表达式。该函数可以定义为以下内容:static BaseAST* expression_parser() { BaseAST *LHS = Base_Parser(); if(!LHS) return 0; return binary_op_parser(0, LHS); }
它是如何工作的…
如果遇到数字标记,将调用数字表达式的构造函数,并返回由解析器返回的 AST 对象,用数字数据填充 AST 的数值部分。
类似地,对于标识符表达式,解析的数据将是变量或函数调用。对于函数声明和定义,将解析函数名和函数参数,并调用相应的 AST 类构造函数。
解析二进制表达式
在这个菜谱中,你将学习如何解析二进制表达式。
准备工作
我们必须有一个自定义定义的语言,即在本例中的玩具语言,以及由词法分析器生成的标记流。二进制表达式解析器需要二进制运算符的优先级来确定左右顺序。可以使用 STL map 来定义二进制运算符的优先级。
如何做到这一点…
要解析二进制表达式,请按照以下代码流程进行:
-
按如下方式打开
toy.cpp
文件:$ vi toy.cpp
-
在
toy.cpp
文件中声明一个用于运算符优先级的map
,以在全局范围内存储优先级,如下所示:static std::map<char, int>Operator_Precedence;
用于演示的 TOY 语言有 4 个运算符,运算符的优先级定义为
-
<+
</
<*
。 -
可以在全局范围内定义一个初始化优先级的函数,即存储
map
中的优先级值,如下所示:static void init_precedence() { Operator_Precedence['-'] = 1; Operator_Precedence['+'] = 2; Operator_Precedence['/'] = 3; Operator_Precedence['*'] = 4; }
-
可以定义一个辅助函数来返回二进制运算符的优先级,如下所示:
static int getBinOpPrecedence() { if(!isascii(Current_token)) return -1; int TokPrec = Operator_Precedence[Current_token]; if(TokPrec <= 0) return -1; return TokPrec; }
-
现在,可以定义如下所示的
binary
运算符解析器:static BaseAST* binary_op_parser(int Old_Prec, BaseAST *LHS) { while(1) { int Operator_Prec = getBinOpPrecedence(); if(Operator_Prec < Old_Prec) return LHS; int BinOp = Current_token; next_token(); BaseAST* RHS = Base_Parser(); if(!RHS) return 0; int Next_Prec = getBinOpPrecedence(); if(Operator_Prec < Next_Prec) { RHS = binary_op_parser(Operator_Prec+1, RHS); if(RHS == 0) return 0; } LHS = new BinaryAST(std::to_string(BinOp), LHS, RHS); } }
在这里,通过检查当前运算符的优先级与旧运算符的优先级,并根据二进制运算符的左右两边(LHS 和 RHS)来决定结果。请注意,由于右边的表达式可以是一个表达式而不是单个标识符,因此二进制运算符解析器是递归调用的。
-
可以定义一个用于括号的
parser
函数,如下所示:static BaseAST* paran_parser() { next_token(); BaseAST* V = expression_parser(); if (!V) return 0; if(Current_token != ')') return 0; return V; }
-
一些作为这些
parser
函数包装器的顶级函数可以定义为以下内容:static void HandleDefn() { if (FunctionDefnAST *F = func_defn_parser()) { if(Function* LF = F->Codegen()) { } } else { next_token(); } } static void HandleTopExpression() { if(FunctionDefnAST *F = top_level_parser()) { if(Function *LF = F->Codegen()) { } } else { next_token(); } }
参见
- 本章中所有剩余的菜谱都与用户对象相关。有关表达式的详细解析和 C++解析,请参阅:
clang.llvm.org/doxygen/classclang_1_1Parser.html
。
调用解析驱动程序
在这个菜谱中,你将学习如何在我们的 TOY 解析器的main
函数中调用解析函数。
如何做到这一点…
要调用驱动程序以开始解析,请定义如以下所示的驱动函数:
-
打开
toy.cpp
文件:$ vi toy.cpp
-
从主函数中调用的
Driver
函数,现在可以定义解析器如下:static void Driver() { while(1) { switch(Current_token) { case EOF_TOKEN : return; case ';' : next_token(); break; case DEF_TOKEN : HandleDefn(); break; default : HandleTopExpression(); break; } } }
-
可以定义如下
main()
函数来运行整个程序:int main(int argc, char* argv[]) { LLVMContext &Context = getGlobalContext(); init_precedence(); file = fopen(argv[1], "r"); if(file == 0) { printf("Could not open file\n"); } next_token(); Module_Ob = new Module("my compiler", Context); Driver(); Module_Ob->dump(); return 0; }
它是如何工作的…
主函数负责调用词法分析和解析器,以便它们可以作用于输入到编译器前端的代码片段。从主函数中,调用驱动函数以启动解析过程。
参见
- 有关 Clang 中 c++解析的主函数和驱动函数的工作原理的详细信息,请参阅
llvm.org/viewvc/llvm-project/cfe/trunk/tools/driver/cc1_main.cpp
在我们的玩具语言上运行词法分析和解析器
现在我们已经为我们的 TOY 语言语法定义了完整的词法分析和解析器,是时候在示例 TOY 语言上运行它了。
准备工作
要做到这一点,您应该了解 TOY 语言语法和本章的所有先前的配方。
如何做…
如以下所示,在 TOY 语言上运行并测试词法分析和解析器:
-
第一步是将
toy.cpp
程序编译成可执行文件:$ clang++ toy.cpp -O3 -o toy
-
toy
可执行文件是我们的 TOY 编译器前端。要解析的toy
语言在名为example
的文件中:$ cat example def foo(x , y) x + y * 16
-
此文件作为参数传递给
toy
编译器进行处理:$ ./toy example
它是如何工作的…
TOY 编译器将以读取模式打开example
文件。然后,它将对单词流进行标记化。它将遇到def
关键字并返回DEF_TOKEN
。然后,将调用HandleDefn()
函数,该函数将存储函数名和参数。然后,它将递归检查令牌类型,然后调用特定的令牌处理函数将它们存储到相应的 AST 中。
参见
- 上述词法分析和解析器除了处理一些简单的语法错误外,不处理语法错误。要实现错误处理,请参阅
llvm.org/docs/tutorial/LangImpl2.html#parser-basics
。
为每个 AST 类定义 IR 代码生成方法
现在,由于 AST 已经准备好,其数据结构中包含所有必要的信息,下一个阶段是生成 LLVM IR。在此代码生成中使用了 LLVM API。LLVM IR 有一个预定义的格式,该格式由 LLVM 内置 API 生成。
准备工作
您必须已从 TOY 语言的任何输入代码中创建 AST。
如何做…
为了生成 LLVM IR,在 AST 类中定义了一个虚拟的CodeGen
函数(这些 AST 类在 AST 部分中定义较早;这些函数是这些类的附加功能)如下:
-
按如下方式打开
toy.cpp
文件:$ vi toy.cpp
-
在之前定义的
BaseAST
类中,按如下方式添加Codegen()
函数:class BaseAST { … … virtual Value* Codegen() = 0; }; class NumericAST : public BaseAST { … … virtual Value* Codegen(); }; class VariableAST : public BaseAST { … … virtual Value* Codegen(); };
这个虚拟的
Codegen()
函数包含在我们定义的每个 AST 类中。此函数返回一个 LLVM Value 对象,它代表 LLVM 中的静态单赋值(SSA)值。定义了一些额外的静态变量,这些变量将在 Codegen 过程中使用。
-
按如下方式在全局作用域中声明以下静态变量:
static Module *Module_Ob; static IRBuilder<> Builder(getGlobalContext()); static std::map<std::string, Value*>Named_Values;
它是如何工作的…
Module_Ob
模块包含代码中的所有函数和变量。
Builder
对象帮助生成 LLVM IR,并跟踪程序中的当前位置以插入 LLVM 指令。Builder
对象有创建新指令的函数。
Named_Values
映射表跟踪当前作用域中定义的所有值,就像符号表一样。对于我们的语言,这个映射表将包含函数参数。
为表达式生成 IR 代码
在这个示例中,你将看到如何使用编译器前端生成表达式的 IR 代码。
如何做…
要为我们的 TOY 语言实现 LLVM IR 代码生成,请按照以下代码流程进行:
-
按如下方式打开
toy.cpp
文件:$ vi toy.cpp
-
生成数值代码的功能可以定义为如下:
Value *NumericAST::Codegen() { return ConstantInt::get(Type::getInt32Ty(getGlobalContext()), numeric_val); }
在 LLVM IR 中,整数常量由
ConstantInt
类表示,其数值由APInt
类持有。 -
生成变量表达式代码的函数可以定义为如下:
Value *VariableAST::Codegen() { Value *V = Named_Values[Var_Name]; return V ? V : 0; }
-
二元表达式的
Codegen()
函数可以定义为如下:Value *BinaryAST::Codegen() { Value *L = LHS->Codegen(); Value *R = RHS->Codegen(); if(L == 0 || R == 0) return 0; switch(atoi(Bin_Operator.c_str())) { case '+' : return Builder.CreateAdd(L, R, "addtmp"); case '-' : return Builder.CreateSub(L, R, "subtmp"); case '*' : return Builder.CreateMul(L, R, "multmp"); case '/' : return Builder.CreateUDiv(L, R, "divtmp"); default : return 0; } }
如果上面的代码生成了多个
addtmp
变量,LLVM 将自动为每个变量提供一个递增的唯一数字后缀。
参见
- 下一个示例将展示如何为函数生成 IR 代码;我们将学习代码生成是如何实际工作的。
为函数生成 IR 代码
在这个示例中,你将学习如何为函数生成 IR 代码。
如何做…
执行以下步骤:
-
函数调用的
Codegen()
函数可以定义为如下:Value *FunctionCallAST::Codegen() { Function *CalleeF = Module_Ob->getFunction(Function_Callee); std::vector<Value*>ArgsV; for(unsigned i = 0, e = Function_Arguments.size(); i != e; ++i) { ArgsV.push_back(Function_Arguments[i]->Codegen()); if(ArgsV.back() == 0) return 0; } return Builder.CreateCall(CalleeF, ArgsV, "calltmp"); }
一旦我们有了要调用的函数,我们将递归地调用
Codegen()
函数,为每个要传入的参数调用,并创建一个 LLVM 调用指令。 -
现在,函数调用的
Codegen()
函数已经定义,是时候定义声明和函数定义的Codegen()
函数了。函数声明的
Codegen()
函数可以定义为如下:Function *FunctionDeclAST::Codegen() { std::vector<Type*>Integers(Arguments.size(), Type::getInt32Ty(getGlobalContext())); FunctionType *FT = FunctionType::get(Type::getInt32Ty(getGlobalContext()), Integers, false); Function *F = Function::Create(FT, Function::ExternalLinkage, Func_Name, Module_Ob); if(F->getName() != Func_Name) { F->eraseFromParent(); F = Module_Ob->getFunction(Func_Name); if(!F->empty()) return 0; if(F->arg_size() != Arguments.size()) return 0; } unsigned Idx = 0; for(Function::arg_iterator Arg_It = F->arg_begin(); Idx != Arguments.size(); ++Arg_It, ++Idx) { Arg_It->setName(Arguments[Idx]); Named_Values[Arguments[Idx]] = Arg_It; } return F; }
函数定义的
Codegen()
函数可以定义为如下:Function *FunctionDefnAST::Codegen() { Named_Values.clear(); Function *TheFunction = Func_Decl->Codegen(); if(TheFunction == 0) return 0; BasicBlock *BB = BasicBlock::Create(getGlobalContext(),"entry", TheFunction); Builder.SetInsertPoint(BB); if(Value *RetVal = Body->Codegen()) { Builder.CreateRet(RetVal); verifyFunction(*TheFunction); return TheFunction; } TheFunction->eraseFromParent(); return 0; }
-
就这样!LLVM IR 现在准备好了。这些
Codegen()
函数可以在解析顶层表达式的包装器中调用,如下所示:static void HandleDefn() { if (FunctionDefnAST *F = func_defn_parser()) { if(Function* LF = F->Codegen()) { } } else { next_token(); } } static void HandleTopExpression() { if(FunctionDefnAST *F = top_level_parser()) { if(Function *LF = F->Codegen()) { } } else { next_token(); } }
因此,在解析成功后,将调用相应的
Codegen()
函数来生成 LLVM IR。调用dump()
函数以打印生成的 IR。
它是如何工作的…
Codegen()
函数使用 LLVM 内置函数调用来生成 IR。为此需要包含的头文件有llvm/IR/Verifier.h
、llvm/IR/DerivedTypes.h
、llvm/IR/IRBuilder.h
和llvm/IR/LLVMContext.h
、llvm/IR/Module.h
。
-
在编译时,此代码需要与 LLVM 库链接。为此,可以使用
llvm-config
工具,如下所示:llvm-config --cxxflags --ldflags --system-libs --libs core.
-
为了这个目的,使用以下附加标志重新编译
toy
程序:$ clang++ -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
-
当
toy
编译器现在运行example
代码时,它将生成如下所示的 LLVM IR:$ ./toy example define i32 @foo (i32 %x, i32 %y) { entry: %multmp = muli32 %y, 16 %addtmp = add i32 %x, %multmp reti32 %addtmp }
另一个
example2
文件有一个函数call.$ cat example2
:foo(5, 6);
它的 LLVM IR 将如下所示:
$ ./toy example2 define i32 @1 () { entry: %calltmp = call i32@foo(i32 5, i32 6) reti32 %calltmp }
参见
- 关于 Clang 中 C++ 的
Codegen()
函数的详细信息,请参阅llvm.org/viewvc/llvm-project/cfe/trunk/lib/CodeGen/
添加 IR 优化支持
LLVM 提供了各种优化过程。LLVM 允许编译器实现决定使用哪些优化、它们的顺序等等。在本教程中,你将学习如何添加 IR 优化支持。
如何做到这一点...
执行以下步骤:
-
要开始添加 IR 优化支持,首先需要定义一个用于函数管理器的静态变量,如下所示:
static FunctionPassManager *Global_FP;
-
然后,需要为之前使用的
Module
对象定义一个函数过程管理器。这可以在main()
函数中如下完成:FunctionPassManager My_FP(TheModule);
-
现在可以在
main()
函数中添加各种优化过程的管道,如下所示:My_FP.add(createBasicAliasAnalysisPass()); My_FP.add(createInstructionCombiningPass()); My_FP.add(createReassociatePass()); My_FP.add(createGVNPass()); My_FP.doInitialization();
-
现在将静态全局函数 Pass Manager 分配给此管道,如下所示:
Global_FP = &My_FP; Driver();
这个 PassManager 有一个运行方法,我们可以在函数定义的
Codegen()
返回之前对生成的函数 IR 运行它。如下所示进行演示:Function* FunctionDefnAST::Codegen() { Named_Values.clear(); Function *TheFunction = Func_Decl->Codegen(); if (!TheFunction) return 0; BasicBlock *BB = BasicBlock::Create(getGlobalContext(), "entry", TheFunction); Builder.SetInsertPoint(BB); if (Value* Return_Value = Body->Codegen()) { Builder.CreateRet(Return_Value); verifyFunction(*TheFunction); Global_FP->run(*TheFunction); returnTheFunction; } TheFunction->eraseFromParent(); return 0; }
这更有益,因为它在原地优化函数,提高了为函数体生成的代码。
参见
- 如何添加我们自己的优化过程及其运行方法将在后续章节中演示
第三章. 扩展前端和添加 JIT 支持
在本章中,我们将涵盖以下食谱:
-
处理决策范式 -
if/then/else
结构 -
生成循环代码
-
处理用户定义的运算符 - 二元运算符
-
处理用户定义的运算符 - 一元运算符
-
添加 JIT 支持
简介
在上一章中,定义了语言前端组件的基本。这包括为不同类型的表达式定义标记,编写一个词法分析器来标记输入流,为各种表达式的抽象语法树勾勒出框架,编写解析器,并为语言生成代码。还解释了如何将各种优化连接到前端。
当一种语言具有控制流和循环来决定程序流程时,它就更加强大和表达。JIT 支持探索了即时编译代码的可能性。在本章中,将讨论这些更复杂的编程范式的实现。本章处理了使编程语言更有意义和强大的增强。本章中的食谱展示了如何为给定的语言包含这些增强。
处理决策范式 - if/then/else
结构
在任何编程语言中,根据某些条件执行语句给语言带来了非常强大的优势。if
/then
/else
结构提供了根据某些条件改变程序控制流的能力。条件存在于if
结构中。如果条件为真,则执行then
结构之后的表达式。如果它是false
,则执行else
结构之后的表达式。这个食谱演示了解析和生成if
/then
/else
结构代码的基本基础设施。
准备工作
对于if
/then
/else
的 TOY 语言可以定义为:
if x < 2 then
x + y
else
x - y
为了检查条件,需要一个比较运算符。一个简单的<
(小于)运算符将满足这个目的。为了处理<
,需要在init_precedence()
函数中定义优先级,如下所示:
static void init_precedence() {
Operator_Precedence['<'] = 0;
…
…
}
此外,还需要包含二进制表达式的codegen()
函数以处理<
:
Value* BinaryAST::Codegen() {
…
…
…
case '<' :
L = Builder.CreateICmpULT(L, R, "cmptmp");
return Builder.CreateZExt(L, Type::getInt32Ty(getGlobalContext()),
"booltmp");…
…
}
现在,LLVM IR 将生成一个比较指令和一个布尔指令作为比较的结果,这将用于确定程序控制流的方向。现在是时候处理if
/then
/else
范式了。
如何操作...
执行以下步骤:
-
toy.cpp
文件中的词法分析器需要扩展以处理if
/then
/else
结构。这可以通过在enum
标记中添加一个标记来完成:enum Token_Type{ … … IF_TOKEN, THEN_TOKEN, ELSE_TOKEN }
-
下一步是在
get_token()
函数中添加这些标记的条目,其中我们匹配字符串并返回适当的标记:static int get_token() { … … … if (Identifier_string == "def") return DEF_TOKEN; if(Identifier_string == "if") return IF_TOKEN; if(Identifier_string == "then") return THEN_TOKEN; if(Identifier_string == "else") return ELSE_TOKEN; … … }
-
然后在
toy.cpp
文件中定义一个 AST 节点:class ExprIfAST : public BaseAST { BaseAST *Cond, *Then, *Else; public: ExprIfAST(BaseAST *cond, BaseAST *then, BaseAST * else_st) : Cond(cond), Then(then), Else(else_st) {} Value *Codegen() override; };
-
下一步是定义
if
/then
/else
结构的解析逻辑:static BaseAST *If_parser() { next_token(); BaseAST *Cond = expression_parser(); if (!Cond) return 0; if (Current_token != THEN_TOKEN) return 0; next_token(); BaseAST *Then = expression_parser(); if (Then == 0) return 0; if (Current_token != ELSE_TOKEN) return 0; next_token(); BaseAST *Else = expression_parser(); if (!Else) return 0; return new ExprIfAST(Cond, Then, Else); }
解析器的逻辑很简单:首先,搜索
if
标记,并解析其后的表达式作为条件。之后,识别then
标记并解析真条件表达式。然后搜索else
标记并解析假条件表达式。 -
接下来我们将之前定义的函数与
Base_Parser()
连接起来:static BaseAST* Base_Parser() { switch(Current_token) { … … … case IF_TOKEN : return If_parser(); … }
-
现在已经通过解析器将
if
/then
/else
的 AST 填充了表达式,是时候生成条件范式的 LLVM IR 了。让我们定义Codegen()
函数:Value *ExprIfAST::Codegen() { Value *Condtn = Cond->Codegen(); if (Condtn == 0) return 0; Condtn = Builder.CreateICmpNE( Condtn, Builder.getInt32(0), "ifcond"); Function *TheFunc = Builder.GetInsertBlock()->getParent(); BasicBlock *ThenBB = BasicBlock::Create(getGlobalContext(), "then", TheFunc); BasicBlock *ElseBB = BasicBlock::Create(getGlobalContext(), "else"); BasicBlock *MergeBB = BasicBlock::Create(getGlobalContext(), "ifcont"); Builder.CreateCondBr(Condtn, ThenBB, ElseBB); Builder.SetInsertPoint(ThenBB); Value *ThenVal = Then->Codegen(); if (ThenVal == 0) return 0; Builder.CreateBr(MergeBB); ThenBB = Builder.GetInsertBlock(); TheFunc->getBasicBlockList().push_back(ElseBB); Builder.SetInsertPoint(ElseBB); Value *ElseVal = Else->Codegen(); if (ElseVal == 0) return 0; Builder.CreateBr(MergeBB); ElseBB = Builder.GetInsertBlock(); TheFunc->getBasicBlockList().push_back(MergeBB); Builder.SetInsertPoint(MergeBB); PHINode *Phi = Builder.CreatePHI(Type::getInt32Ty(getGlobalContext()), 2, "iftmp"); Phi->addIncoming(ThenVal, ThenBB); Phi->addIncoming(ElseVal, ElseBB); return Phi; }
现在我们已经准备好了代码,让我们在一个包含if
/then
/else
结构的示例程序上编译并运行它。
如何工作…
执行以下步骤:
-
编译
toy.cpp
文件:$ g++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core ` -O3 -o toy
-
打开一个示例文件:
$ vi example
-
在示例文件中编写以下
if
/then
/else
代码:def fib(x) if x < 3 then 1 Else fib(x-1)+fib(x-2);
-
使用 TOY 编译器编译示例文件:
$ ./toy example
为if
/then
/else
代码生成的 LLVM IR 看起来如下:
; ModuleID = 'my compiler'
target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128"
define i32 @fib(i32 %x) {
entry:
%cmptmp = icmp ult i32 %x, 3
br i1 %cmptmp, label %ifcont, label %else
else: ; preds = %entry
%subtmp = add i32 %x, -1
%calltmp = call i32 @fib(i32 %subtmp)
%subtmp1 = add i32 %x, -2
%calltmp2 = call i32 @fib(i32 %subtmp1)
%addtmp = add i32 %calltmp2, %calltmp
br label %ifcont
ifcont: ; preds = %entry, %else
%iftmp = phi i32 [ %addtmp, %else ], [ 1, %entry ]
ret i32 %iftmp
}
下面是输出结果的样子:
解析器识别if
/then
/else
结构和在真和假条件下要执行的语句,并将它们存储在 AST 中。然后代码生成器将 AST 转换为 LLVM IR,其中生成条件语句。为真和假条件都生成了 IR。根据条件变量的状态,在运行时执行适当的语句。
参见
- 有关 Clang 如何处理 C++中的
if else
语句的详细示例,请参阅clang.llvm.org/doxygen/classclang_1_1IfStmt.html
。
生成循环的代码
循环使语言足够强大,可以在有限的代码行数内执行相同的操作多次。几乎每种语言都有循环。这个示例展示了在 TOY 语言中如何处理循环。
准备工作
循环通常有一个初始化归纳变量的起始点,一个指示归纳变量增加或减少的步长,以及一个终止循环的结束条件。在我们的 TOY 语言中,循环可以定义为以下内容:
for i = 1, i < n, 1 in
x + y;
起始表达式是i = 1
的初始化。循环的结束条件是i<n
。代码的第一行表示i
增加1
。
只要结束条件为真,循环就会迭代,并且在每次迭代后,归纳变量i
会增加 1。一个有趣的现象叫做PHI节点,它将决定归纳变量i
将取哪个值。记住,我们的 IR 是单赋值(SSA)形式。在控制流图中,对于给定的变量,其值可能来自两个不同的块。为了在 LLVM IR 中表示 SSA,定义了phi
指令。以下是一个phi
的例子:
%i = phi i32 [ 1, %entry ], [ %nextvar, %loop ]
上述 IR 表明i
的值可以来自两个基本块:%entry
和%loop
。%entry
块的值将是1
,而%nextvar
变量将来自%loop
。我们将在实现我们的玩具编译器的循环后看到详细信息。
如何做到这一点...
和任何其他表达式一样,循环也是通过在词法分析器中包含状态、定义用于存储循环值的 AST 数据结构以及定义解析器和Codegen()
函数来生成 LLVM IR 来处理的:
-
第一步是在
toy.cpp
文件中的词法分析器中定义标记:enum Token_Type { … … FOR_TOKEN, IN_TOKEN … … };
-
然后我们在词法分析器中包含逻辑:
static int get_token() { … … if (Identifier_string == "else") return ELSE_TOKEN; if (Identifier_string == "for") return FOR_TOKEN; if (Identifier_string == "in") return IN_TOKEN; … … }
-
下一步是定义
for
循环的 AST:class ExprForAST : public BaseAST { std::string Var_Name; BaseAST *Start, *End, *Step, *Body; public: ExprForAST (const std::string &varname, BaseAST *start, BaseAST *end, BaseAST *step, BaseAST *body) : Var_Name(varname), Start(start), End(end), Step(step), Body(body) {} Value *Codegen() override; };
-
然后我们定义循环的解析逻辑:
static BaseAST *For_parser() { next_token(); if (Current_token != IDENTIFIER_TOKEN) return 0; std::string IdName = Identifier_string; next_token(); if (Current_token != '=') return 0; next_token(); BaseAST *Start = expression_parser(); if (Start == 0) return 0; if (Current_token != ',') return 0; next_token(); BaseAST *End = expression_parser(); if (End == 0) return 0; BaseAST *Step = 0; if (Current_token == ',') { next_token(); Step = expression_parser(); if (Step == 0) return 0; } if (Current_token != IN_TOKEN) return 0; next_token(); BaseAST *Body = expression_parser(); if (Body == 0) return 0; return new ExprForAST (IdName, Start, End, Step, Body); }
-
接下来我们定义
Codegen()
函数以生成 LLVM IR:Value *ExprForAST::Codegen() { Value *StartVal = Start->Codegen(); if (StartVal == 0) return 0; Function *TheFunction = Builder.GetInsertBlock()->getParent(); BasicBlock *PreheaderBB = Builder.GetInsertBlock(); BasicBlock *LoopBB = BasicBlock::Create(getGlobalContext(), "loop", TheFunction); Builder.CreateBr(LoopBB); Builder.SetInsertPoint(LoopBB); PHINode *Variable = Builder.CreatePHI(Type::getInt32Ty(getGlobalContext()), 2, Var_Name.c_str()); Variable->addIncoming(StartVal, PreheaderBB); Value *OldVal = Named_Values[Var_Name]; Named_Values[Var_Name] = Variable; if (Body->Codegen() == 0) return 0; Value *StepVal; if (Step) { StepVal = Step->Codegen(); if (StepVal == 0) return 0; } else { StepVal = ConstantInt::get(Type::getInt32Ty(getGlobalContext()), 1); } Value *NextVar = Builder.CreateAdd(Variable, StepVal, "nextvar"); Value *EndCond = End->Codegen(); if (EndCond == 0) return EndCond; EndCond = Builder.CreateICmpNE( EndCond, ConstantInt::get(Type::getInt32Ty(getGlobalContext()), 0), "loopcond"); BasicBlock *LoopEndBB = Builder.GetInsertBlock(); BasicBlock *AfterBB = BasicBlock::Create(getGlobalContext(), "afterloop", TheFunction); Builder.CreateCondBr(EndCond, LoopBB, AfterBB); Builder.SetInsertPoint(AfterBB); Variable->addIncoming(NextVar, LoopEndBB); if (OldVal) Named_Values[Var_Name] = OldVal; else Named_Values.erase(Var_Name); return Constant::getNullValue(Type::getInt32Ty(getGlobalContext())); }
它是如何工作的...
执行以下步骤:
-
编译
toy.cpp
文件:$ g++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core ` -O3 -o toy
-
打开一个示例文件:
$ vi example
-
在示例文件中为
for
循环编写以下代码:def printstar(n x) for i = 1, i < n, 1.0 in x + 1
-
使用 TOY 编译器编译示例文件:
$ ./toy example
-
以下
for
循环代码的 LLVM IR 如下:; ModuleID = 'my compiler' target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128" define i32 @printstar(i32 %n, i32 %x) { entry: br label %loop loop: ; preds = %loop, %entry %i = phi i32 [ 1, %entry ], [ %nextvar, %loop ] %nextvar = add i32 %i, 1 %cmptmp = icmp ult i32 %i, %n br i1 %cmptmp, label %loop, label %afterloop afterloop: ; preds = %loop ret i32 0 }
你刚才看到的解析器识别了循环、归纳变量的初始化、终止条件、归纳变量的步长值以及循环体。然后它将 LLVM IR 中每个块转换为前面看到的。
如前所述,一个phi
指令从两个基本块%entry
和%loop
中为变量i
获取两个值。在前面的例子中,%entry
块表示循环开始时分配给归纳变量的值(这是1
)。i
的下一个更新值来自%loop
块,它完成了循环的一次迭代。
参见
- 要详细了解 Clang 中如何处理 C++中的循环,请访问
llvm.org/viewvc/llvm-project/cfe/trunk/lib/Parse/ParseExprCXX.cpp
处理用户定义的操作符 - 二元操作符
用户定义的操作符类似于 C++中的操作符重载概念,其中默认的操作符定义被修改以在多种对象上操作。通常,操作符是一元或二元操作符。使用现有基础设施实现二元操作符重载更容易。一元操作符需要一些额外的代码来处理。首先,将定义二元操作符重载,然后研究一元操作符重载。
准备工作
第一部分是定义一个用于重载的二进制操作符。逻辑或操作符(|
)是一个很好的起点。在我们的 TOY 语言中,|
操作符可以如下使用:
def binary | (LHS RHS)
if LHS then
1
else if RHS then
1
else
0;
如前所述,如果 LHS 或 RHS 的任何值不等于 0,则返回1
。如果 LHS 和 RHS 都为 null,则返回0
。
如何做到这一点...
执行以下步骤:
-
第一步,像往常一样,是追加二元操作符的
enum
状态,并在遇到binary
关键字时返回枚举状态:enum Token_Type { … … BINARY_TOKEN } static int get_token() { … … if (Identifier_string == "in") return IN_TOKEN; if (Identifier_string == "binary") return BINARY_TOKEN; … … }
-
下一步是为同一运算符添加 AST。请注意,不需要定义新的 AST。它可以由函数声明 AST 处理。我们只需要通过添加一个标志来修改它,以表示它是否为二进制运算符。如果是,则确定其优先级:
class FunctionDeclAST { std::string Func_Name; std::vector<std::string> Arguments; bool isOperator; unsigned Precedence; public: FunctionDeclAST(const std::string &name, const std::vector<std::string> &args, bool isoperator = false, unsigned prec = 0) : Func_Name(name), Arguments(args), isOperator(isoperator), Precedence(prec) {} bool isUnaryOp() const { return isOperator && Arguments.size() == 1; } bool isBinaryOp() const { return isOperator && Arguments.size() == 2; } char getOperatorName() const { assert(isUnaryOp() || isBinaryOp()); return Func_Name[Func_Name.size() - 1]; } unsigned getBinaryPrecedence() const { return Precedence; } Function *Codegen(); };
-
一旦修改后的 AST 准备好了,下一步是修改函数声明的解析器:
static FunctionDeclAST *func_decl_parser() { std::string FnName; unsigned Kind = 0; unsigned BinaryPrecedence = 30; switch (Current_token) { default: return 0; case IDENTIFIER_TOKEN: FnName = Identifier_string; Kind = 0; next_token(); break; case UNARY_TOKEN: next_token(); if (!isascii(Current_token)) return 0; FnName = "unary"; FnName += (char)Current_token; Kind = 1; next_token(); break; case BINARY_TOKEN: next_token(); if (!isascii(Current_token)) return 0; FnName = "binary"; FnName += (char)Current_token; Kind = 2; next_token(); if (Current_token == NUMERIC_TOKEN) { if (Numeric_Val < 1 || Numeric_Val > 100) return 0; BinaryPrecedence = (unsigned)Numeric_Val; next_token(); } break; } if (Current_token != '(') return 0; std::vector<std::string> Function_Argument_Names; while (next_token() == IDENTIFIER_TOKEN) Function_Argument_Names.push_back(Identifier_string); if (Current_token != ')') return 0; next_token(); if (Kind && Function_Argument_Names.size() != Kind) return 0; return new FunctionDeclAST(FnName, Function_Argument_Names, Kind != 0, BinaryPrecedence); }
-
然后我们修改二进制 AST 的
Codegen()
函数:Value* BinaryAST::Codegen() { Value* L = LHS->Codegen(); Value* R = RHS->Codegen(); switch(Bin_Operator) { case '+' : return Builder.CreateAdd(L, R, "addtmp"); case '-' : return Builder.CreateSub(L, R, "subtmp"); case '*': return Builder.CreateMul(L, R, "multmp"); case '/': return Builder.CreateUDiv(L, R, "divtmp"); case '<' : L = Builder.CreateICmpULT(L, R, "cmptmp"); return Builder.CreateUIToFP(L, Type::getIntTy(getGlobalContext()), "booltmp"); default : break; } Function *F = TheModule->getFunction(std::string("binary")+Op); Value *Ops[2] = { L, R }; return Builder.CreateCall(F, Ops, "binop"); }
-
下一步我们修改函数定义;它可以定义为:
Function* FunctionDefnAST::Codegen() { Named_Values.clear(); Function *TheFunction = Func_Decl->Codegen(); if (!TheFunction) return 0; if (Func_Decl->isBinaryOp()) Operator_Precedence [Func_Decl->getOperatorName()] = Func_Decl->getBinaryPrecedence(); BasicBlock *BB = BasicBlock::Create(getGlobalContext(), "entry", TheFunction); Builder.SetInsertPoint(BB); if (Value* Return_Value = Body->Codegen()) { Builder.CreateRet(Return_Value); … …
它是如何工作的...
执行以下步骤:
-
编译
toy.cpp
文件:$ g++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core ` -O3 -o toy
-
打开一个示例文件:
$ vi example
-
在示例文件中编写以下二进制运算符重载代码:
def binary| 5 (LHS RHS) if LHS then 1 else if RHS then 1 else 0;
-
使用 TOY 编译器编译示例文件:
$ ./toy example output : ; ModuleID = 'my compiler' target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128" define i32 @"binary|"(i32 %LHS, i32 %RHS) { entry: %ifcond = icmp eq i32 %LHS, 0 %ifcond1 = icmp eq i32 %RHS, 0 %. = select i1 %ifcond1, i32 0, i32 1 %iftmp5 = select i1 %ifcond, i32 %., i32 1 ret i32 %iftmp5 }
我们刚刚定义的二进制运算符将被解析。其定义也将被解析。每当遇到|
二进制运算符时,LHS 和 RHS 将被初始化,并执行定义体,根据定义给出适当的结果。在上面的示例中,如果 LHS 或 RHS 中任意一个不为零,则结果为1
。如果 LHS 和 RHS 都为零,则结果为0
。
参见
- 有关处理其他二进制运算符的详细示例,请参阅
llvm.org/docs/tutorial/LangImpl6.html
处理用户定义的运算符 - 一元运算符
我们在先前的配方中看到了如何处理二进制运算符。一种语言也可能有一些一元运算符,它们对一个操作数进行操作。在这个配方中,我们将看到如何处理一元运算符。
准备工作
第一步是在 TOY 语言中定义一元运算符。一个简单的 NOT 一元运算符(!
)可以作为一个很好的例子;让我们看看一个定义:
def unary!(v)
if v then
0
else
1;
如果值v
等于1
,则返回0
。如果值是0
,则输出1
。
如何做到这一点...
执行以下步骤:
-
第一步是在
toy.cpp
文件中定义一元运算符的enum
标记:enum Token_Type { … … BINARY_TOKEN, UNARY_TOKEN }
-
然后我们识别一元字符串并返回一个一元标记:
static int get_token() { … … if (Identifier_string == "in") return IN_TOKEN; if (Identifier_string == "binary") return BINARY_TOKEN; if (Identifier_string == "unary") return UNARY_TOKEN; … … }
-
接下来,我们为一元运算符定义 AST:
class ExprUnaryAST : public BaseAST { char Opcode; BaseAST *Operand; public: ExprUnaryAST(char opcode, BaseAST *operand) : Opcode(opcode), Operand(operand) {} virtual Value *Codegen(); };
-
AST 现在准备好了。让我们为一元运算符定义一个解析器:
static BaseAST *unary_parser() { if (!isascii(Current_token) || Current_token == '(' || Current_token == ',') return Base_Parser(); int Op = Current_token; next_token(); if (ExprAST *Operand = unary_parser()) return new ExprUnaryAST(Opc, Operand); return 0; }
-
下一步是从二进制运算符解析器中调用
unary_parser()
函数:static BaseAST *binary_op_parser(int Old_Prec, BaseAST *LHS) { while (1) { int Operator_Prec = getBinOpPrecedence(); if (Operator_Prec < Old_Prec) return LHS; int BinOp = Current_token; next_token(); BaseAST *RHS = unary_parser(); if (!RHS) return 0; int Next_Prec = getBinOpPrecedence(); if (Operator_Prec < Next_Prec) { RHS = binary_op_parser(Operator_Prec + 1, RHS); if (RHS == 0) return 0; } LHS = new BinaryAST(std::to_string(BinOp), LHS, RHS); } }
-
现在让我们从表达式解析器中调用
unary_parser()
函数:static BaseAST *expression_parser() { BaseAST *LHS = unary_parser(); if (!LHS) return 0; return binary_op_parser(0, LHS); }
-
然后我们修改函数声明解析器:
static FunctionDeclAST* func_decl_parser() { std::string Function_Name = Identifier_string; unsigned Kind = 0; unsigned BinaryPrecedence = 30; switch (Current_token) { default: return 0; case IDENTIFIER_TOKEN: Function_Name = Identifier_string; Kind = 0; next_token(); break; case UNARY_TOKEN: next_token(); if (!isascii(Current_token)) return0; Function_Name = "unary"; Function_Name += (char)Current_token; Kind = 1; next_token(); break; case BINARY_TOKEN: next_token(); if (!isascii(Current_token)) return 0; Function_Name = "binary"; Function_Name += (char)Current_token; Kind = 2; next_token(); if (Current_token == NUMERIC_TOKEN) { if (Numeric_Val < 1 || Numeric_Val > 100) return 0; BinaryPrecedence = (unsigned)Numeric_Val; next_token(); } break; } if (Current_token ! = '(') { printf("error in function declaration"); return 0; } std::vector<std::string> Function_Argument_Names; while(next_token() == IDENTIFIER_TOKEN) Function_Argument_Names.push_back(Identifier_string); if(Current_token != ')') { printf("Expected ')' "); return 0; } next_token(); if (Kind && Function_Argument_Names.size() != Kind) return 0; return new FunctionDeclAST(Function_Name, Function_Arguments_Names, Kind !=0, BinaryPrecedence); }
-
最后一步是为一元运算符定义
Codegen()
函数:Value *ExprUnaryAST::Codegen() { Value *OperandV = Operand->Codegen(); if (OperandV == 0) return 0; Function *F = TheModule->getFunction(std::string("unary")+Opcode); if (F == 0) return 0; return Builder.CreateCall(F, OperandV, "unop"); }
它是如何工作的...
执行以下步骤:
-
编译
toy.cpp
文件:$ g++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core ` -O3 -o toy
-
打开一个示例文件:
$ vi example
-
在示例文件中编写以下一元运算符重载代码:
def unary!(v) if v then 0 else 1;
-
使用 TOY 编译器编译示例文件:
$ ./toy example
输出应该如下所示:
; ModuleID = 'my compiler' target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128" define i32 @"unary!"(i32 %v) { entry: %ifcond = icmp eq i32 %v, 0 %. = select i1 %ifcond, i32 1, i32 0 ret i32 %. }
用户定义的一元运算符将被解析,并为它生成 IR。在你刚才看到的例子中,如果一元操作数不为零,则结果为0
。如果操作数为零,则结果为1
。
参见
- 要了解一元运算符的更详细实现,请访问
llvm.org/docs/tutorial/LangImpl6.html
添加 JIT 支持
可以将各种工具应用于 LLVM IR。例如,正如在第一章中所示,LLVM 设计和使用,IR 可以被转换为位码或汇编。可以在 IR 上运行一个名为 opt 的优化工具。IR 作为通用平台——所有这些工具的抽象层。
可以添加 JIT 支持。它立即评估输入的最高级表达式。例如,1 + 2;
,一旦输入,就会评估代码并打印出值 3
。
如何做到这一点...
执行以下步骤:
-
在
toy.cpp
文件中为执行引擎定义一个静态全局变量:static ExecutionEngine *TheExecutionEngine;
-
在
toy.cpp
文件的main()
函数中,编写 JIT 代码:int main() { … … init_precedence(); TheExecutionEngine = EngineBuilder(TheModule).create(); … … }
-
修改
toy.cpp
文件中的顶层表达式解析器:static void HandleTopExpression() { if (FunctionDefAST *F = expression_parser()) if (Function *LF = F->Codegen()) { LF -> dump(); void *FPtr = TheExecutionEngine->getPointerToFunction(LF); int (*Int)() = (int (*)())(intptr_t)FPtr; printf("Evaluated to %d\n", Int()); } else next_token(); }
它是如何工作的...
执行以下步骤:
-
编译
toy.cpp
程序:$ g++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core mcjit native` -O3 -o toy
-
打开一个示例文件:
$ vi example
-
在示例文件中编写以下 TOY 代码:
… 4+5;
-
最后,在示例文件上运行 TOY 编译器:
$ ./toy example The output will be define i32 @0() { entry: ret i32 9 }
LLVM JIT 编译器匹配原生平台 ABI,将结果指针转换为该类型的函数指针,并直接调用它。JIT 编译的代码与与应用程序静态链接的原生机器代码之间没有区别。
第四章 准备优化
在本章中,我们将介绍以下内容:
-
不同的优化级别
-
编写自己的 LLVM 插件
-
使用 opt 工具运行自己的插件
-
在新插件中使用另一个插件
-
使用插件管理器注册插件
-
编写一个分析插件
-
编写别名分析插件
-
使用其他分析插件
简介
一旦源代码转换完成,输出将以 LLVM IR 形式呈现。这个 IR 作为将代码转换为汇编代码的通用平台,具体取决于后端。然而,在转换为汇编代码之前,IR 可以被优化以生成更有效的代码。IR 是 SSA 形式,其中每个对变量的新赋值都是一个新变量——这是 SSA 表示的经典案例。
在 LLVM 基础设施中,一个插件用于优化 LLVM IR。插件在 LLVM IR 上运行,处理 IR,分析它,识别优化机会,并修改 IR 以生成优化代码。命令行界面 opt 用于在 LLVM IR 上运行优化插件。
在接下来的章节中,将讨论各种优化技术。还将探讨如何编写和注册新的优化插件。
不同的优化级别
优化级别有多种,从 0 级开始,到 3 级结束(也有 s
用于空间优化)。随着优化级别的提高,代码的优化程度也越来越高。让我们尝试探索各种优化级别。
准备工作...
通过在 LLVM IR 上运行 opt 命令行界面,可以理解不同的优化级别。为此,可以使用 Clang 前端首先将一个示例 C 程序转换为 IR。
-
打开一个
example.c
文件,并在其中编写以下代码:$ vi example.c int main(int argc, char **argv) { int i, j, k, t = 0; for(i = 0; i < 10; i++) { for(j = 0; j < 10; j++) { for(k = 0; k < 10; k++) { t++; } } for(j = 0; j < 10; j++) { t++; } } for(i = 0; i < 20; i++) { for(j = 0; j < 20; j++) { t++; } for(j = 0; j < 20; j++) { t++; } } return t; }
-
现在可以使用
clang
命令将其转换为 LLVM IR,如下所示:$ clang –S –O0 –emit-llvm example.c
将生成一个新的文件,
example.ll
,其中包含 LLVM IR。此文件将用于演示可用的各种优化级别。
如何操作…
执行以下步骤:
-
可以在生成的 IR 文件
example.ll
上运行 opt 命令行工具:$ opt –O0 –S example.ll
–O0
语法指定了最低的优化级别。 -
同样,您可以运行其他优化级别:
$ opt –O1 –S example.ll $ opt –O2 –S example.ll $ opt –O3 –S example.ll
它是如何工作的…
opt 命令行界面接受 example.ll
文件作为输入,并运行每个优化级别中指定的插件系列。它可以在同一优化级别中重复某些插件。要查看每个优化级别中使用的插件,您必须添加 --debug-pass=Structure
命令行选项到之前的 opt 命令中。
相关内容
- 要了解更多关于可以与 opt 工具一起使用的其他选项,请参阅
llvm.org/docs/CommandGuide/opt.html
编写自己的 LLVM 插件
所有 LLVM pass 都是pass
类的子类,它们通过重写从pass
继承的虚方法来实现功能。LLVM 对目标程序应用一系列分析和转换。pass 是 Pass LLVM 类的实例。
准备工作
让我们看看如何编写一个 pass。让我们把这个 pass 命名为function block counter
;一旦完成,它将在运行时简单地显示函数的名称并计算该函数中的基本块数量。首先,需要为这个 pass 编写一个Makefile
。按照以下步骤编写Makefile
:
-
在
llvm lib/Transform
文件夹中打开一个Makefile
:$ vi Makefile
-
指定 LLVM 根文件夹的路径和库名称,并在
Makefile
中指定它,如下所示:LEVEL = ../../.. LIBRARYNAME = FuncBlockCount LOADABLE_MODULE = 1 include $(LEVEL)/Makefile.common
这个Makefile
指定当前目录中的所有.cpp
文件都要编译并链接成一个共享对象。
如何做…
执行以下步骤:
-
创建一个名为
FuncBlockCount.cpp
的新.cpp
文件:$ vi FuncBlockCount.cpp
-
在这个文件中,包含一些来自 LLVM 的头文件:
#include "llvm/Pass.h" #include "llvm/IR/Function.h" #include "llvm/Support/raw_ostream.h"
-
包含
llvm
命名空间以启用对 LLVM 函数的访问:using namespace llvm;
-
然后从匿名命名空间开始:
namespace {
-
接下来声明这个 pass:
struct FuncBlockCount : public FunctionPass {
-
然后声明 pass 标识符,LLVM 将使用它来识别 pass:
static char ID; FuncBlockCount() : FunctionPass(ID) {}
-
这个步骤是编写一个 pass 过程中最重要的步骤之一——编写一个
run
函数。因为这个 pass 继承了FunctionPass
并在函数上运行,所以定义了一个runOnFunction
来在函数上运行:bool runOnFunction(Function &F) override { errs() << "Function " << F.getName() << '\n'; return false; } }; }
这个函数打印正在处理的函数的名称。
-
下一步是初始化 pass ID:
char FuncBlockCount::ID = 0;
-
最后,需要注册这个 pass,包括命令行参数和名称:
static RegisterPass<FuncBlockCount> X("funcblockcount", "Function Block Count", false, false);
将所有内容组合起来,整个代码看起来像这样:
#include "llvm/Pass.h" #include "llvm/IR/Function.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { struct FuncBlockCount : public FunctionPass { static char ID; FuncBlockCount() : FunctionPass(ID) {} bool runOnFunction(Function &F) override { errs() << "Function " << F.getName() << '\n'; return false; } }; } char FuncBlockCount::ID = 0; static RegisterPass<FuncBlockCount> X("funcblockcount", "Function Block Count", false, false);
工作原理
一个简单的gmake
命令编译文件,因此会在 LLVM 根目录下生成一个新的文件FuncBlockCount.so
。这个共享对象文件可以动态加载到 opt 工具中,以便在 LLVM IR 代码上运行。如何在下一节中加载和运行它将会演示。
参见
- 要了解更多关于如何从头开始构建 pass 的信息,请访问
llvm.org/docs/WritingAnLLVMPass.html
使用 opt 工具运行自己的 pass
在前面的配方中编写的 pass,即编写自己的 LLVM pass,已经准备好在 LLVM IR 上运行。这个 pass 需要动态加载,以便 opt 工具能够识别和执行它。
如何做…
执行以下步骤:
-
在
sample.c
文件中编写 C 测试代码,我们将在下一步将其转换为.ll
文件:$ vi sample.c int foo(int n, int m) { int sum = 0; int c0; for (c0 = n; c0 > 0; c0--) { int c1 = m; for (; c1 > 0; c1--) { sum += c0 > c1 ? 1 : 0; } } return sum; }
-
使用以下命令将 C 测试代码转换为 LLVM IR:
$ clang –O0 –S –emit-llvm sample.c –o sample.ll
这将生成一个
sample.ll
文件。 -
使用以下命令使用 opt 工具运行新的 pass:
$ opt -load (path_to_.so_file)/FuncBlockCount.so -funcblockcount sample.ll
输出将类似于以下内容:
Function foo
它是如何工作的…
如前述代码所示,共享对象动态地加载到 opt 命令行工具中并运行 pass。它遍历函数并显示其名称。它不修改 IR。新 pass 的进一步增强将在下一菜谱中演示。
参见
- 要了解更多关于 Pass 类的各种类型的信息,请访问
llvm.org/docs/WritingAnLLVMPass.html#pass-classes-and-requirements
在新 pass 中使用另一个 pass
一个 pass 可能需要另一个 pass 来获取一些分析数据、启发式方法或任何此类信息以决定进一步的行动。该 pass 可能只需要一些分析,如内存依赖,或者它可能还需要修改后的 IR。你刚刚看到的新的 pass 只是简单地打印出函数的名称。让我们看看如何增强它以计算循环中的基本块数量,这同时也展示了如何使用其他 pass 的结果。
准备工作
在上一个菜谱中使用的代码保持不变。然而,为了增强它——如下一节所示——以便它能够计算 IR 中的基本块数量,需要进行一些修改。
如何做到这一点…
getAnalysis
函数用于指定将使用哪个其他 pass:
-
由于新的 pass 将计算基本块的数量,它需要循环信息。这通过使用
getAnalysis
循环函数来指定:LoopInfo *LI = &getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
-
这将调用
LoopInfo
pass 以获取有关循环的信息。遍历此对象提供了基本块信息:unsigned num_Blocks = 0; Loop::block_iterator bb; for(bb = L->block_begin(); bb != L->block_end();++bb) num_Blocks++; errs() << "Loop level " << nest << " has " << num_Blocks << " blocks\n";
-
这将遍历循环以计算其内部的基本块数量。然而,它只计算最外层循环中的基本块。要获取最内层循环的信息,递归调用
getSubLoops
函数将有所帮助。将逻辑放入单独的函数并递归调用它更有意义:void countBlocksInLoop(Loop *L, unsigned nest) { unsigned num_Blocks = 0; Loop::block_iterator bb; for(bb = L->block_begin(); bb != L->block_end();++bb) num_Blocks++; errs() << "Loop level " << nest << " has " << num_Blocks << " blocks\n"; std::vector<Loop*> subLoops = L->getSubLoops(); Loop::iterator j, f; for (j = subLoops.begin(), f = subLoops.end(); j != f; ++j) countBlocksInLoop(*j, nest + 1); } virtual bool runOnFunction(Function &F) { LoopInfo *LI = &getAnalysis<LoopInfoWrapperPass>().getLoopInfo(); errs() << "Function " << F.getName() + "\n"; for (Loop *L : *LI) countBlocksInLoop(L, 0); return false; }
它是如何工作的…
新修改的 pass 现在需要在样本程序上运行。按照以下步骤修改并运行样本程序:
-
打开
sample.c
文件,并用以下程序替换其内容:int main(int argc, char **argv) { int i, j, k, t = 0; for(i = 0; i < 10; i++) { for(j = 0; j < 10; j++) { for(k = 0; k < 10; k++) { t++; } } for(j = 0; j < 10; j++) { t++; } } for(i = 0; i < 20; i++) { for(j = 0; j < 20; j++) { t++; } for(j = 0; j < 20; j++) { t++; } } return t; }
-
使用 Clang 将其转换为
.ll
文件:$ clang –O0 –S –emit-llvm sample.c –o sample.ll
-
在先前的样本程序上运行新的 pass:
$ opt -load (path_to_.so_file)/FuncBlockCount.so -funcblockcount sample.ll
输出将类似于以下内容:
Function main Loop level 0 has 11 blocks Loop level 1 has 3 blocks Loop level 1 has 3 blocks Loop level 0 has 15 blocks Loop level 1 has 7 blocks Loop level 2 has 3 blocks Loop level 1 has 3 blocks
更多内容…
LLVM 的 pass manager 提供了一个调试 pass 选项,它给我们机会看到哪些 pass 与我们的分析和优化交互,如下所示:
$ opt -load (path_to_.so_file)/FuncBlockCount.so -funcblockcount sample.ll –disable-output –debug-pass=Structure
使用 pass manager 注册一个 pass
到目前为止,一个新的 pass 是一个独立运行的动态对象。opt 工具由一系列这样的 pass 组成,这些 pass 已注册到 pass manager 和 LLVM 中。让我们看看如何将我们的 pass 注册到 Pass Manager 中。
准备工作
PassManager
类接受一个 pass 列表,确保它们的先决条件设置正确,然后安排 pass 以高效运行。Pass Manager 执行两个主要任务以尝试减少一系列 pass 的执行时间:
-
共享分析结果,以尽可能避免重新计算分析结果
-
通过流水线传递的执行,将程序中的传递执行流水线化,以通过流水线传递来获得一系列传递更好的缓存和内存使用行为
如何做…
按照给定步骤使用 Pass Manager 注册传递:
-
在
FuncBlockCount.cpp
文件中定义DEBUG_TYPE
宏,指定调试名称:#define DEBUG_TYPE "func-block-count"
-
在
FuncBlockCount
结构中,指定getAnalysisUsage
语法如下:void getAnalysisUsage(AnalysisUsage &AU) const override { AU.addRequired<LoopInfoWrapperPass>(); }
-
现在初始化新传递的宏:
INITIALIZE_PASS_BEGIN(FuncBlockCount, " funcblockcount ", "Function Block Count", false, false) INITIALIZE_PASS_DEPENDENCY(LoopInfoWrapperPass) INITIALIZE_PASS_END(FuncBlockCount, "funcblockcount", "Function Block Count", false, false) Pass *llvm::createFuncBlockCountPass() { return new FuncBlockCount(); }
-
在位于
include/llvm/
的LinkAllPasses.h
文件中添加createFuncBlockCount
传递函数:(void) llvm:: createFuncBlockCountPass ();
-
将声明添加到位于
include/llvm/Transforms
的Scalar.h
文件中:Pass * createFuncBlockCountPass ();
-
还要修改传递的构造函数:
FuncBlockCount() : FunctionPass(ID) {initializeFuncBlockCount Pass (*PassRegistry::getPassRegistry());}
-
在位于
lib/Transforms/Scalar/
的Scalar.cpp
文件中,添加初始化传递条目:initializeFuncBlockCountPass (Registry);
-
将此初始化声明添加到位于
include/llvm/
的InitializePasses.h
文件中:void initializeFuncBlockCountPass (Registry);
-
最后,将
FuncBlockCount.cpp
文件名添加到位于lib/Transforms/Scalar/
的CMakeLists.txt
文件中:FuncBlockCount.cpp
它是如何工作的…
使用指定在第一章的cmake
命令编译 LLVM,LLVM 设计和使用。Pass Manager 将包括此传递在 opt 命令行工具的传递管道中。此外,此传递可以从命令行独立运行:
$ opt –funcblockcount sample.ll
参见
- 要了解如何在 Pass Manager 中简单步骤添加传递,请研究
llvm.org/viewvc/llvm-project/llvm/trunk/lib/Transforms/Scalar/LoopInstSimplify.cpp
中的 LoopInstSimplify 传递
编写分析传递
分析传递提供了关于 IR 的高级信息,但实际上并不改变 IR。分析传递提供的结果可以被另一个分析传递使用来计算其结果。此外,一旦分析传递计算出结果,其结果可以被不同的传递多次使用,直到运行此传递的 IR 被更改。在本食谱中,我们将编写一个分析传递,用于计算并输出函数中使用的指令码数量。
准备工作
首先,我们编写将要运行传递的测试代码:
$ cat testcode.c
int func(int a, int b){
int sum = 0;
int iter;
for (iter = 0; iter < a; iter++) {
int iter1;
for (iter1 = 0; iter1 < b; iter1++) {
sum += iter > iter1 ? 1 : 0;
}
}
return sum;
}
将其转换为.bc
文件,我们将将其用作分析传递的输入:
$ clang -c -emit-llvm testcode.c -o testcode.bc
现在创建包含传递源代码的文件,位于llvm_root_dir/lib/Transforms/opcodeCounter
。在这里,opcodeCounter
是我们创建的目录,我们的传递源代码将驻留于此。
进行必要的Makefile
更改,以便此传递可以编译。
如何做…
现在让我们开始编写我们的分析传递的源代码:
-
包含必要的头文件并使用
llvm
命名空间:#define DEBUG_TYPE "opcodeCounter" #include "llvm/Pass.h" #include "llvm/IR/Function.h" #include "llvm/Support/raw_ostream.h" #include <map> using namespace llvm;
-
创建定义传递的结构:
namespace { struct CountOpcode: public FunctionPass {
-
在结构中,创建必要的用于计算指令码数量和表示传递 ID 的数据结构:
std::map< std::string, int> opcodeCounter; static char ID; CountOpcode () : FunctionPass(ID) {}
-
在前面的结构中,编写过程的实际实现代码,重载
runOnFunction
函数:virtual bool runOnFunction (Function &F) { llvm::outs() << "Function " << F.getName () << '\n'; for ( Function::iterator bb = F.begin(), e = F.end(); bb != e; ++bb) { for ( BasicBlock::iterator i = bb->begin(), e = bb->end(); i!= e; ++i) { if(opcodeCounter.find(i->getOpcodeName()) == opcodeCounter.end()) { opcodeCounter[i->getOpcodeName()] = 1; } else { opcodeCounter[i->getOpcodeName()] += 1; } } } std::map< std::string, int>::iterator i = opcodeCounter.begin(); std::map< std::string, int>::iterator e = opcodeCounter.end(); while (i != e) { llvm::outs() << i->first << ": " << i->second << "\n"; i++; } llvm::outs() << "\n"; opcodeCounter.clear(); return false; } }; }
-
编写注册过程的代码:
char CountOpcode::ID = 0; static RegisterPass<CountOpcode> X("opcodeCounter", "Count number of opcode in a functions");
-
使用
make
或cmake
命令编译此过程。 -
使用 opt 工具在测试代码上运行此过程,以获取函数中存在的操作码数量的信息:
$ opt -load path-to-build-folder/lib/LLVMCountopcodes.so -opcodeCounter -disable-output testcode.bc Function func add: 3 alloca: 5 br: 8 icmp: 3 load: 10 ret: 1 select: 1 store: 8
它是如何工作的…
此分析过程在函数级别上工作,为程序中的每个函数运行一次。因此,在声明CountOpcodes : public FunctionPass
结构时,我们继承了FunctionPass
函数。
opcodeCounter
函数记录函数中使用的每个操作码的数量。在下面的循环中,我们从所有函数中收集操作码:
for (Function::iterator bb = F.begin(), e = F.end(); bb != e; ++bb) {
for (BasicBlock::iterator i = bb->begin(), e = bb->end(); i != e; ++i) {
第一个for
循环遍历函数中存在的所有基本块,第二个for
循环遍历基本块中存在的所有指令。
第一个for
循环中的代码是实际收集操作码及其数量的代码。for
循环下面的代码是为了打印结果。由于我们使用映射来存储结果,我们遍历它以打印函数中操作码名称及其数量的配对。
我们返回false
,因为我们没有在测试代码中修改任何内容。代码的最后两行是为了将此过程注册为给定名称,以便 opt 工具可以使用此过程。
最后,在执行测试代码时,我们得到函数中使用的不同操作码及其数量。
编写别名分析过程
别名分析是一种技术,通过它我们可以知道两个指针是否指向同一位置——也就是说,是否可以通过多种方式访问同一位置。通过获取此分析的结果,您可以决定进一步的优化,例如公共子表达式消除。有不同方式和算法可以执行别名分析。在本配方中,我们不会处理这些算法,但我们将了解 LLVM 如何提供编写自己的别名分析过程的基础设施。在本配方中,我们将编写一个别名分析过程,以了解如何开始编写此类过程。我们不会使用任何特定算法,但在分析的每个情况下都会返回MustAlias
响应。
准备工作
编写用于别名分析的测试代码。在这里,我们将使用之前配方中使用的testcode.c
文件作为测试代码。
进行必要的Makefile
更改,通过在llvm/lib/Analysis/Analysis.cpp
、llvm/include/llvm/InitializePasses.h
、llvm/include/llvm/LinkAllPasses.h
和llvm/include/llvm/Analysis/Passes.h
中添加过程条目来注册过程,并在llvm_source_dir/lib/Analysis/
下创建一个名为EverythingMustAlias.cpp
的文件,该文件将包含我们过程的源代码。
如何做到这一点...
执行以下步骤:
-
包含必要的头文件并使用
llvm
命名空间:#include "llvm/Pass.h" #include "llvm/Analysis/AliasAnalysis.h" #include "llvm/IR/DataLayout.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IR/Module.h" using namespace llvm;
-
通过继承
ImmutablePass
和AliasAnalysis
类来为我们的传递创建一个结构:namespace { struct EverythingMustAlias : public ImmutablePass, public AliasAnalysis {
-
声明数据结构和构造函数:
static char ID; EverythingMustAlias() : ImmutablePass(ID) {} initializeEverythingMustAliasPass(*PassRegistry::getPassRegistry());}
-
实现用于获取调整后的分析指针的
getAdjustedAnalysisPointer
函数:void *getAdjustedAnalysisPointer(const void *ID) override { if (ID == &AliasAnalysis::ID) return (AliasAnalysis*)this; return this; }
-
实现用于初始化传递的
initializePass
函数:bool doInitialization(Module &M) override { DL = &M.getDataLayout(); return true; }
-
实现用于
alias
的函数:void *getAdjustedAnalysisPointer(const void *ID) override { if (ID == &AliasAnalysis::ID) return (AliasAnalysis*)this; return this; } }; }
-
注册传递:
char EverythingMustAlias::ID = 0; INITIALIZE_AG_PASS(EverythingMustAlias, AliasAnalysis, "must-aa", "Everything Alias (always returns 'must' alias)", true, true, true) ImmutablePass *llvm::createEverythingMustAliasPass() { return new EverythingMustAlias(); }
-
使用
cmake
或make
命令编译传递: -
使用编译传递后形成的
.so
文件执行测试代码:$ opt -must-aa -aa-eval -disable-output testcode.bc ===== Alias Analysis Evaluator Report ===== 10 Total Alias Queries Performed 0 no alias responses (0.0%) 0 may alias responses (0.0%) 0 partial alias responses (0.0%) 10 must alias responses (100.0%) Alias Analysis Evaluator Pointer Alias Summary: 0%/0%/0%/100% Alias Analysis Mod/Ref Evaluator Summary: no mod/ref!
它是如何工作的…
AliasAnalysis
类提供了各种别名分析实现应支持的接口。它导出 AliasResult
和 ModRefResult
枚举,分别表示 alias
和 modref
查询的结果。
alias
方法用于检查两个内存对象是否指向同一位置。它接受两个内存对象作为输入,并返回适当的 MustAlias
、PartialAlias
、MayAlias
或 NoAlias
。
getModRefInfo
方法返回有关指令执行是否可以读取或修改内存位置的信息。前面示例中的传递通过为每一对指针返回值 MustAlias
来工作,正如我们实现的那样。在这里,我们继承了 ImmutablePasses
类,它适合我们的传递,因为它是一个非常基础的传递。我们继承了 AliasAnalysis
传递,它为我们提供了实现接口。
当传递通过多重继承实现分析接口时,使用 getAdjustedAnalysisPointer
函数。如果需要,它应该覆盖此方法以调整指针以满足指定的传递信息。
initializePass
函数用于初始化包含 InitializeAliasAnalysis
方法的传递,该方法应包含实际的别名分析实现。
getAnalysisUsage
方法用于通过显式调用 AliasAnalysis::getAnalysisUsage
方法来声明对其他传递的任何依赖。
alias
方法用于确定两个内存对象是否相互别名。它接受两个内存对象作为输入,并返回适当的 MustAlias
、PartialAlias
、MayAlias
或 NoAlias
响应。
alias
方法之后的代码用于注册传递。最后,当我们使用此传递覆盖测试代码时,我们得到 10 个 MustAlias
响应(100.0%
),正如我们在传递中实现的那样。
参见
要更详细地了解 LLVM 别名分析,请参阅 llvm.org/docs/AliasAnalysis.html
。
使用其他分析传递
在这个菜谱中,我们将简要了解由 LLVM 提供的其他分析传递,这些传递可以用于获取关于基本块、函数、模块等的分析信息。我们将查看已经实现在内的传递,以及我们如何为我们的目的使用它们。我们不会查看所有传递,而只会查看其中的一些。
准备中…
在 testcode1.c
文件中编写测试代码,该文件将用于分析目的:
$ cat testcode1.c
void func() {
int i;
char C[2];
char A[10];
for(i = 0; i != 10; ++i) {
((short*)C)[0] = A[i];
C[1] = A[9-i];
}
}
使用以下命令行将 C 代码转换为位码格式:
$ clang -c -emit-llvm testcode1.c -o testcode1.bc
如何做到这一点...
按照给出的步骤使用其他分析遍历:
-
通过将
–aa-eval
作为命令行选项传递给 opt 工具来使用别名分析评估遍历:$ opt -aa-eval -disable-output testcode1.bc ===== Alias Analysis Evaluator Report ===== 36 Total Alias Queries Performed 0 no alias responses (0.0%) 36 may alias responses (100.0%) 0 partial alias responses (0.0%) 0 must alias responses (0.0%) Alias Analysis Evaluator Pointer Alias Summary: 0%/100%/0%/0% Alias Analysis Mod/Ref Evaluator Summary: no mod/ref!
-
使用
–print-dom-info
命令行选项与 opt 一起打印支配树信息:$ opt -print-dom-info -disable-output testcode1.bc =============================-------------------------------- Inorder Dominator Tree: [1] %0 {0,9} [2] %1 {1,8} [3] %4 {2,5} [4] %19 {3,4} [3] %22 {6,7}
-
使用
–count-aa
命令行选项与 opt 一起计算一个遍历对另一个遍历发出的查询次数:$ opt -count-aa -basicaa -licm -disable-output testcode1.bc No alias: [4B] i32* %i, [1B] i8* %7 No alias: [4B] i32* %i, [2B] i16* %12 No alias: [1B] i8* %7, [2B] i16* %12 No alias: [4B] i32* %i, [1B] i8* %16 Partial alias: [1B] i8* %7, [1B] i8* %16 No alias: [2B] i16* %12, [1B] i8* %16 Partial alias: [1B] i8* %7, [1B] i8* %16 No alias: [4B] i32* %i, [1B] i8* %18 No alias: [1B] i8* %18, [1B] i8* %7 No alias: [1B] i8* %18, [1B] i8* %16 Partial alias: [2B] i16* %12, [1B] i8* %18 Partial alias: [2B] i16* %12, [1B] i8* %18 ===== Alias Analysis Counter Report ===== Analysis counted: 12 Total Alias Queries Performed 8 no alias responses (66%) 0 may alias responses (0%) 4 partial alias responses (33%) 0 must alias responses (0%) Alias Analysis Counter Summary: 66%/0%/33%/0% 0 Total Mod/Ref Queries Performed
-
使用
-print-alias-sets
命令行选项在程序中打印别名集,带上 opt:$ opt -basicaa -print-alias-sets -disable-output testcode1.bc Alias Set Tracker: 3 alias sets for 5 pointer values. AliasSet[0x336b120, 1] must alias, Mod/Ref Pointers: (i32* %i, 4) AliasSet[0x336b1c0, 2] may alias, Ref Pointers: (i8* %7, 1), (i8* %16, 1) AliasSet[0x338b670, 2] may alias, Mod Pointers: (i16* %12, 2), (i8* %18, 1)
它是如何工作的...
在第一种情况下,当我们使用 -aa-eval
选项时,opt 工具运行别名分析评估遍历,该遍历将分析输出到屏幕上。它遍历函数中所有指针对,并查询这两个指针是否是别名。
使用 -print-dom-info
选项,运行打印支配树的遍历,通过这个遍历可以获得关于支配树的信息。
在第三种情况下,我们执行 opt -count-aa -basicaa –licm
命令。count-aa
命令选项计算 licm
遍历对 basicaa
遍历发出的查询次数。这个信息是通过 opt 工具的计数别名分析遍历获得的。
要打印程序中的所有别名集,我们使用 - print-alias-sets
命令行选项。在这种情况下,它将打印出使用 basicaa
遍历分析后获得的别名集。
参见
参考以下链接了解此处未提及的更多遍历:llvm.org/docs/Passes.html#anal
。
第五章:实现优化
在本章中,我们将涵盖以下食谱:
-
编写死代码消除传递
-
编写内联转换传递
-
编写内存优化传递
-
结合 LLVM IR
-
转换和优化循环
-
重新关联表达式
-
向量化 IR
-
其他优化传递
简介
在上一章中,我们看到了如何在 LLVM 中编写传递。我们还通过别名分析示例演示了编写几个分析传递。这些传递只是读取源代码并提供了有关它的信息。在本章中,我们将进一步编写转换传递,这些传递实际上会更改源代码,试图优化代码以实现更快的执行。在前两个食谱中,我们将向您展示如何编写转换传递以及它是如何更改代码的。之后,我们将看到我们如何可以在传递的代码中进行更改,以调整传递的行为。
编写死代码消除传递
在本食谱中,你将学习如何从程序中消除死代码。通过消除死代码,我们指的是移除对源程序执行输出的结果没有任何影响的代码。这样做的主要原因包括减少程序大小,从而提高代码质量并使代码更容易调试;以及提高程序的运行时间,因为不必要的代码被阻止执行。在本食谱中,我们将向您展示一种死代码消除的变体,称为激进死代码消除,它假设每一段代码都是死代码,直到证明其不是为止。我们将看到如何自己实现这个传递,以及我们需要对传递进行哪些修改,以便它可以在 LLVM 主干的lib/Transforms/Scalar
文件夹中的其他传递一样运行。
准备工作
为了展示死代码消除的实现,我们需要一段测试代码,我们将在这个代码上运行激进死代码消除传递:
$ cat testcode.ll
declare i32 @strlen(i8*) readonly nounwind
define void @test() {
call i32 @strlen( i8* null )
ret void
}
在这段测试代码中,我们可以看到在test
函数中调用了strlen
函数,但返回值没有被使用。因此,这应该被我们的传递视为死代码。
在文件中,包含位于/llvm/
的InitializePasses.h
文件;并在llvm
命名空间中,添加我们即将编写的传递的条目:
namespace llvm {
…
…
void initializeMYADCEPass(PassRegistry&); // Add this line
在scalar.h
文件中,位于include/llvm-c/scalar.h/Transform/
,添加传递的条目:
void LLVMAddMYAggressiveDCEPass(LLVMPassManagerRef PM);
在include/llvm/Transform/scalar.h
文件中,在llvm
命名空间中添加传递的条目:
FunctionPass *createMYAggressiveDCEPass();
在lib/Transforms/Scalar/scalar.cpp
文件中,在两个地方添加传递的条目。在void llvm::initializeScalarOpts(PassRegistry &Registry)
函数中,添加以下代码:
initializeMergedLoadStoreMotionPass(Registry); // already present in the file
initializeMYADCEPass(Registry); // add this line
initializeNaryReassociatePass(Registry); // already present in the file
…
…
void LLVMAddMemCpyOptPass(LLVMPassManagerRef PM) {
unwrap(PM)->add(createMemCpyOptPass());
}
// add the following three lines
void LLVMAddMYAggressiveDCEPass(LLVMPassManagerRef PM) {
unwrap(PM)->add(createMYAggressiveDCEPass());
}
void LLVMAddPartiallyInlineLibCallsPass(LLVMPassManagerRef PM) {
unwrap(PM)->add(createPartiallyInlineLibCallsPass());
}
…
如何做到这一点…
我们现在将编写传递的代码:
-
包含必要的头文件:
#include "llvm/Transforms/Scalar.h" #include "llvm/ADT/DepthFirstIterator.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/Statistic.h" #include "llvm/IR/BasicBlock.h" #include "llvm/IR/CFG.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/IntrinsicInst.h" #include "llvm/Pass.h" using namespace llvm;
-
声明我们的传递结构:
namespace { struct MYADCE : public FunctionPass { static char ID; // Pass identification, replacement for typeid MYADCE() : FunctionPass(ID) { initializeMYADCEPass(*PassRegistry::getPassRegistry()); } bool runOnFunction(Function& F) override; void getAnalysisUsage(AnalysisUsage& AU) const override { AU.setPreservesCFG(); } }; }
-
初始化传递及其 ID:
char MYADCE::ID = 0; INITIALIZE_PASS(MYADCE, "myadce", "My Aggressive Dead Code Elimination", false, false)
-
在
runOnFunction
函数中实现实际的传递:bool MYADCE::runOnFunction(Function& F) { if (skipOptnoneFunction(F)) return false; SmallPtrSet<Instruction*, 128> Alive; SmallVector<Instruction*, 128> Worklist; // Collect the set of "root" instructions that are known live. for (Instruction &I : inst_range(F)) { if (isa<TerminatorInst>(I) || isa<DbgInfoIntrinsic>(I) || isa<LandingPadInst>(I) || I.mayHaveSideEffects()) { Alive.insert(&I); Worklist.push_back(&I); } } // Propagate liveness backwards to operands. while (!Worklist.empty()) { Instruction *Curr = Worklist.pop_back_val(); for (Use &OI : Curr->operands()) { if (Instruction *Inst = dyn_cast<Instruction>(OI)) if (Alive.insert(Inst).second) Worklist.push_back(Inst); } } // the instructions which are not in live set are considered dead in this pass. The instructions which do not effect the control flow, return value and do not have any side effects are hence deleted. for (Instruction &I : inst_range(F)) { if (!Alive.count(&I)) { Worklist.push_back(&I); I.dropAllReferences(); } } for (Instruction *&I : Worklist) { I->eraseFromParent(); } return !Worklist.empty(); } } FunctionPass *llvm::createMYAggressiveDCEPass() { return new MYADCE(); }
-
在编译
testcode.ll
文件后运行前面的 pass,该文件可以在本教程的准备工作部分找到:$ opt -myadce -S testcode.ll ; ModuleID = 'testcode.ll' ; Function Attrs: nounwind readonly declare i32 @strlen(i8*) #0 define void @test() { ret void }
它是如何工作的...
此 pass 通过首先在runOnFunction
函数的第一个for
循环中收集所有活跃的根指令列表来工作。
使用这些信息,我们在while
(!Worklist.empty())
循环中向后传播活跃性到操作数。
在下一个for
循环中,我们移除不活跃的指令,即死代码。同时,我们检查是否对这些值有任何引用。如果有,我们将丢弃所有这样的引用,它们也是死代码。
在测试代码上运行此 pass 后,我们看到死代码;strlen
函数的调用被移除。
注意,代码已被添加到 LLVM 主分支修订号 234045 中。因此,当您实际尝试实现它时,一些定义可能会更新。在这种情况下,相应地修改代码。
参见
对于其他各种死代码消除方法,您可以参考llvm/lib/Transforms/Scalar
文件夹,其中包含其他类型 DCEs 的代码。
编写内联转换 pass
正如我们所知,内联意味着在调用点展开被调用函数的函数体,因为它可能通过代码的更快执行而变得有用。编译器决定是否内联一个函数。在本教程中,您将学习如何编写一个简单的函数内联 pass,该 pass 利用 LLVM 的内联实现。我们将编写一个处理带有alwaysinline
属性的函数的 pass。
准备工作
让我们编写一个测试代码,我们将在这个测试代码上运行我们的 pass。在lib/Transforms/IPO/IPO.cpp
和include/llvm/InitializePasses.h
文件、include/llvm/Transforms/IPO.h
文件以及/include/llvm-c/Transforms/IPO.h
文件中做出必要的更改,以包含以下 pass。还需要对makefile
进行必要的更改以包含此 pass:
$ cat testcode.c
define i32 @inner1() alwaysinline {
ret i32 1
}
define i32 @outer1() {
%r = call i32 @inner1()
ret i32 %r
}
如何做这件事...
我们现在将编写 pass 的代码:
-
包含必要的头文件:
#include "llvm/Transforms/IPO.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/Analysis/AliasAnalysis.h" #include "llvm/Analysis/AssumptionCache.h" #include "llvm/Analysis/CallGraph.h" #include "llvm/Analysis/InlineCost.h" #include "llvm/IR/CallSite.h" #include "llvm/IR/CallingConv.h" #include "llvm/IR/DataLayout.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/IntrinsicInst.h" #include "llvm/IR/Module.h" #include "llvm/IR/Type.h" #include "llvm/Transforms/IPO/InlinerPass.h" using namespace llvm;
-
描述我们的 pass 的类:
namespace { class MyInliner : public Inliner { InlineCostAnalysis *ICA; public: MyInliner() : Inliner(ID, -2000000000, /*InsertLifetime*/ true), ICA(nullptr) { initializeMyInlinerPass(*PassRegistry::getPassRegistry()); } MyInliner(bool InsertLifetime) : Inliner(ID, -2000000000, InsertLifetime), ICA(nullptr) { initializeMyInlinerPass(*PassRegistry::getPassRegistry()); } static char ID; InlineCost getInlineCost(CallSite CS) override; void getAnalysisUsage(AnalysisUsage &AU) const override; bool runOnSCC(CallGraphSCC &SCC) override; using llvm::Pass::doFinalization; bool doFinalization(CallGraph &CG) override { return removeDeadFunctions(CG, /*AlwaysInlineOnly=*/ true); } }; }
-
初始化 pass 并添加依赖项:
char MyInliner::ID = 0; INITIALIZE_PASS_BEGIN(MyInliner, "my-inline", "Inliner for always_inline functions", false, false) INITIALIZE_AG_DEPENDENCY(AliasAnalysis) INITIALIZE_PASS_DEPENDENCY(AssumptionTracker) INITIALIZE_PASS_DEPENDENCY(CallGraphWrapperPass) INITIALIZE_PASS_DEPENDENCY(InlineCostAnalysis) INITIALIZE_PASS_END(MyInliner, "my-inline", "Inliner for always_inline functions", false, false) Pass *llvm::createMyInlinerPass() { return new MyInliner(); } Pass *llvm::createMynlinerPass(bool InsertLifetime) { return new MyInliner(InsertLifetime); }
-
实现获取内联成本的函数:
InlineCost MyInliner::getInlineCost(CallSite CS) { Function *Callee = CS.getCalledFunction(); if (Callee && !Callee->isDeclaration() && CS.hasFnAttr(Attribute::AlwaysInline) && ICA->isInlineViable(*Callee)) return InlineCost::getAlways(); return InlineCost::getNever(); }
-
编写其他辅助方法:
bool MyInliner::runOnSCC(CallGraphSCC &SCC) { ICA = &getAnalysis<InlineCostAnalysis>(); return Inliner::runOnSCC(SCC); } void MyInliner::getAnalysisUsage(AnalysisUsage &AU) const { AU.addRequired<InlineCostAnalysis>(); Inliner::getAnalysisUsage(AU); }
-
编译通过。编译完成后,在先前的测试用例上运行它:
$ opt -inline-threshold=0 -always-inline -S test.ll ; ModuleID = 'test.ll' ; Function Attrs: alwaysinline define i32 @inner1() #0 { ret i32 1 } define i32 @outer1() { ret i32 1 }
它是如何工作的...
我们编写的这个 pass 将适用于具有alwaysinline
属性的函数。这个 pass 将始终内联这些函数。
这里工作的主函数是InlineCost
getInlineCost(CallSite
CS
)。这是一个位于
inliner.cpp文件中的函数,需要在这里重写。因此,基于这里计算的内联成本,我们决定是否内联一个函数。内联过程的实际实现,即内联过程是如何工作的,可以在
inliner.cpp`文件中找到。
在这种情况下,我们返回 InlineCost::getAlways()
;对于带有 alwaysinline
属性的函数。对于其他函数,我们返回 InlineCost::getNever()
。这样,我们可以实现这个简单情况的内联。如果你想要深入了解并尝试其他内联变体——以及学习如何做出内联决策——你可以查看 inlining.cpp
文件。
当这个传递在测试代码上运行时,我们看到 inner1
函数的调用被其实际函数体所替换。
编写内存优化传递
在这个配方中,我们将简要讨论一个处理内存优化的转换传递。
准备工作
对于这个配方,你需要安装 opt 工具。
如何实现它…
-
编写我们将运行
memcpy
优化传递的测试代码:$ cat memcopytest.ll @cst = internal constant [3 x i32] [i32 -1, i32 -1, i32 -1], align 4 declare void @llvm.memcpy.p0i8.p0i8.i64(i8* nocapture, i8* nocapture, i64, i32, i1) nounwind declare void @foo(i32*) nounwind define void @test1() nounwind { %arr = alloca [3 x i32], align 4 %arr_i8 = bitcast [3 x i32]* %arr to i8* call void @llvm.memcpy.p0i8.p0i8.i64(i8* %arr_i8, i8* bitcast ([3 x i32]* @cst to i8*), i64 12, i32 4, i1 false) %arraydecay = getelementptr inbounds [3 x i32], [3 x i32]* %arr, i64 0, i64 0 call void @foo(i32* %arraydecay) nounwind ret void }
-
在前面的测试用例上运行
memcpyopt
传递:$ opt -memcpyopt -S memcopytest.ll ; ModuleID = ' memcopytest.ll' @cst = internal constant [3 x i32] [i32 -1, i32 -1, i32 -1], align 4 ; Function Attrs: nounwind declare void @llvm.memcpy.p0i8.p0i8.i64(i8* nocapture, i8* nocapture readonly, i64, i32, i1) #0 ; Function Attrs: nounwind declare void @foo(i32*) #0 ; Function Attrs: nounwind define void @test1() #0 { %arr = alloca [3 x i32], align 4 %arr_i8 = bitcast [3 x i32]* %arr to i8* call void @llvm.memset.p0i8.i64(i8* %arr_i8, i8 -1, i64 12, i32 4, i1 false) %arraydecay = getelementptr inbounds [3 x i32]* %arr, i64 0, i64 0 call void @foo(i32* %arraydecay) #0 ret void } ; Function Attrs: nounwind declare void @llvm.memset.p0i8.i64(i8* nocapture, i8, i64, i32, i1) #0 attributes #0 = { nounwind }
它是如何工作的…
Memcpyopt
传递处理尽可能消除 memcpy
调用,或将它们转换为其他调用。
考虑这个 memcpy
调用:
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %arr_i8, i8* bitcast ([3 x i32]* @cst to i8*), i64 12, i32 4, i1 false)
。
在前面的测试用例中,这个传递将其转换为 memset
调用:
call void @llvm.memset.p0i8.i64(i8* %arr_i8, i8 -1, i64 12, i32 4, i1 false)
。
如果我们查看传递的源代码,我们会意识到这种转换是由 llvm/lib/Transforms/Scalar
文件夹中的 MemCpyOptimizer.cpp
文件中的 tryMergingIntoMemset
函数引起的。
tryMergingIntoMemset
函数在扫描指令时会寻找一些其他模式来折叠。它寻找相邻内存中的存储,并在看到连续的存储时,尝试将它们合并到 memset
中。
processMemSet
函数会检查这个 memset
附近的任何其他相邻的 memset
,这有助于我们扩展 memset
调用来创建一个更大的存储。
参见
要查看各种内存优化传递的详细信息,请访问 llvm.org/docs/Passes.html#memcpyopt-memcpy-optimization
。
结合 LLVM IR
在这个配方中,你将了解 LLVM 中的指令组合。通过指令组合,我们指的是用更有效的指令替换一系列指令,这些指令在更少的机器周期内产生相同的结果。在这个配方中,我们将看到我们如何修改 LLVM 代码以组合某些指令。
入门
为了测试我们的实现,我们将编写测试代码,我们将使用它来验证我们的实现是否正确地组合了指令:
define i32 @test19(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
}
如何实现它…
-
打开
lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
文件。 -
在
InstCombiner::visitXor(BinaryOperator &I)
函数中,进入if
条件——if
(Op0I && Op1I)`——并添加以下内容:if (match(Op0I, 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)); }
-
现在重新构建 LLVM,以便 Opt 工具可以使用新功能并按这种方式运行测试用例:
Opt –instcombine –S testcode.ll define i32 @test19(i32 %x, i32 %y, i32 %z) { %1 = xor i32 %y, %z %res = and i32 %1, %x ret i32 %res }
它是如何工作的…
在这个配方中,我们在指令组合文件中添加了代码,该代码处理涉及 AND、OR 和 XOR 运算符的转换。
我们添加了匹配(A
|
(B
^
C))
^
((A
^
C)
^
B)
形式的代码,并将其简化为A
&
(B
^
C)
。if (match(Op0I, 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));
这一行在构建新指令后返回简化后的值,替换了之前匹配的代码。
当我们在测试代码上运行instcombine
优化步骤时,我们得到减少后的结果。你可以看到操作数从五个减少到两个。
参考以下内容
- 指令组合的主题非常广泛,有大量的可能性。与指令组合功能相似的是指令简化功能,我们在其中简化复杂的指令,但不一定减少指令的数量,就像指令组合那样。要深入了解这一点,请查看
lib/Transforms/InstCombine
文件夹中的代码。
转换和优化循环
在这个配方中,我们将了解如何转换和优化循环以获得更短的执行时间。我们将主要关注循环不变代码移动(LICM)优化技术,并了解其工作原理和代码转换方式。我们还将查看一个相对简单的技术,称为循环删除,其中我们消除那些具有非无限、可计算的循环次数且对函数返回值没有副作用的无用循环。
准备工作
你必须为这个配方构建 opt 工具。
如何做…
-
为 LICM(Loop-Invariant Code Motion)优化步骤编写测试用例:
$ cat testlicm.ll define void @testfunc(i32 %i) { ; <label>:0 br label %Loop Loop: ; preds = %Loop, %0 %j = phi i32 [ 0, %0 ], [ %Next, %Loop ] ; <i32> [#uses=1] %i2 = mul i32 %i, 17 ; <i32> [#uses=1] %Next = add i32 %j, %i2 ; <i32> [#uses=2] %cond = icmp eq i32 %Next, 0 ; <i1> [#uses=1] br i1 %cond, label %Out, label %Loop Out: ; preds = %Loop ret void }
-
在以下测试代码上执行 LICM 优化步骤:
$ opt licmtest.ll -licm -S ; ModuleID = 'licmtest.ll' define void @testfunc(i32 %i) { %i2 = mul i32 %i, 17 br label %Loop Loop: ; preds = %Loop, %0 %j = phi i32 [ 0, %0 ], [ %Next, %Loop ] %Next = add i32 %j, %i2 %cond = icmp eq i32 %Next, 0 br i1 %cond, label %Out, label %Loop Out: ; preds = %Loop ret void }
-
为循环删除优化步骤编写测试代码:
$ cat deletetest.ll define void @foo(i64 %n, i64 %m) nounwind { entry: br label %bb bb: %x.0 = phi i64 [ 0, %entry ], [ %t0, %bb2 ] %t0 = add i64 %x.0, 1 %t1 = icmp slt i64 %x.0, %n br i1 %t1, label %bb2, label %return bb2: %t2 = icmp slt i64 %x.0, %m br i1 %t1, label %bb, label %return return: ret void }
-
最后,在测试代码上运行循环删除优化步骤:
$ opt deletetest.ll -loop-deletion -S ; ModuleID = "deletetest.ll' ; Function Attrs: nounwind define void @foo(i64 %n, i64 %m) #0 { entry: br label %return return: ; preds = %entry ret void } attributes #0 = { nounwind }
它是如何工作的…
LICM 优化步骤执行循环不变代码移动;它试图将循环中未修改的代码移出循环。它可以移动到循环前头的代码块之上,或者从退出块之后退出循环。
在前面显示的示例中,我们看到了代码的%i2
=
mul
i32
%i,
17
部分被移动到循环之上,因为它在那个示例中显示的循环块内没有被修改。
循环删除优化步骤会寻找那些具有非无限循环次数且不影响函数返回值的循环。
在测试代码中,我们看到了具有循环部分的两个基本块bb:
和bb2:
都被删除了。我们还看到了foo
函数直接跳转到返回语句。
还有许多其他优化循环的技术,例如loop-rotate
、loop-unswitch
和loop-unroll
,你可以亲自尝试。然后你会看到它们如何影响代码。
重新关联表达式
在本食谱中,你将了解重新关联表达式及其在优化中的帮助。
准备就绪
该 opt 工具需要安装才能使本食谱生效。
如何做到这一点…
-
为简单的重新关联转换编写测试用例:
$ cat testreassociate.ll define i32 @test(i32 %b, i32 %a) { %tmp.1 = add i32 %a, 1234 %tmp.2 = add i32 %b, %tmp.1 %tmp.4 = xor i32 %a, -1 ; (b+(a+1234))+~a -> b+1233 %tmp.5 = add i32 %tmp.2, %tmp.4 ret i32 %tmp.5 }
-
在这个测试用例上运行重新关联传递,以查看代码是如何修改的:
$ opt testreassociate.ll –reassociate –die –S define i32 @test(i32 %b, i32 %a) { %tmp.5 = add i32 %b, 1233 ret i32 %tmp.5 }
它是如何工作的 …
通过重新关联,我们是指应用代数性质,如结合性、交换性和分配性,来重新排列一个表达式,以便启用其他优化,例如常量折叠、LICM 等。
在前面的例子中,我们使用了逆性质来消除像 "X
+
~X"
->
"-1"
这样的模式,通过重新关联来实现。
测试用例的前三行给出了形式为 (b+(a+1234))+~a
的表达式。在这个表达式中,使用重新关联传递,我们将 a+~a
转换为 -1
。因此,在结果中,我们得到最终的返回值是 b+1234-1
=
b+1233
。
处理这种转换的代码位于 lib/Transforms/Scalar
下的 Reassociate.cpp
文件中。
如果你查看此文件,特别是代码段,你可以看到它检查操作数列表中是否有 a
和 ~a
:
if (!BinaryOperator::isNeg(TheOp) && !BinaryOperator::isNot(TheOp))
continue;
Value *X = nullptr;
…
…
else if (BinaryOperator::isNot(TheOp))
X = BinaryOperator::getNotArgument(TheOp);
unsigned FoundX = FindInOperandList(Ops, i, X);
以下代码负责在表达式中遇到此类值时处理和插入 -1
值:
if (BinaryOperator::isNot(TheOp)) {
Value *V = Constant::getAllOnesValue(X->getType());
Ops.insert(Ops.end(), ValueEntry(getRank(V), V));
e += 1;
}
向量化 IR
向量化是编译器的重要优化,我们可以向量化代码以一次执行多个数据集上的指令。如果后端架构支持向量寄存器,可以加载大量数据到这些向量寄存器中,并在寄存器上执行特殊的向量指令。
在 LLVM 中有两种类型的向量化——超词并行性(SLP)和循环向量化。循环向量化处理循环中的向量化机会,而 SLP 向量化处理基本块中的直线代码的向量化。在本食谱中,我们将看到直线代码是如何向量化。
准备就绪
SLP 向量化构建 IR 表达式的自下而上的树,并广泛比较树的节点,以查看它们是否相似,从而可以组合成向量。需要修改的文件是 lib/Transform/Vectorize/SLPVectorizer.cpp
。
我们将尝试向量化一段直线代码,例如 return
a[0]
+
a[1]
+
a[2]
+
a[3]
。
前一类代码的表达式树将是一个有点单边的树。我们将运行 DFS 来存储操作数和运算符。
前一类表达式的 IR 将看起来像这样:
define i32 @hadd(i32* %a) {
entry:
%0 = load i32* %a, align 4
%arrayidx1 = getelementptr inbounds i32* %a, i32 1
%1 = load i32* %arrayidx1, align 4
%add = add nsw i32 %0, %1
%arrayidx2 = getelementptr inbounds i32* %a, i32 2
%2 = load i32* %arrayidx2, align 4
%add3 = add nsw i32 %add, %2
%arrayidx4 = getelementptr inbounds i32* %a, i32 3
%3 = load i32* %arrayidx4, align 4
%add5 = add nsw i32 %add3, %3
ret i32 %add5
}
向量化模型遵循三个步骤:
-
检查是否可以向量化。
-
计算向量化代码相对于标量代码的盈利能力。
-
如果满足这两个条件,则向量化代码:
如何做到它...
-
打开
SLPVectorizer.cpp
文件。需要实现一个新函数,用于对 准备就绪 部分中显示的 IR 表达式的表达式树进行 DFS 遍历:bool matchFlatReduction(PHINode *Phi, BinaryOperator *B, const DataLayout *DL) { if (!B) return false; if (B->getType()->isVectorTy() || !B->getType()->isIntegerTy()) return false; ReductionOpcode = B->getOpcode(); ReducedValueOpcode = 0; ReduxWidth = MinVecRegSize / DL->getTypeAllocSizeInBits(B->getType()); ReductionRoot = B; ReductionPHI = Phi; if (ReduxWidth < 4) return false; if (ReductionOpcode != Instruction::Add) return false; SmallVector<BinaryOperator *, 32> Stack; ReductionOps.push_back(B); ReductionOpcode = B->getOpcode(); Stack.push_back(B); // Traversal of the tree. while (!Stack.empty()) { BinaryOperator *Bin = Stack.back(); if (Bin->getParent() != B->getParent()) return false; Value *Op0 = Bin->getOperand(0); Value *Op1 = Bin->getOperand(1); if (!Op0->hasOneUse() || !Op1->hasOneUse()) return false; BinaryOperator *Op0Bin = dyn_cast<BinaryOperator>(Op0); BinaryOperator *Op1Bin = dyn_cast<BinaryOperator>(Op1); Stack.pop_back(); // Do not handle case where both the operands are binary //operators if (Op0Bin && Op1Bin) return false; // Both the operands are not binary operator. if (!Op0Bin && !Op1Bin) { ReducedVals.push_back(Op1); ReducedVals.push_back(Op0); ReductionOps.push_back(Bin); continue; } // One of the Operand is binary operand, push that into stack // for further processing. Push the other non-binary operand //into ReducedVals. if (Op0Bin) { if (Op0Bin->getOpcode() != ReductionOpcode) return false; Stack.push_back(Op0Bin); ReducedVals.push_back(Op1); ReductionOps.push_back(Op0Bin); } if (Op1Bin) { if (Op1Bin->getOpcode() != ReductionOpcode) return false; Stack.push_back(Op1Bin); ReducedVals.push_back(Op0); ReductionOps.push_back(Op1Bin); } } SmallVector<Value *, 16> Temp; // Reverse the loads from a[3], a[2], a[1], a[0] // to a[0], a[1], a[2], a[3] for checking incremental // consecutiveness further ahead. while (!ReducedVals.empty()) Temp.push_back(ReducedVals.pop_back_val()); ReducedVals.clear(); for (unsigned i = 0, e = Temp.size(); i < e; ++i) ReducedVals.push_back(Temp[i]); return true; }
-
计算结果向量化 IR 的成本,并得出是否进行向量化是有利可图的结论。在
SLPVectorizer.cpp
文件中,向getReductionCost()
函数中添加以下行:int HAddCost = INT_MAX; // If horizontal addition pattern is identified, calculate cost. // Such horizontal additions can be modeled into combination of // shuffle sub-vectors and vector adds and one single extract element // from last resultant vector. // e.g. a[0]+a[1]+a[2]+a[3] can be modeled as // %1 = load <4 x> %0 // %2 = shuffle %1 <2, 3, undef, undef> // %3 = add <4 x> %1, %2 // %4 = shuffle %3 <1, undef, undef, undef> // %5 = add <4 x> %3, %4 // %6 = extractelement %5 <0> if (IsHAdd) { unsigned VecElem = VecTy->getVectorNumElements(); unsigned NumRedxLevel = Log2_32(VecElem); HAddCost = NumRedxLevel * (TTI->getArithmeticInstrCost(ReductionOpcode, VecTy) + TTI->getShuffleCost(TargetTransformInfo::SK_ExtractSubvector, VecTy, VecElem / 2, VecTy)) + TTI->getVectorInstrCost(Instruction::ExtractElement, VecTy, 0); }
-
在同一函数中,在计算
PairwiseRdxCost
和SplittingRdxCost
之后,将它们与HAddCost
进行比较:VecReduxCost = HAddCost < VecReduxCost ? HAddCost : VecReduxCost;
-
在
vectorizeChainsInBlock()
函数中,调用您刚刚定义的matchFlatReduction()
函数:// Try to vectorize horizontal reductions feeding into a return. if (ReturnInst *RI = dyn_cast<ReturnInst>(it)) if (RI->getNumOperands() != 0) if (BinaryOperator *BinOp = dyn_cast<BinaryOperator>(RI->getOperand(0))) { DEBUG(dbgs() << "SLP: Found a return to vectorize.\n"); HorizontalReduction HorRdx; IsReturn = true; if ((HorRdx.matchFlatReduction(nullptr, BinOp, DL) && HorRdx.tryToReduce(R, TTI)) || tryToVectorizePair(BinOp->getOperand(0), BinOp->getOperand(1), R)) { Changed = true; it = BB->begin(); e = BB->end(); continue; } }
-
定义两个全局标志以跟踪水平减少,该减少输入到返回中:
static bool IsReturn = false; static bool IsHAdd = false;
-
如果小树能够返回输入,则允许对它们进行向量化。将以下行添加到
isFullyVectorizableTinyTree()
函数中:if (VectorizableTree.size() == 1 && IsReturn && IsHAdd)return true;
它是如何工作的……
保存包含上述代码的文件后,编译 LLVM 项目,并在示例 IR 上运行 opt 工具,如下所示:
-
打开
example.ll
文件,并将以下 IR 粘贴到其中:define i32 @hadd(i32* %a) { entry: %0 = load i32* %a, align 4 %arrayidx1 = getelementptr inbounds i32* %a, i32 1 %1 = load i32* %arrayidx1, align 4 %add = add nsw i32 %0, %1 %arrayidx2 = getelementptr inbounds i32* %a, i32 2 %2 = load i32* %arrayidx2, align 4 %add3 = add nsw i32 %add, %2 %arrayidx4 = getelementptr inbounds i32* %a, i32 3 %3 = load i32* %arrayidx4, align 4 %add5 = add nsw i32 %add3, %3 ret i32 %add5 }
-
在
example.ll
上运行 opt 工具:$ opt -basicaa -slp-vectorizer -mtriple=aarch64-unknown-linux-gnu -mcpu=cortex-a57
输出将是向量化的代码,如下所示:
define i32 @hadd(i32* %a) { entry: %0 = bitcast i32* %a to <4 x i32>* %1 = load <4 x i32>* %0, align 4 %rdx.shuf = shufflevector <4 x i32> %1, <4 x i32> undef, <4 x i32> <i32 2, i32 3, i32 undef, i32 undef> %bin.rdx = add <4 x i32> %1, %rdx.shuf %rdx.shuf1 = shufflevector <4 x i32> %bin.rdx, <4 x i32> undef, <4 x i32> <i32 1, i32 undef, i32 undef, i32 undef> %bin.rdx2 = add <4 x i32> %bin.rdx, %rdx.shuf1 %2 = extractelement <4 x i32> %bin.rdx2, i32 0 ret i32 %2 }
观察到代码被向量化了。matchFlatReduction()
函数对表达式进行深度优先遍历,并将所有加载存储在 ReducedVals
中,而加法存储在 ReductionOps
中。之后,在 HAddCost
中计算水平向量化的成本,并与标量成本进行比较。结果证明这是有利的。因此,它将表达式向量化。这由 tryToReduce()
函数处理,该函数已经实现。
参考以下内容……
- 对于详细的向量化概念,请参阅 Ira Rosen、Dorit Nuzman 和 Ayal Zaks 撰写的论文 GCC 中的循环感知 SLP。
其他优化过程
在这个菜谱中,我们将查看一些更多的转换过程,这些过程更像是工具过程。我们将查看 strip-debug-symbols
过程和 prune-eh
过程。
准备工作……
必须安装 opt 工具。
如何操作……
-
编写一个测试用例来检查 strip-debug 过程,该过程从测试代码中删除调试符号:
$ cat teststripdebug.ll @x = common global i32 0 ; <i32*> [#uses=0] define void @foo() nounwind readnone optsize ssp { entry: tail call void @llvm.dbg.value(metadata i32 0, i64 0, metadata !5, metadata !{}), !dbg !10 ret void, !dbg !11 } declare void @llvm.dbg.value(metadata, i64, metadata, metadata) nounwind readnone !llvm.dbg.cu = !{!2} !llvm.module.flags = !{!13} !llvm.dbg.sp = !{!0} !llvm.dbg.lv.foo = !{!5} !llvm.dbg.gv = !{!8} !0 = !MDSubprogram(name: "foo", linkageName: "foo", line: 2, isLocal: false, isDefinition: true, virtualIndex: 6, isOptimized: true, file: !12, scope: !1, type: !3, function: void ()* @foo) !1 = !MDFile(filename: "b.c", directory: "/tmp") !2 = !MDCompileUnit(language: DW_LANG_C89, producer: "4.2.1 (Based on Apple Inc. build 5658) (LLVM build)", isOptimized: true, emissionKind: 0, file: !12, enums: !4, retainedTypes: !4) !3 = !MDSubroutineType(types: !4) !4 = !{null} !5 = !MDLocalVariable(tag: DW_TAG_auto_variable, name: "y", line: 3, scope: !6, file: !1, type: !7) !6 = distinct !MDLexicalBlock(line: 2, column: 0, file: !12, scope: !0) !7 = !MDBasicType(tag: DW_TAG_base_type, name: "int", size: 32, align: 32, encoding: DW_ATE_signed) !8 = !MDGlobalVariable(name: "x", line: 1, isLocal: false, isDefinition: true, scope: !1, file: !1, type: !7, variable: i32* @x) !9 = !{i32 0} !10 = !MDLocation(line: 3, scope: !6) !11 = !MDLocation(line: 4, scope: !6) !12 = !MDFile(filename: "b.c", directory: "/tmp") !13 = !{i32 1, !"Debug Info Version", i32 3}
-
通过将
–strip-debug
命令行选项传递给opt
工具来运行strip-debug-symbols
过程:$ opt -strip-debug teststripdebug.ll -S ; ModuleID = ' teststripdebug.ll' @x = common global i32 0 ; Function Attrs: nounwind optsize readnone ssp define void @foo() #0 { entry: ret void } attributes #0 = { nounwind optsize readnone ssp } !llvm.module.flags = !{!0} !0 = metadata !{i32 1, metadata !"Debug Info Version", i32 2}
-
编写一个测试用例来检查
prune-eh
过程:$ cat simpletest.ll declare void @nounwind() nounwind define internal void @foo() { call void @nounwind() ret void } define i32 @caller() { invoke void @foo( ) to label %Normal unwind label %Except Normal: ; preds = %0 ret i32 0 Except: ; preds = %0 landingpad { i8*, i32 } personality i32 (...)* @__gxx_personality_v0 catch i8* null ret i32 1 } declare i32 @__gxx_personality_v0(...)
-
通过将
–prune-eh
命令行选项传递给 opt 工具来运行该过程,以删除未使用的异常信息:$ opt -prune-eh -S simpletest.ll ; ModuleID = 'simpletest.ll' ; Function Attrs: nounwind declare void @nounwind() #0 ; Function Attrs: nounwind define internal void @foo() #0 { call void @nounwind() ret void } ; Function Attrs: nounwind define i32 @caller() #0 { call void @foo() br label %Normal Normal: ; preds = %0 ret i32 0 } declare i32 @__gxx_personality_v0(...) attributes #0 = { nounwind }
它是如何工作的……
在第一种情况下,当我们运行 strip-debug
过程时,它会从代码中删除调试信息,我们可以得到紧凑的代码。此过程仅在寻找紧凑代码时必须使用,因为它可以删除虚拟寄存器的名称以及内部全局变量和函数的符号,从而使源代码更难以阅读,并使代码逆向工程变得困难。
处理此转换的代码部分位于 llvm/lib/Transforms/IPO/StripSymbols.cpp
文件中,其中 StripDeadDebugInfo::runOnModule
函数负责删除调试信息。
第二个测试是使用prune-eh
过程来移除未使用的异常信息,该过程实现了一个跨过程过程。它会遍历调用图,只有当被调用者不能抛出异常时,才将调用指令转换为调用指令,并且如果函数不能抛出异常,则将其标记为nounwind
。
参见
- 参考以下链接了解其他转换过程:
llvm.org/docs/Passes.html#transform-passes
第六章:目标无关代码生成器
在本章中,我们将涵盖以下内容:
-
LLVM IR 指令的生命周期
-
使用 GraphViz 可视化 LLVM IR CFG
-
使用 TableGen 描述目标
-
定义指令集
-
添加机器代码描述符
-
实现 MachineInstrBuilder 类
-
实现 MachineBasicBlock 类
-
实现 MachineFunction 类
-
编写指令选择器
-
合法化 SelectionDAG
-
优化 SelectionDAG
-
从 DAG 中选择指令
-
在 SelectionDAG 中调度指令
简介
在优化 LLVM IR 之后,需要将其转换为执行所需的机器指令。机器无关的代码生成器接口提供了一个抽象层,帮助将 IR 转换为机器指令。在这个阶段,IR 转换为 SelectionDAG(DAG 代表 有向无环图)。各种阶段在 SelectionDAG 的节点上工作。本章描述了目标无关代码生成中的重要阶段。
LLVM IR 指令的生命周期
在前面的章节中,我们看到了高级语言指令、语句、逻辑块、函数调用、循环等如何被转换成 LLVM IR。然后,各种优化过程处理 IR 以使其更优化。生成的 IR 是 SSA 形式,在抽象格式上几乎独立于任何高级或低级语言约束,这有助于优化过程在其上运行。可能存在一些特定于目标的优化,它们在 IR 转换为机器指令时发生。
在我们得到最优的 LLVM IR 之后,下一个阶段是将它转换为特定于目标机的指令。LLVM 使用 SelectionDAG 方法将 IR 转换为机器指令。线性 IR 转换为 SelectionDAG,一个表示指令为节点的 DAG。SDAG 然后经过各种阶段:
-
SelectionDAG 是由 LLVM IR 创建的
-
合法化 SDAG 节点
-
DAG 合并优化
-
从目标指令中选择指令
-
调度和生成机器指令
-
寄存器分配——SSA 摧毁、寄存器分配和寄存器溢出
-
生成代码
所有的前一个阶段在 LLVM 中都是模块化的。
C 代码到 LLVM IR
第一步是将前端语言示例转换为 LLVM IR。让我们举一个例子:
int test (int a, int b, int c) {
return c/(a+b);
}
其 LLVM IR 将如下所示:
define i32 @test(i32 %a, i32 %b, i32 %c) {
%add = add nsw i32 %a, %b
%div = sdiv i32 %add, %c
return i32 %div
}
IR 优化
然后 IR 经过各种优化过程,如前几章所述。在转换阶段,IR 经过 InstCombiner::visitSDiv()
函数在 InstCombine
过程中。在该函数中,它还经过 SimplifySDivInst()
函数,并尝试检查是否存在进一步简化指令的机会。
LLVM IR 到 SelectionDAG
在 IR 转换和优化完成后,LLVM IR 指令通过一个选择 DAG 节点阶段。选择 DAG 节点由SelectionDAGBuilder
类创建。SelectionDAGISel
类中的SelectionDAGBuilder::visit()
函数调用遍历每个 IR 指令以创建一个SDAGNode
节点。处理SDiv
指令的方法是SelectionDAGBuilder::visitSDiv
。它从 DAG 请求一个新的SDNode
节点,具有ISD::SDIV
操作码,然后成为 DAG 中的一个节点。
选择 DAG 合法化
创建的SelectionDAG
节点可能不被目标架构支持。在 Selection DAG 的初始阶段,这些不支持的节点被称为非法。在实际从 DAG 节点发出机器指令之前,这些节点会经历几个其他转换,合法化是其中重要阶段之一。
SDNode
的合法化涉及类型和操作的合法化。目标特定的信息通过一个名为TargetLowering
的接口传递给目标无关的算法。这个接口由目标实现,并描述了 LLVM IR 指令应该如何降低到合法的SelectionDAG
操作。例如,x86 降低是通过X86TargetLowering
接口实现的。setOperationAction()
函数指定 ISD 节点是否需要通过操作合法化进行展开或定制。当SelectionDAGLegalize::LegalizeOp
看到展开标志时,它将SDNode
节点替换为setOperationAction()
调用中指定的参数。
从目标无关的 DAG 转换为机器 DAG
现在我们已经合法化了指令,SDNode
应该转换为MachineSDNode
。机器指令在目标描述.td
文件中以通用的基于表的格式描述。使用tablegen
,这些文件随后被转换为具有寄存器/指令枚举的.inc
文件,以便在 C++代码中引用。指令可以通过自动选择器SelectCode
选择,或者可以通过在SelectionDAGISel
类中编写定制的Select
函数来专门处理。在此步骤中创建的 DAG 节点是一个MachineSDNode
节点,它是SDNode
的子类,它包含构建实际机器指令所需的信息,但仍然以 DAG 节点形式存在。
调度说明
一台机器执行一系列线性指令。到目前为止,我们仍然有以 DAG(有向无环图)形式存在的机器指令。要将 DAG 转换为线性指令集,可以通过对 DAG 进行拓扑排序来得到线性顺序的指令。然而,生成的线性指令集可能不会产生最优化代码,并且由于指令之间的依赖性、寄存器压力和流水线停滞问题,可能会导致执行延迟。因此,出现了指令调度的概念。由于每个目标都有自己的寄存器集和指令的定制流水线,每个目标都有自己的调度钩子和计算启发式算法来生成优化、更快的代码。在计算最佳指令排列方式后,调度器将机器指令输出到机器基本块中,并最终销毁 DAG。
寄存器分配
在发射机器指令之后,分配的寄存器是虚拟寄存器。实际上,可以分配无限数量的虚拟寄存器,但实际的目标具有有限的寄存器数量。这些有限的寄存器需要有效地分配。如果没有这样做,一些寄存器必须溢出到内存中,这可能会导致冗余的加载/存储操作。这也会导致 CPU 周期的浪费,从而减慢执行速度并增加内存占用。
存在着各种寄存器分配算法。在分配寄存器时进行的一个重要分析是变量的存活性和存活区间分析。如果两个变量在同一个区间内存活(即存在区间干扰),则它们不能分配到同一个寄存器。通过分析存活性创建一个干扰图,可以使用图着色算法来分配寄存器。然而,这个算法的运行时间是二次的。因此,它可能会导致编译时间更长。
LLVM 在寄存器分配上采用贪婪算法,首先为具有大存活范围的变量分配寄存器。小范围适合填充可用的寄存器间隙,从而减少溢出重量。溢出是一种由于没有可分配的寄存器而发生的加载/存储操作。溢出重量是涉及溢出操作的成本。有时,为了将变量放入寄存器,还会进行存活范围分割。
注意,在寄存器分配之前,指令处于 SSA(单赋值)形式。然而,由于可用的寄存器数量有限,SSA 形式在现实世界中无法存在。在某些类型的架构中,某些指令需要固定的寄存器。
代码发射
现在原始的高级代码已经被翻译成机器指令,下一步是生成代码。LLVM 以两种方式完成此操作;第一种是 JIT,它直接将代码输出到内存。第二种方式是通过使用 MC 框架为所有后端目标生成汇编和目标文件。《LLVMTargetMachine::addPassesToEmitFile》函数负责定义生成对象文件所需的操作序列。实际的 MI 到 MCInst 转换是在 AsmPrinter
接口的 EmitInstruction
函数中完成的。静态编译器工具 llc 为目标生成汇编指令。通过实现 MCStreamer
接口来完成对象文件(或汇编代码)的生成。
使用 GraphViz 可视化 LLVM IR CFG
使用 GraphViz 工具可以可视化 LLVM IR 控制流图。它以可视化的方式展示了形成的节点以及代码流如何在生成的 IR 中遵循。由于 LLVM 中的重要数据结构是图,这可以在编写自定义传递或研究 IR 模式行为时,成为一种非常有用的理解 IR 流的方式。
准备工作
-
要在 Ubuntu 上安装
graphviz
,首先添加其ppa
仓库:$ sudo apt-add-repository ppa:dperry/ppa-graphviz-test
-
更新软件包仓库:
$ sudo apt-get update
-
安装
graphviz
:$ sudo apt-get install graphviz
注意
如果遇到
graphviz : Depends: libgraphviz4 (>= 2.18) but it is not going to be installed
错误,请运行以下命令:$ sudo apt-get remove libcdt4 $ sudo apt-get remove libpathplan4
然后使用以下命令再次安装
graphviz
:$ sudo apt-get install graphviz
如何做到这一点...
-
一旦 IR 转换为 DAG,它可以在不同的阶段进行查看。创建一个包含以下代码的 test.ll 文件:
$ 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 }
-
要在构建后但在第一次优化传递之前显示 DAG,请输入以下命令:
$ llc -view-dag-combine1-dags test.ll
以下图显示了第一次优化传递之前的 DAG:
-
要显示合法化之前的 DAG,请运行以下命令:
$ llc -view-legalize-dags test.ll
下面是一个显示合法化阶段之前 DAG 的图:
-
要显示第二次优化传递之前的 DAG,请运行以下命令:
$ llc -view-dag-combine2-dags test.ll
以下图显示了第二次优化传递之前的 DAG:
-
要显示选择阶段之前的 DAG,请输入以下命令:
$ llc -view-isel-dags test.ll
下面是一个显示选择阶段之前 DAG 的图:
-
要显示调度之前的 DAG,请运行以下命令:
$ llc -view-sched-dags test.ll
以下图显示了调度阶段之前的 DAG:
-
要显示调度器的依赖图,请运行以下命令:
$ llc -view-sunit-dags test.ll
此图显示了调度器的依赖图:
注意合法化阶段前后 DAG 的差异。sdiv
节点已被转换为 sdivrem
节点。x86 目标不支持 sdiv
节点,但支持 sdivrem
指令。从某种意义上说,sdiv
指令对于 x86 目标是不合法的。合法化阶段将其转换为 x86 目标支持的 sdivrem
指令。
还要注意指令选择 (ISel) 阶段前后 DAG 的差异。像 Load
这样的目标机无关指令被转换为 MOV32rm
机器代码(这意味着,将 32 位数据从内存移动到寄存器)。ISel 阶段是一个重要的阶段,将在后面的菜谱中描述。
观察 DAG 的调度单元。每个单元都与其他单元相连接,这显示了它们之间的依赖关系。这些依赖信息对于决定调度算法非常重要。在前面的例子中,调度单元 0 (SU0) 依赖于调度单元 1 (SU1)。因此,SU0 中的指令不能在 SU1 中的指令之前调度。SU1 依赖于 SU2,同样 SU2 也依赖于 SU3。
参见
- 关于如何在调试模式下查看图的更多详细信息,请访问
llvm.org/docs/ProgrammersManual.html#viewing-graphs-while-debugging-code
使用 TableGen 描述目标
目标架构可以用现有的寄存器、指令集等来描述。手动描述每一个都是一项繁琐的任务。TableGen
是一个用于后端开发者的工具,它使用声明性语言 *.td
来描述他们的目标机器。*.td
文件将被转换为枚举、DAG 模式匹配函数、指令编码/解码函数等,这些可以在其他 C++ 文件中用于编码。
为了在目标描述的 .td
文件中定义寄存器和寄存器集,tablegen
将目标 .td
文件转换为 .inc
文件,这些文件将在我们的 .cpp
文件中以 #include
语法引用寄存器。
准备工作
假设示例目标机器有四个寄存器,r0-r3
;一个栈寄存器,sp
;以及一个链接寄存器,lr
。这些可以在 SAMPLERegisterInfo.td
文件中指定。TableGen
提供了 Register
类,它可以扩展以指定寄存器。
如何操作
-
在
lib/Target
目录下创建一个名为SAMPLE
的新文件夹:$ mkdir llvm_root_directory/lib/Target/SAMPLE
-
在新的
SAMPLE
文件夹中创建一个名为SAMPLERegisterInfo.td
的新文件:$ cd llvm_root_directory/lib/Target/SAMPLE $ vi SAMPLERegisterInfo.td
-
定义硬件编码、命名空间、寄存器和寄存器类:
class SAMPLEReg<bits<16> Enc, string n> : Register<n> { let HWEncoding = Enc; let Namespace = "SAMPLE"; } foreach i = 0-3 in { def R#i : R<i, "r"#i >; } def SP : SAMPLEReg<13, "sp">; def LR : SAMPLEReg<14, "lr">; def GRRegs : RegisterClass<"SAMPLE", [i32], 32, (add R0, R1, R2, R3, SP)>;
它是如何工作的
TableGen
处理这个 .td
文件以生成 .inc
文件,这些文件以枚举的形式表示寄存器,可以在 .cpp
文件中使用。当我们构建 LLVM 项目时,将生成这些 .inc
文件。
参见
- 要获取关于如何为更高级的架构(如 x86)定义寄存器的详细信息,请参考位于
llvm_source_code/lib/Target/X86/
的X86RegisterInfo.td
文件。
定义指令集
架构的指令集根据架构中存在的各种特性而有所不同。这个配方演示了如何为目标架构定义指令集。
准备工作
在指令目标描述文件中定义了三件事:操作数、汇编字符串和指令模式。规范包含一个定义或输出列表和一个使用或输入列表。可以有不同类型的操作数类,如寄存器类,以及立即数或更复杂的register + imm
操作数。
在这里,演示了一个简单的加法指令定义。它接受两个寄存器作为输入,一个寄存器作为输出。
如何操作…
-
在
lib/Target/SAMPLE
文件夹中创建一个名为SAMPLEInstrInfo.td
的新文件:$ vi SAMPLEInstrInfo.td
-
指定两个寄存器操作数之间加法指令的操作数、汇编字符串和指令模式:
def ADDrr : InstSAMPLE<(outs GRRegs:$dst), (ins GRRegs:$src1, GRRegs:$src2), "add $dst, $src1, $src2", [(set i32:$dst, (add i32:$src1, i32:$src2))]>;
它是如何工作的…
add
寄存器指令指定$dst
作为结果操作数,它属于通用寄存器类型类;$src1
和$src2
输入作为两个输入操作数,它们也属于通用寄存器类;指令汇编字符串为add $dst, $src1, $src2
,它是 32 位整型。
因此,将生成两个寄存器之间加法的汇编,如下所示:
add r0, r0, r1
这告诉我们将r0
和r1
寄存器的内容相加,并将结果存储在r0
寄存器中。
参见
-
要获取关于高级架构(如 x86)各种类型指令集的更详细信息,请参考位于
lib/Target/X86/
的X86InstrInfo.td
文件。 -
详细介绍了如何定义特定目标的相关信息将在第八章中介绍,编写 LLVM 后端。一些概念可能会重复,因为前面的配方只是简要描述,以了解目标架构描述并预览即将到来的配方。
添加机器代码描述符
LLVM IR 有函数,这些函数包含基本块。基本块反过来又包含指令。下一步合乎逻辑的操作是将这些 IR 抽象块转换为特定机器的块。LLVM 代码被转换成由MachineFunction
、MachineBasicBlock
和MachineInstr
实例组成的特定机器表示形式。这种表示形式包含最抽象形式的指令——即具有操作码和一系列操作数。
如何完成…
现在必须将 LLVM IR 指令表示为机器指令。机器指令是 MachineInstr
类的实例。这个类是一种极端抽象的方式来表示机器指令。特别是,它只跟踪一个操作码数字和一组操作数。操作码数字是一个只有对特定后端有意义的简单无符号整数。
让我们看看在 MachineInstr.cpp
文件中定义的一些重要函数:
MachineInstr
构造函数:
MachineInstr::MachineInstr(MachineFunction &MF, const MCInstrDesc &tid, const DebugLoc dl, bool NoImp)
: MCID(&tid), Parent(nullptr), Operands(nullptr), NumOperands(0),
Flags(0), AsmPrinterFlags(0),
NumMemRefs(0), MemRefs(nullptr), debugLoc(dl) {
// Reserve space for the expected number of operands.
if (unsigned NumOps = MCID->getNumOperands() +
MCID->getNumImplicitDefs() + MCID->getNumImplicitUses()) {
CapOperands = OperandCapacity::get(NumOps);
Operands = MF.allocateOperandArray(CapOperands);
}
if (!NoImp)
addImplicitDefUseOperands(MF);
}
此构造函数创建一个 MachineInstr
类对象并添加隐式操作数。它为 MCInstrDesc
类指定的操作数数量预留空间。
其中一个重要的函数是 addOperand
。它将指定的操作数添加到指令中。如果是隐式操作数,它将被添加到操作数列表的末尾。如果是显式操作数,它将被添加到显式操作数列表的末尾,如下所示:
void MachineInstr::addOperand(MachineFunction &MF, const MachineOperand &Op) {
assert(MCID && "Cannot add operands before providing an instr descriptor");
if (&Op >= Operands && &Op < Operands + NumOperands) {
MachineOperand CopyOp(Op);
return addOperand(MF, CopyOp);
}
unsigned OpNo = getNumOperands();
bool isImpReg = Op.isReg() && Op.isImplicit();
if (!isImpReg && !isInlineAsm()) {
while (OpNo && Operands[OpNo-1].isReg() && Operands[OpNo-1].isImplicit()) {
--OpNo;
assert(!Operands[OpNo].isTied() && "Cannot move tied operands");
}
}
#ifndef NDEBUG
bool isMetaDataOp = Op.getType() == MachineOperand::MO_Metadata;
assert((isImpReg || Op.isRegMask() || MCID->isVariadic() ||
OpNo < MCID->getNumOperands() || isMetaDataOp) &&
"Trying to add an operand to a machine instr that is already done!");
#endif
MachineRegisterInfo *MRI = getRegInfo();
OperandCapacity OldCap = CapOperands;
MachineOperand *OldOperands = Operands;
if (!OldOperands || OldCap.getSize() == getNumOperands()) {
CapOperands = OldOperands ? OldCap.getNext() : OldCap.get(1);
Operands = MF.allocateOperandArray(CapOperands);
if (OpNo)
moveOperands(Operands, OldOperands, OpNo, MRI);
}
if (OpNo != NumOperands)
moveOperands(Operands + OpNo + 1, OldOperands + OpNo, NumOperands - OpNo,
MRI);
++NumOperands;
if (OldOperands != Operands && OldOperands)
MF.deallocateOperandArray(OldCap, OldOperands);
MachineOperand *NewMO = new (Operands + OpNo) MachineOperand(Op);
NewMO->ParentMI = this;
if (NewMO->isReg()) {
NewMO->Contents.Reg.Prev = nullptr;
NewMO->TiedTo = 0;
if (MRI)
MRI->addRegOperandToUseList(NewMO);
if (!isImpReg) {
if (NewMO->isUse()) {
int DefIdx = MCID->getOperandConstraint(OpNo, MCOI::TIED_TO);
if (DefIdx != -1)
tieOperands(DefIdx, OpNo);
}
if (MCID->getOperandConstraint(OpNo, MCOI::EARLY_CLOBBER) != -1)
NewMO->setIsEarlyClobber(true);
}
}
}
目标架构也有一些内存操作数。为了添加这些内存操作数,定义了一个名为 addMemOperands()
的函数:
void MachineInstr::addMemOperand(MachineFunction &MF,
MachineMemOperand *MO) {
mmo_iterator OldMemRefs = MemRefs;
unsigned OldNumMemRefs = NumMemRefs;
unsigned NewNum = NumMemRefs + 1;
mmo_iterator NewMemRefs = MF.allocateMemRefsArray(NewNum);
std::copy(OldMemRefs, OldMemRefs + OldNumMemRefs, NewMemRefs);
NewMemRefs[NewNum - 1] = MO;
setMemRefs(NewMemRefs, NewMemRefs + NewNum);
}
setMemRefs()
函数是设置 MachineInstr
MemRefs
列表的主要方法。
它是如何工作的…
MachineInstr
类有一个 MCID 成员,用于描述指令的 MCInstrDesc
类型,一个 uint8_t
标志成员,一个内存引用成员 (mmo_iterator
MemRefs
),以及一个 std::vector<MachineOperand>
的操作数成员。在方法方面,MachineInstr
类提供了以下功能:
-
一组基本的
get**
和set**
函数用于信息查询,例如,getOpcode()
,getNumOperands()
等 -
与捆绑相关的操作,例如,
isInsideBundle()
-
检查指令是否具有某些属性,例如,
isVariadic()
,isReturn()
,isCall()
等 -
机器指令操作,例如,
eraseFromParent()
-
与寄存器相关的操作,例如,
ubstituteRegister()
,addRegisterKilled()
等 -
创建机器指令的方法,例如,
addOperand()
,setDesc()
等
注意,尽管 MachineInstr
类提供了创建机器指令的方法,但基于 MachineInstrBuilder
类的专用函数 BuildMI()
更方便。
实现 MachineInstrBuilder
类
MachineInstrBuilder
类公开了一个名为 BuildMI()
的函数。此函数用于构建机器指令。
如何做到这一点…
通过使用位于 include/llvm/CodeGen/MachineInstrBuilder.h
文件中的 BuildMI
函数创建机器指令。BuildMI
函数使得构建任意机器指令变得容易。
例如,您可以在代码片段中使用 BuildMI
来实现以下目的:
-
要创建一个
DestReg = mov 42
(在 x86 汇编中表示为mov DestReg, 42
)指令:MachineInstr *MI = BuildMI(X86::MOV32ri, 1, DestReg).addImm(42);
-
要创建相同的指令,但将其插入到基本块的末尾:
MachineBasicBlock &MBB = BuildMI(MBB, X86::MOV32ri, 1, DestReg).addImm(42);
-
要创建相同的指令,但将其插入到指定的迭代器点之前:
MachineBasicBlock::iterator MBBI = BuildMI(MBB, MBBI, X86::MOV32ri, 1, DestReg).addImm(42)
-
要创建一个循环分支指令:
BuildMI(MBB, X86::JNE, 1).addMBB(&MBB);
它是如何工作的...
BuildMI()
函数用于指定机器指令将采用的操作数数量,这有助于有效的内存分配。它还必须指定操作数使用值还是定义。
实现 MachineBasicBlock
类
与 LLVM IR 中的基本块类似,MachineBasicBlock
类按顺序包含一组机器指令。大多数情况下,MachineBasicBlock
类映射到单个 LLVM IR 基本块。然而,也可能存在多个 MachineBasicBlock
类映射到单个 LLVM IR 基本块的情况。MachineBasicBlock
类有一个名为 getBasicBlock()
的方法,它返回映射到的 IR 基本块。
如何做到这一点...
以下步骤展示了如何添加机器基本块:
-
getBasicBlock
方法将仅返回当前的基本块:const BasicBlock *getBasicBlock() const { return BB; }
-
基本块有后继和前驱基本块。为了跟踪这些,定义了如下向量:
std::vector<MachineBasicBlock *> Predecessors; std::vector<MachineBasicBlock *> Successors;
-
应添加一个
insert
函数,用于将机器指令插入到基本块中:MachineBasicBlock::insert(instr_iterator I, MachineInstr *MI) { assert(!MI->isBundledWithPred() && !MI->isBundledWithSucc() && "Cannot insert instruction with bundle flags"); if (I != instr_end() && I->isBundledWithPred()) { MI->setFlag(MachineInstr::BundledPred); MI->setFlag(MachineInstr::BundledSucc); } return Insts.insert(I, MI); }
-
一个名为
SplitCriticalEdge()
的函数将从这个块到给定后继块的临界边拆分出来,并返回新创建的块,如果拆分不可行则返回 null。此函数更新LiveVariables
、MachineDominatorTree
和MachineLoopInfo
类:MachineBasicBlock * MachineBasicBlock::SplitCriticalEdge(MachineBasicBlock *Succ, Pass *P) { … … … }
注意
上述代码的完整实现位于 lib/CodeGen/
目录下的 MachineBasicBlock.cpp
文件中。
它是如何工作的...
如前所述,不同类别的几个代表性函数构成了 MachineBasicBlock
类的接口定义。MachineBasicBlock
类保持一个包含机器指令的列表,例如 typedef ilist<MachineInstr>
指令、Insts
指令和原始的 LLVM BB(基本块)。它还提供了如下目的的方法:
-
BB 信息查询(例如,
getBasicBlock()
和setHasAddressTaken()
) -
BB 级别操作(例如,
moveBefore()
、moveAfter()
、addSuccessor()
) -
指令级操作(例如,
push_back()
、insertAfter()
等)
参见
- 要查看
MachineBasicBlock
类的详细实现,请查阅位于lib/CodeGen/
的MachineBasicBlock.cpp
文件。
实现 MachineFunction
类
与 LLVM IR 的 FunctionBlock
类类似,MachineFunction
类包含一系列 MachineBasicBlocks
类。这些 MachineFunction
类映射到指令选择器输入的 LLVM IR 函数。除了基本块列表外,MachineFunction
类还包含 MachineConstantPool
、MachineFrameInfo
、MachineFunctionInfo
和 MachineRegisterInfo
类。
如何做到这一点...
许多函数定义在 MachineFunction
类中,该类执行特定任务。还有许多类成员对象保持信息,如下所示:
-
RegInfo
跟踪函数中每个正在使用的寄存器的信息:MachineRegisterInfo *RegInfo;
-
MachineFrameInfo
跟踪堆栈上分配的对象:MachineFrameInfo *FrameInfo;
-
ConstantPool
跟踪已溢出到内存中的常量:MachineConstantPool *ConstantPool;
-
JumpTableInfo
跟踪跳转表的 switch 指令:MachineJumpTableInfo *JumpTableInfo;
-
函数中机器基本块的列表:
typedef ilist<MachineBasicBlock> BasicBlockListType; BasicBlockListType BasicBlocks;
-
getFunction
函数返回当前机器代码表示的 LLVM 函数:const Function *getFunction() const { return Fn; }
-
CreateMachineInstr
分配一个新的MachineInstr
类:MachineInstr *CreateMachineInstr(const MCInstrDesc &MCID, DebugLoc DL, bool NoImp = false);
它是如何工作的…
MachineFunction
类主要包含一个MachineBasicBlock
对象列表(typedef ilist<MachineBasicBlock> BasicBlockListType; BasicBlockListType BasicBlocks;
),并定义了各种用于检索机器函数信息以及操作基本块成员中对象的方法。一个需要注意的重要点是,MachineFunction
类维护函数中所有基本块的控制流图(CFG)。CFG 中的控制流信息对于许多优化和分析至关重要。因此,了解MachineFunction
对象和相应的 CFG 是如何构建的非常重要。
参见
MachineFunction
类的详细实现可以在位于lib/Codegen/
目录下的MachineFunction.cpp
文件中找到。
编写指令选择器
LLVM 使用SelectionDAG
表示法来表示 LLVM IR 的低级数据依赖 DAG,用于指令选择。可以对SelectionDAG
表示法应用各种简化和特定于目标的优化。这种表示法是目标无关的。它是一个重要、简单且强大的表示法,用于实现 IR 降低到目标指令。
如何做到这一点…
以下代码展示了SelectionDAG
类的简要框架,其数据成员以及用于从该类设置/检索有用信息的各种方法。SelectionDAG
类定义如下:
class SelectionDAG {
const TargetMachine &TM;
const TargetLowering &TLI;
const TargetSelectionDAGInfo &TSI;
MachineFunction *MF;
LLVMContext *Context;
CodeGenOpt::Level OptLevel;
SDNode EntryNode;
// Root - The root of the entire DAG.
SDValue Root;
// AllNodes - A linked list of nodes in the current DAG.
ilist<SDNode> AllNodes;
// NodeAllocatorType - The AllocatorType for allocating SDNodes. We use
typedef RecyclingAllocator<BumpPtrAllocator, SDNode, sizeof(LargestSDNode),
AlignOf<MostAlignedSDNode>::Alignment>
NodeAllocatorType;
BumpPtrAllocator OperandAllocator;
BumpPtrAllocator Allocator;
SDNodeOrdering *Ordering;
public:
struct DAGUpdateListener {
DAGUpdateListener *const Next;
SelectionDAG &DAG;
explicit DAGUpdateListener(SelectionDAG &D)
: Next(D.UpdateListeners), DAG(D) {
DAG.UpdateListeners = this;
}
private:
friend struct DAGUpdateListener;
DAGUpdateListener *UpdateListeners;
void init(MachineFunction &mf);
// Function to set root node of SelectionDAG
const SDValue &setRoot(SDValue N) {
assert((!N.getNode() || N.getValueType() == MVT::Other) &&
"DAG root value is not a chain!");
if (N.getNode())
checkForCycles(N.getNode());
Root = N;
if (N.getNode())
checkForCycles(this);
return Root;
}
void Combine(CombineLevel Level, AliasAnalysis &AA,
CodeGenOpt::Level OptLevel);
SDValue getConstant(uint64_t Val, EVT VT, bool isTarget = false);
SDValue getConstantFP(double Val, EVT VT, bool isTarget = false);
SDValue getGlobalAddress(const GlobalValue *GV, DebugLoc DL, EVT VT, int64_t offset = 0, bool isTargetGA = false,
unsigned char TargetFlags = 0);
SDValue getFrameIndex(int FI, EVT VT, bool isTarget = false);
SDValue getTargetIndex(int Index, EVT VT, int64_t Offset = 0,
unsigned char TargetFlags = 0);
// Function to return Basic Block corresponding to this MachineBasicBlock
SDValue getBasicBlock(MachineBasicBlock *MBB);
SDValue getBasicBlock(MachineBasicBlock *MBB, DebugLoc dl);
SDValue getExternalSymbol(const char *Sym, EVT VT);
SDValue getExternalSymbol(const char *Sym, DebugLoc dl, EVT VT);
SDValue getTargetExternalSymbol(const char *Sym, EVT VT,
unsigned char TargetFlags = 0);
// Return the type of the value this SelectionDAG node corresponds // to
SDValue getValueType(EVT);
SDValue getRegister(unsigned Reg, EVT VT);
SDValue getRegisterMask(const uint32_t *RegMask);
SDValue getEHLabel(DebugLoc dl, SDValue Root, MCSymbol *Label);
SDValue getBlockAddress(const BlockAddress *BA, EVT VT,
int64_t Offset = 0, bool isTarget = false,
unsigned char TargetFlags = 0);
SDValue getSExtOrTrunc(SDValue Op, DebugLoc DL, EVT VT);
SDValue getZExtOrTrunc(SDValue Op, DebugLoc DL, EVT VT);
SDValue getZeroExtendInReg(SDValue Op, DebugLoc DL, EVT SrcTy);
SDValue getNOT(DebugLoc DL, SDValue Val, EVT VT);
// Function to get SelectionDAG node.
SDValue getNode(unsigned Opcode, DebugLoc DL, EVT VT);
SDValue getNode(unsigned Opcode, DebugLoc DL, EVT VT, SDValue N);
SDValue getNode(unsigned Opcode, DebugLoc DL, EVT VT, SDValue N1, SDValue N2);
SDValue getNode(unsigned Opcode, DebugLoc DL, EVT VT,
SDValue N1, SDValue N2, SDValue N3);
SDValue getMemcpy(SDValue Chain, DebugLoc dl, SDValue Dst, SDValue Src,SDValue Size, unsigned Align, bool isVol, bool AlwaysInline,
MachinePointerInfo DstPtrInfo,MachinePointerInfo SrcPtrInfo);
SDValue getAtomic(unsigned Opcode, DebugLoc dl, EVT MemVT, SDValue Chain,
SDValue Ptr, SDValue Cmp, SDValue Swp,
MachinePointerInfo PtrInfo, unsigned Alignment,
AtomicOrdering Ordering,
SynchronizationScope SynchScope);
SDNode *UpdateNodeOperands(SDNode *N, SDValue Op);
SDNode *UpdateNodeOperands(SDNode *N, SDValue Op1, SDValue Op2);
SDNode *UpdateNodeOperands(SDNode *N, SDValue Op1, SDValue Op2,
SDValue Op3);
SDNode *SelectNodeTo(SDNode *N, unsigned TargetOpc, EVT VT);
SDNode *SelectNodeTo(SDNode *N, unsigned TargetOpc, EVT VT, SDValue Op1);
SDNode *SelectNodeTo(SDNode *N, unsigned TargetOpc, EVT VT,
SDValue Op1, SDValue Op2);
MachineSDNode *getMachineNode(unsigned Opcode, DebugLoc dl, EVT VT);
MachineSDNode *getMachineNode(unsigned Opcode, DebugLoc dl, EVT VT,
SDValue Op1);
MachineSDNode *getMachineNode(unsigned Opcode, DebugLoc dl, EVT VT,
SDValue Op1, SDValue Op2);
void ReplaceAllUsesWith(SDValue From, SDValue Op);
void ReplaceAllUsesWith(SDNode *From, SDNode *To);
void ReplaceAllUsesWith(SDNode *From, const SDValue *To);
bool isBaseWithConstantOffset(SDValue Op) const;
bool isKnownNeverNaN(SDValue Op) const;
bool isKnownNeverZero(SDValue Op) const;
bool isEqualTo(SDValue A, SDValue B) const;
SDValue UnrollVectorOp(SDNode *N, unsigned ResNE = 0);
bool isConsecutiveLoad(LoadSDNode *LD, LoadSDNode *Base,
unsigned Bytes, int Dist) const;
unsigned InferPtrAlignment(SDValue Ptr) const;
private:
bool RemoveNodeFromCSEMaps(SDNode *N);
void AddModifiedNodeToCSEMaps(SDNode *N);
SDNode *FindModifiedNodeSlot(SDNode *N, SDValue Op, void *&InsertPos);
SDNode *FindModifiedNodeSlot(SDNode *N, SDValue Op1, SDValue Op2,
void *&InsertPos);
SDNode *FindModifiedNodeSlot(SDNode *N, const SDValue *Ops, unsigned NumOps,void *&InsertPos);
SDNode *UpdadeDebugLocOnMergedSDNode(SDNode *N, DebugLoc loc);
void DeleteNodeNotInCSEMaps(SDNode *N);
void DeallocateNode(SDNode *N);
unsigned getEVTAlignment(EVT MemoryVT) const;
void allnodes_clear();
std::vector<SDVTList> VTList;
std::vector<CondCodeSDNode*> CondCodeNodes;
std::vector<SDNode*> ValueTypeNodes;
std::map<EVT, SDNode*, EVT::compareRawBits> ExtendedValueTypeNodes;
StringMap<SDNode*> ExternalSymbols;
std::map<std::pair<std::string, unsigned char>,SDNode*> TargetExternalSymbols;
};
它是如何工作的…
从前面的代码中可以看出,SelectionDAG
类提供了许多目标无关的方法来创建各种类型的SDNode
,并从SelectionDAG
图中的节点检索/计算有用信息。SelectionDAG
类还提供了更新和替换方法。这些方法中的大多数都在SelectionDAG.cpp
文件中定义。请注意,SelectionDAG
图及其节点类型SDNode
的设计能够存储目标无关和目标特定的信息。例如,SDNode
类中的isTargetOpcode()
和isMachineOpcode()
方法可以用来确定操作码是否是目标操作码或机器操作码(目标无关)。这是因为相同的类类型NodeType
被用来表示真实目标的操作码和机器指令的操作码,但范围是分开的。
Legalizing SelectionDAG
SelectionDAG
表示是指令和操作数的目标无关表示。然而,目标可能并不总是支持由 SelectionDAG
表示的指令或数据类型。从这个意义上说,最初构建的 SelectionDAG
图可以称为非法图。DAG 合法化阶段将非法 DAG 转换为目标架构支持的合法 DAG。
DAG 合法化阶段可以通过两种方式将不支持的数据类型转换为支持的数据类型——通过将较小的数据类型提升为较大的数据类型,或者通过截断较大的数据类型为较小的数据类型。例如,假设目标架构只支持 i32 数据类型。在这种情况下,较小的数据类型,如 i8 和 i16,需要提升为 i32 类型。较大的数据类型,如 i64,可以扩展为两个 i32 数据类型。可以添加 Sign
和 Zero
扩展,以确保在提升或扩展数据类型的过程中结果保持一致。
类似地,可以通过将向量拆分为更小的向量(通过从向量中提取元素)或将较小的向量类型扩展为较大的、受支持的向量类型来将向量类型合法化为受支持的向量类型。如果目标架构不支持向量,那么 IR 中的向量中的每个元素都需要以标量形式提取。
合法化阶段还可以指示支持给定数据的寄存器类。
如何操作…
SelectionDAGLegalize
类包含各种数据成员,跟踪数据结构以跟踪合法化节点,以及各种用于操作节点以合法化它们的方法。从 LLVM 主干中合法化阶段代码的示例快照显示了合法化阶段实现的基本框架,如下所示:
namespace {
class SelectionDAGLegalize : public SelectionDAG::DAGUpdateListener {
const TargetMachine &TM;
const TargetLowering &TLI;
SelectionDAG &DAG;
SelectionDAG::allnodes_iterator LegalizePosition;
// LegalizedNodes - The set of nodes which have already been legalized.
SmallPtrSet<SDNode *, 16> LegalizedNodes;
public:
explicit SelectionDAGLegalize(SelectionDAG &DAG);
void LegalizeDAG();
private:
void LegalizeOp(SDNode *Node);
SDValue OptimizeFloatStore(StoreSDNode *ST);
// Legalize Load operations
void LegalizeLoadOps(SDNode *Node);
// Legalize Store operations
void LegalizeStoreOps(SDNode *Node);
// Main legalize function which operates on Selection DAG node
void SelectionDAGLegalize::LegalizeOp(SDNode *Node) {
// A target node which is constant need not be legalized further
if (Node->getOpcode() == ISD::TargetConstant)
return;
for (unsigned i = 0, e = Node->getNumValues(); i != e; ++i)
assert(TLI.getTypeAction(*DAG.getContext(), Node->getValueType(i)) == TargetLowering::TypeLegal && "Unexpected illegal type!");
for (unsigned i = 0, e = Node->getNumOperands(); i != e; ++i)
assert((TLI.getTypeAction(*DAG.getContext(),
Node->getOperand(i).getValueType()) == TargetLowering::TypeLegal ||
Node->getOperand(i).getOpcode() == ISD::TargetConstant) && "Unexpected illegal type!");
TargetLowering::LegalizeAction Action = TargetLowering::Legal;
bool SimpleFinishLegalizing = true;
// Legalize based on instruction opcode
switch (Node->getOpcode()) {
case ISD::INTRINSIC_W_CHAIN:
case ISD::INTRINSIC_WO_CHAIN:
case ISD::INTRINSIC_VOID:
case ISD::STACKSAVE:
Action = TLI.getOperationAction(Node->getOpcode(), MVT::Other);
break;
…
…
}
如何工作…
SelectionDAGLegalize
类的许多函数成员,如 LegalizeOp
,依赖于 SelectionDAGLegalize
类中由 const TargetLowering &TLI
成员提供的特定于目标的详细信息(其他函数成员也可能依赖于 SelectionDAGLegalize
类中的 const TargetMachine &TM
成员)。让我们通过一个例子来演示合法化是如何工作的。
合法化有两种类型:类型合法化和指令合法化。让我们首先看看类型合法化是如何工作的。使用以下命令创建一个 test.ll
文件:
$ cat test.ll
define i64 @test(i64 %a, i64 %b, i64 %c) {
%add = add nsw i64 %a, %b
%div = sdiv i64 %add, %c
ret i64 %div
}
在这种情况下,数据类型是 i64。对于只支持 32 位数据类型的 x86 目标,您刚才看到的数据类型是非法的。要运行前面的代码,数据类型必须转换为 i32。这是由 DAG 合法化阶段完成的。
要查看类型合法化前的 DAG,请运行以下命令行:
$ llc -view-dag-combine1-dags test.ll
下图显示了类型合法化前的 DAG:
要查看类型合法化后的 DAG,请输入以下命令行:
$ llc -view-dag-combine2-dags test.ll
下图显示了类型合法化后的 DAG:
仔细观察 DAG 节点,你可以看到在合法化之前每个操作都有 i64 类型。这是因为 IR 有 i64 数据类型——IR 指令到 DAG 节点的单一映射。然而,目标 x86 机器只支持 i32 类型(32 位整数类型)。DAG 合法化阶段将不支持的 i64 类型转换为支持的 i32 类型。这个操作被称为扩展——将较大的类型拆分成较小的类型。例如,在一个只接受 i32 值的目标中,所有 i64 值都被分解成 i32 值的对。因此,在合法化之后,你可以看到所有操作现在都有 i32 作为数据类型。
让我们看看指令是如何合法化的;使用以下命令创建一个 test.ll
文件:
$ 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
}
要查看合法化前的 DAG,请输入以下命令:
$ llc –view-dag-combine1-dags test.ll
以下图显示了合法化前的 DAG:
要查看合法化后的 DAG,请输入以下命令:
$ llc -view-dag-combine2-dags test.ll
以下图显示了合法化阶段后的 DAG:
在指令合法化之前,DAG 由 sdiv
指令组成。现在,x86 目标不支持 sdiv
指令,因此对于目标来说是不合法的。然而,它支持 sdivrem
指令。因此,合法化阶段涉及将 sdiv
指令转换为 sdivrem
指令,如前述两个 DAG 所示。
优化 SelectionDAG
SelectionDAG
表示法以节点形式展示数据和指令。类似于 LLVM IR 中的 InstCombine
阶段,这些节点可以被合并和优化,形成一个最小化的 SelectionDAG
。但是,优化 SelectionDAG
的不仅仅是 DAGCombine
操作。DAGLegalize
阶段可能会生成一些不必要的 DAG 节点,这些节点会在后续的 DAG 优化阶段运行中被清理。这最终以更简单、更优雅的方式表示了 SelectionDAG
。
如何做到这一点…
在 DAGCombiner
类中提供了大量的函数成员(大多数命名方式如下:visit**()
),用于通过折叠、重排、合并和修改 SDNode
节点来执行优化。请注意,从 DAGCombiner
构造函数中,我们可以推测一些优化需要别名分析信息:
class DAGCombiner {
SelectionDAG &DAG;
const TargetLowering &TLI;
CombineLevel Level;
CodeGenOpt::Level OptLevel;
bool LegalOperations;
bool LegalTypes;
SmallPtrSet<SDNode*, 64> WorkListContents;
SmallVector<SDNode*, 64> WorkListOrder;
AliasAnalysis &AA;
// Add SDnodes users to worklist
void AddUsersToWorkList(SDNode *N) {
for (SDNode::use_iterator UI = N->use_begin(),
UE = N->use_end(); UI != UE; ++UI)
AddToWorkList(*UI);
}
SDValue visit(SDNode *N);
public:
void AddToWorkList(SDNode *N) {
WorkListContents.insert(N);
WorkListOrder.push_back(N);
}
void removeFromWorkList(SDNode *N) {
WorkListContents.erase(N);
}
// SDnode combine operations.
SDValue CombineTo(SDNode *N, const SDValue *To, unsigned NumTo,
bool AddTo = true);
SDValue CombineTo(SDNode *N, SDValue Res, bool AddTo = true) {
return CombineTo(N, &Res, 1, AddTo);
}
SDValue CombineTo(SDNode *N, SDValue Res0, SDValue Res1,
bool AddTo = true) {
SDValue To[] = { Res0, Res1 };
return CombineTo(N, To, 2, AddTo);
}
void CommitTargetLoweringOpt(const TargetLowering::TargetLoweringOpt &TLO);
private:
bool SimplifyDemandedBits(SDValue Op) {
unsigned BitWidth = Op.getValueType().getScalarType().getSizeInBits();
APInt Demanded = APInt::getAllOnesValue(BitWidth);
return SimplifyDemandedBits(Op, Demanded);
}
bool SimplifyDemandedBits(SDValue Op, const APInt &Demanded);
bool CombineToPreIndexedLoadStore(SDNode *N);
bool CombineToPostIndexedLoadStore(SDNode *N);
void ReplaceLoadWithPromotedLoad(SDNode *Load, SDNode *ExtLoad);
SDValue PromoteOperand(SDValue Op, EVT PVT, bool &Replace);
SDValue SExtPromoteOperand(SDValue Op, EVT PVT);
SDValue ZExtPromoteOperand(SDValue Op, EVT PVT);
SDValue PromoteIntBinOp(SDValue Op);
SDValue PromoteIntShiftOp(SDValue Op);
SDValue PromoteExtend(SDValue Op);
bool PromoteLoad(SDValue Op);
void ExtendSetCCUses(SmallVector<SDNode*, 4> SetCCs,
SDValue Trunc, SDValue ExtLoad, DebugLoc DL,
ISD::NodeType ExtType);
SDValue combine(SDNode *N);
// Various visit functions operating on instructions represented
// by SD node. Similar to instruction combining at IR level.
SDValue visitTokenFactor(SDNode *N);
SDValue visitMERGE_VALUES(SDNode *N);
SDValue visitADD(SDNode *N);
SDValue visitSUB(SDNode *N);
SDValue visitADDC(SDNode *N);
SDValue visitSUBC(SDNode *N);
SDValue visitADDE(SDNode *N);
SDValue visitSUBE(SDNode *N);
SDValue visitMUL(SDNode *N);
public:
DAGCombiner(SelectionDAG &D, AliasAnalysis &A, CodeGenOpt::Level OL)
: DAG(D), TLI(D.getTargetLoweringInfo()), Level(BeforeLegalizeTypes),
OptLevel(OL), LegalOperations(false), LegalTypes(false), AA(A) {}
// Selection DAG transformation for following ops
SDValue DAGCombiner::visitMUL(SDNode *N) {
SDValue N0 = N->getOperand(0);
SDValue N1 = N->getOperand(1);
ConstantSDNode *N0C = dyn_cast<ConstantSDNode>(N0);
ConstantSDNode *N1C = dyn_cast<ConstantSDNode>(N1);
EVT VT = N0.getValueType();
if (VT.isVector()) {
SDValue FoldedVOp = SimplifyVBinOp(N);
if (FoldedVOp.getNode()) return FoldedVOp;
}
if (N0.getOpcode() == ISD::UNDEF || N1.getOpcode() == ISD::UNDEF)
return DAG.getConstant(0, VT);
if (N0C && N1C)
return DAG.FoldConstantArithmetic(ISD::MUL, VT, N0C, N1C);
if (N0C && !N1C)
return DAG.getNode(ISD::MUL, N->getDebugLoc(), VT, N1, N0);
if (N1C && N1C->isNullValue())
return N1;
if (N1C && N1C->isAllOnesValue())
return DAG.getNode(ISD::SUB, N->getDebugLoc(), VT, DAG.getConstant(0, VT), N0);
if (N1C && N1C->getAPIntValue().isPowerOf2())
return DAG.getNode(ISD::SHL, N->getDebugLoc(), VT, N0,
DAG.getConstant(N1C->getAPIntValue().logBase2(),
getShiftAmountTy(N0.getValueType())));
if (N1C && (-N1C->getAPIntValue()).isPowerOf2()) {
unsigned Log2Val = (-N1C->getAPIntValue()).logBase2();
return DAG.getNode(ISD::SUB, N->getDebugLoc(), VT, DAG.getConstant(0, VT),
DAG.getNode(ISD::SHL, N->getDebugLoc(), VT, N0,
DAG.getConstant(Log2Val, getShiftAmountTy(N0.getValueType()))));
}
if (N1C && N0.getOpcode() == ISD::SHL &&
isa<ConstantSDNode>(N0.getOperand(1))) {
SDValue C3 = DAG.getNode(ISD::SHL, N->getDebugLoc(), VT, N1, N0.getOperand(1));
AddToWorkList(C3.getNode());
return DAG.getNode(ISD::MUL, N->getDebugLoc(), VT,
N0.getOperand(0), C3);
}
if (N0.getOpcode() == ISD::SHL && isa<ConstantSDNode>(N0.getOperand(1)) &&
N0.getNode()->hasOneUse()) {
Sh = N0; Y = N1;
} else if (N1.getOpcode() == ISD::SHL && isa<ConstantSDNode>(N1.getOperand(1)) &&
N1.getNode()->hasOneUse()) {
Sh = N1; Y = N0;
}
if (Sh.getNode()) {
SDValue Mul = DAG.getNode(ISD::MUL, N->getDebugLoc(), VT, Sh.getOperand(0), Y);
return DAG.getNode(ISD::SHL, N->getDebugLoc(), VT,
Mul, Sh.getOperand(1));
}
}
if (N1C && N0.getOpcode() == ISD::ADD && N0.getNode()->hasOneUse() &&
isa<ConstantSDNode>(N0.getOperand(1)))
return DAG.getNode(ISD::ADD, N->getDebugLoc(), VT, DAG.getNode(ISD::MUL, N0.getDebugLoc(),
VT, N0.getOperand(0), N1), DAG.getNode(ISD::MUL, N1.getDebugLoc(), VT, N0.getOperand(1), N1));
SDValue RMUL = ReassociateOps(ISD::MUL, N->getDebugLoc(), N0, N1);
if (RMUL.getNode() != 0) return RMUL;
return SDValue();
}
它是如何工作的…
如前述代码所示,一些 DAGCombine
阶段会搜索一个模式,然后将模式折叠成一个单一的 DAG。这基本上减少了 DAG 的数量,同时降低了 DAG 的复杂度。结果是优化后的 SelectionDAG
类。
参见
- 要查看优化后的
SelectionDAG
类的更详细实现,请参阅位于lib/CodeGen/SelectionDAG/
目录下的DAGCombiner.cpp
文件。
从 DAG 中选择指令
在合法化和 DAG 组合之后,SelectionDAG
表示处于优化阶段。然而,表示的指令仍然是目标无关的,需要映射到特定于目标的指令上。指令选择阶段以目标无关的 DAG 节点作为输入,匹配其中的模式,并给出输出 DAG 节点,这些节点是特定于目标的。
TableGen
DAG 指令选择器从.td
文件中读取指令模式,并自动构建部分模式匹配代码。
如何操作…
SelectionDAGISel
是用于基于SelectionDAG
的模式匹配指令选择器的通用基类。它继承自MachineFunctionPass
类。它包含用于确定折叠等操作的合法性和盈利性的各种函数。此类的基本结构如下:
class SelectionDAGISel : public MachineFunctionPass {
public:
const TargetMachine &TM;
const TargetLowering &TLI;
const TargetLibraryInfo *LibInfo;
FunctionLoweringInfo *FuncInfo;
MachineFunction *MF;
MachineRegisterInfo *RegInfo;
SelectionDAG *CurDAG;
SelectionDAGBuilder *SDB;
AliasAnalysis *AA;
GCFunctionInfo *GFI;
CodeGenOpt::Level OptLevel;
static char ID;
explicit SelectionDAGISel(const TargetMachine &tm,
CodeGenOpt::Level OL = CodeGenOpt::Default);
virtual ~SelectionDAGISel();
const TargetLowering &getTargetLowering() { return TLI; }
virtual void getAnalysisUsage(AnalysisUsage &AU) const;
virtual bool runOnMachineFunction(MachineFunction &MF);
virtual void EmitFunctionEntryCode() {}
virtual void PreprocessISelDAG() {}
virtual void PostprocessISelDAG() {}
virtual SDNode *Select(SDNode *N) = 0;
virtual bool SelectInlineAsmMemoryOperand(const SDValue &Op,
char ConstraintCode,
std::vector<SDValue> &OutOps) {
return true;
}
virtual bool IsProfitableToFold(SDValue N, SDNode *U, SDNode *Root) const;
static bool IsLegalToFold(SDValue N, SDNode *U, SDNode *Root,
CodeGenOpt::Level OptLevel,
bool IgnoreChains = false);
enum BuiltinOpcodes {
OPC_Scope,
OPC_RecordNode,
OPC_CheckOpcode,
OPC_SwitchOpcode,
OPC_CheckFoldableChainNode,
OPC_EmitInteger,
OPC_EmitRegister,
OPC_EmitRegister2,
OPC_EmitConvertToTarget,
OPC_EmitMergeInputChains,
};
static inline int getNumFixedFromVariadicInfo(unsigned Flags) {
return ((Flags&OPFL_VariadicInfo) >> 4)-1;
}
protected:
// DAGSize - Size of DAG being instruction selected.
unsigned DAGSize;
void ReplaceUses(SDValue F, SDValue T) {
CurDAG->ReplaceAllUsesOfValueWith(F, T);
}
void ReplaceUses(const SDValue *F, const SDValue *T, unsigned Num) {
CurDAG->ReplaceAllUsesOfValuesWith(F, T, Num);
}
void ReplaceUses(SDNode *F, SDNode *T) {
CurDAG->ReplaceAllUsesWith(F, T);
}
void SelectInlineAsmMemoryOperands(std::vector<SDValue> &Ops);
public:
bool CheckAndMask(SDValue LHS, ConstantSDNode *RHS,
int64_t DesiredMaskS) const;
bool CheckOrMask(SDValue LHS, ConstantSDNode *RHS,
int64_t DesiredMaskS) const;
virtual bool CheckPatternPredicate(unsigned PredNo) const {
llvm_unreachable("Tblgen should generate the implementation of this!");
}
virtual bool CheckNodePredicate(SDNode *N, unsigned PredNo) const {
llvm_unreachable("Tblgen should generate the implementation of this!");
}
private:
SDNode *Select_INLINEASM(SDNode *N);
SDNode *Select_UNDEF(SDNode *N);
void CannotYetSelect(SDNode *N);
void DoInstructionSelection();
SDNode *MorphNode(SDNode *Node, unsigned TargetOpc, SDVTList VTs,
const SDValue *Ops, unsigned NumOps, unsigned EmitNodeInfo);
void PrepareEHLandingPad();
void SelectAllBasicBlocks(const Function &Fn);
bool TryToFoldFastISelLoad(const LoadInst *LI, const Instruction *FoldInst, FastISel *FastIS);
void FinishBasicBlock();
void SelectBasicBlock(BasicBlock::const_iterator Begin,
BasicBlock::const_iterator End,
bool &HadTailCall);
void CodeGenAndEmitDAG();
void LowerArguments(const BasicBlock *BB);
void ComputeLiveOutVRegInfo();
ScheduleDAGSDNodes *CreateScheduler();
};
如何工作…
指令选择阶段涉及将目标无关指令转换为特定于目标的指令。TableGen
类帮助选择特定于目标的指令。此阶段基本上匹配目标无关的输入节点,从而给出由目标支持的节点组成的输出。
CodeGenAndEmitDAG()
函数调用DoInstructionSelection()
函数,该函数遍历每个 DAG 节点,并为每个节点调用Select()
函数,如下所示:
SDNode *ResNode = Select(Node);
Select()
函数是由目标实现的抽象方法。x86 目标在X86DAGToDAGISel::Select()
函数中实现它。X86DAGToDAGISel::Select()
函数拦截一些节点进行手动匹配,但将大部分工作委托给X86DAGToDAGISel::SelectCode()
函数。
X86DAGToDAGISel::SelectCode
函数由TableGen
自动生成。它包含匹配器表,随后调用通用的SelectionDAGISel::SelectCodeCommon()
函数,并传递该表。
例如:
$ 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
}
要查看指令选择之前的 DAG,请输入以下命令行:
$ llc –view-isel-dags test.ll
以下图显示了指令选择之前的 DAG:
要查看指令选择之后的 DAG 看起来如何,请输入以下命令:
$ llc –view-sched-dags test.ll
以下图显示了指令选择之后的 DAG:
如所见,指令选择阶段将Load
操作转换为MOV32rm
机器代码。
参见
- 要查看指令选择的详细实现,请查看位于
lib/CodeGen/SelectionDAG/
的SelectionDAGISel.cpp
文件。
SelectionDAG 中的调度指令
到目前为止,我们已经有由目标支持的指令和操作数组成的SelectionDAG
节点。然而,代码仍然以 DAG 表示形式存在。目标架构以顺序形式执行指令。因此,下一步合乎逻辑的步骤是对SelectionDAG
节点进行调度。
调度器从 DAG(有向无环图)中分配指令的执行顺序。在这个过程中,它考虑了各种启发式方法,例如寄存器压力,以优化指令的执行顺序并最小化指令执行中的延迟。在将执行顺序分配给 DAG 节点之后,节点被转换成一个MachineInstrs
列表,并且SelectionDAG
节点被销毁。
如何操作…
在ScheduleDAG.h
文件中定义了几个基本结构,并在ScheduleDAG.cpp
文件中实现。ScheduleDAG
类是其他调度器继承的基类,它仅提供与图相关的操作,例如迭代器、DFS(深度优先搜索)、拓扑排序、移动节点的函数等:
class ScheduleDAG {
public:
const TargetMachine &TM; // Target processor
const TargetInstrInfo *TII; // Target instruction
const TargetRegisterInfo *TRI; // Target processor register info
MachineFunction &MF; // Machine function
MachineRegisterInfo &MRI; // Virtual/real register map
std::vector<SUnit> SUnits; // The scheduling units.
SUnit EntrySU; // Special node for the region entry.
SUnit ExitSU; // Special node for the region exit.
explicit ScheduleDAG(MachineFunction &mf);
virtual ~ScheduleDAG();
void clearDAG();
const MCInstrDesc *getInstrDesc(const SUnit *SU) const {
if (SU->isInstr()) return &SU->getInstr()->getDesc();
return getNodeDesc(SU->getNode());
}
virtual void dumpNode(const SUnit *SU) const = 0;
private:
const MCInstrDesc *getNodeDesc(const SDNode *Node) const;
};
class SUnitIterator : public std::iterator<std::forward_iterator_tag,
SUnit, ptrdiff_t> {
};
template <> struct GraphTraits<SUnit*> {
typedef SUnit NodeType;
typedef SUnitIterator ChildIteratorType;
static inline NodeType *getEntryNode(SUnit *N) {
return N;
}
static inline ChildIteratorType child_begin(NodeType *N) {
return SUnitIterator::begin(N);
}
static inline ChildIteratorType child_end(NodeType *N) {
return SUnitIterator::end(N);
}
};
template <> struct GraphTraits<ScheduleDAG*> : public GraphTraits<SUnit*> {
…};
// Topological sorting of DAG to linear set of instructions
class ScheduleDAGTopologicalSort {
std::vector<SUnit> &SUnits;
SUnit *ExitSU;
std::vector<int> Index2Node;
std::vector<int> Node2Index;
BitVector Visited;
// DFS to be run on DAG to sort topologically
void DFS(const SUnit *SU, int UpperBound, bool& HasLoop);
void Shift(BitVector& Visited, int LowerBound, int UpperBound);
void Allocate(int n, int index);
public:
ScheduleDAGTopologicalSort(std::vector<SUnit> &SUnits, SUnit *ExitSU);
void InitDAGTopologicalSorting();
bool IsReachable(const SUnit *SU, const SUnit *TargetSU);
bool WillCreateCycle(SUnit *SU, SUnit *TargetSU);
void AddPred(SUnit *Y, SUnit *X);
void RemovePred(SUnit *M, SUnit *N);
typedef std::vector<int>::iterator iterator;
typedef std::vector<int>::const_iterator const_iterator;
iterator begin() { return Index2Node.begin(); }
const_iterator begin() const { return Index2Node.begin(); }
iterator end() { return Index2Node.end();}}
它是如何工作的…
调度算法在SelectionDAG
类中实现了指令的调度,涉及多种算法,如拓扑排序、深度优先搜索、函数操作、节点移动和遍历指令列表。它考虑了各种启发式方法,例如寄存器压力、溢出成本、活跃区间分析等,以确定指令的最佳调度方案。
相关内容
- 有关调度指令的详细实现,请参阅位于
lib/CodeGen/SelectionDAG
文件夹中的ScheduleDAGSDNodes.cpp
、ScheduleDAGSDNodes.h
、ScheduleDAGRRList.cpp
、ScheduleDAGFast.cpp
和ScheduleDAGVLIW.cpp
文件
第七章. 优化机器代码
在本章中,我们将涵盖以下内容:
-
从机器代码中消除公共子表达式
-
分析活跃区间
-
寄存器分配
-
插入前导和后继代码
-
代码发射
-
尾调用优化
-
兄弟调用优化
简介
到目前为止生成的机器代码尚未分配到真实的目标架构寄存器。到目前为止看到的寄存器都是虚拟寄存器,数量无限。生成的机器代码处于 SSA 形式。然而,目标寄存器的数量是有限的。因此,寄存器分配算法需要大量的启发式计算来以最优方式分配寄存器。
但是,在寄存器分配之前,存在代码优化的机会。机器代码处于 SSA(Static Single Assignment)形式,也使得应用优化算法变得容易。一些优化技术(如机器死代码消除和机器公共子表达式消除)的算法几乎与 LLVM IR 中的相同。区别在于需要检查的约束条件。
在这里,我们将讨论在 LLVM 主干代码库中实现的一种机器代码优化技术——机器 CSE(Common Subexpression Elimination),以便您了解机器代码的算法是如何实现的。
从机器代码中消除公共子表达式
CSE 算法的目的是消除公共子表达式,使机器代码紧凑,并移除不必要的、重复的代码。让我们看看 LLVM 主干中的代码,以了解它是如何实现的。详细代码位于 lib/CodeGen/MachineCSE.cpp
文件中。
如何做到这一点……
-
MachineCSE
类在机器函数上运行,因此它应该继承MachineFunctionPass
类。它有各种成员,例如TargetInstructionInfo
,用于获取目标指令的信息(用于执行 CSE);TargetRegisterInfo
,用于获取目标寄存器的信息(是否属于保留的寄存器类,或更多此类类似类;以及MachineDominatorTree
,用于获取机器块的支配树信息:class MachineCSE : public MachineFunctionPass { const TargetInstrInfo *TII; const TargetRegisterInfo *TRI; AliasAnalysis *AA; MachineDominatorTree *DT; MachineRegisterInfo *MRI;
-
该类的构造函数定义如下,它初始化了遍历:
public: static char ID; // Pass identification MachineCSE() : MachineFunctionPass(ID), LookAheadLimit(5), CurrVN(0) { initializeMachineCSEPass(*PassRegistry::getPassRegistry()); }
-
getAnalysisUsage()
函数确定在当前遍历之前将运行哪些遍历,以获取可用于当前遍历的统计数据:void getAnalysisUsage(AnalysisUsage &AU) const override { AU.setPreservesCFG(); MachineFunctionPass::getAnalysisUsage(AU); AU.addRequired<AliasAnalysis>(); AU.addPreservedID(MachineLoopInfoID); AU.addRequired<MachineDominatorTree>(); AU.addPreserved<MachineDominatorTree>(); }
-
在此遍历中声明一些辅助函数以检查简单的复制传播和明显无用的定义,检查物理寄存器的活跃性和它们的定义使用,等等:
private: ….. ….. bool PerformTrivialCopyPropagation(MachineInstr *MI, MachineBasicBlock *MBB); bool isPhysDefTriviallyDead(unsigned Reg, MachineBasicBlock::const_iterator I, MachineBasicBlock::const_iterator E) const; bool hasLivePhysRegDefUses(const MachineInstr *MI, const MachineBasicBlock *MBB, SmallSet<unsigned,8> &PhysRefs, SmallVectorImpl<unsigned> &PhysDefs, bool &PhysUseDef) const; bool PhysRegDefsReach(MachineInstr *CSMI, MachineInstr *MI, SmallSet<unsigned,8> &PhysRefs, SmallVectorImpl<unsigned> &PhysDefs, bool &NonLocal) const;
-
一些辅助函数有助于确定表达式作为 CSE 候选者的合法性和盈利性:
bool isCSECandidate(MachineInstr *MI); bool isProfitableToCSE(unsigned CSReg, unsigned Reg, MachineInstr *CSMI, MachineInstr *MI); Actual CSE performing function bool PerformCSE(MachineDomTreeNode *Node);
让我们看看 CSE 函数的实际实现:
-
runOnMachineFunction()
函数首先被调用,因为该遍历运行:bool MachineCSE::runOnMachineFunction(MachineFunction &MF){ if (skipOptnoneFunction(*MF.getFunction())) return false; TII = MF.getSubtarget().getInstrInfo(); TRI = MF.getSubtarget().getRegisterInfo(); MRI = &MF.getRegInfo(); AA = &getAnalysis<AliasAnalysis>(); DT = &getAnalysis<MachineDominatorTree>(); return PerformCSE(DT->getRootNode()); }
-
接下来调用
PerformCSE()
函数。它接受DomTree
的根节点,在DomTree
上执行 DFS 遍历(从根节点开始),并填充一个包含DomTree
节点的作业列表。在 DFS 遍历DomTree
之后,它处理作业列表中每个节点对应的MachineBasicBlock
类:bool MachineCSE::PerformCSE(MachineDomTreeNode *Node) { SmallVector<MachineDomTreeNode*, 32> Scopes; SmallVector<MachineDomTreeNode*, 8> WorkList; DenseMap<MachineDomTreeNode*, unsigned> OpenChildren; CurrVN = 0; // DFS to populate worklist WorkList.push_back(Node); do { Node = WorkList.pop_back_val(); Scopes.push_back(Node); const std::vector<MachineDomTreeNode*> &Children = Node->getChildren(); unsigned NumChildren = Children.size(); OpenChildren[Node] = NumChildren; for (unsigned i = 0; i != NumChildren; ++i) { MachineDomTreeNode *Child = Children[i]; WorkList.push_back(Child); } } while (!WorkList.empty()); // perform CSE. bool Changed = false; for (unsigned i = 0, e = Scopes.size(); i != e; ++i) { MachineDomTreeNode *Node = Scopes[i]; MachineBasicBlock *MBB = Node->getBlock(); EnterScope(MBB); Changed |= ProcessBlock(MBB); ExitScopeIfDone(Node, OpenChildren); } return Changed; }
-
下一个重要的函数是
ProcessBlock()
函数,它作用于机器基本块。MachineBasicBlock
类中的指令被迭代并检查其合法性及盈利性,如果它们可以成为 CSE 候选者:bool MachineCSE::ProcessBlock(MachineBasicBlock *MBB) { bool Changed = false; SmallVector<std::pair<unsigned, unsigned>, 8> CSEPairs; SmallVector<unsigned, 2> ImplicitDefsToUpdate; // Iterate over each Machine instructions in the MachineBasicBlock for (MachineBasicBlock::iterator I = MBB->begin(), E = MBB->end(); I != E; ) { MachineInstr *MI = &*I; ++I; // Check if this can be a CSE candidate. if (!isCSECandidate(MI)) continue; bool FoundCSE = VNT.count(MI); if (!FoundCSE) { // Using trivial copy propagation to find more CSE opportunities. if (PerformTrivialCopyPropagation(MI, MBB)) { Changed = true; // After coalescing MI itself may become a copy. if (MI->isCopyLike()) continue; // Try again to see if CSE is possible. FoundCSE = VNT.count(MI); } } bool Commuted = false; if (!FoundCSE && MI->isCommutable()) { MachineInstr *NewMI = TII->commuteInstruction(MI); if (NewMI) { Commuted = true; FoundCSE = VNT.count(NewMI); if (NewMI != MI) { // New instruction. It doesn't need to be kept. NewMI->eraseFromParent(); Changed = true; } else if (!FoundCSE) // MI was changed but it didn't help, commute it back! (void)TII->commuteInstruction(MI); } } // If the instruction defines physical registers and the values *may* be // used, then it's not safe to replace it with a common subexpression. // It's also not safe if the instruction uses physical registers. bool CrossMBBPhysDef = false; SmallSet<unsigned, 8> PhysRefs; SmallVector<unsigned, 2> PhysDefs; bool PhysUseDef = false; // Check if this instruction has been marked for CSE. Check if it is using physical register, if yes then mark as non-CSE candidate if (FoundCSE && hasLivePhysRegDefUses(MI, MBB, PhysRefs, PhysDefs, PhysUseDef)) { FoundCSE = false; … … } if (!FoundCSE) { VNT.insert(MI, CurrVN++); Exps.push_back(MI); continue; } // Finished job of determining if there exists a common subexpression. // Found a common subexpression, eliminate it. unsigned CSVN = VNT.lookup(MI); MachineInstr *CSMI = Exps[CSVN]; DEBUG(dbgs() << "Examining: " << *MI); DEBUG(dbgs() << "*** Found a common subexpression: " << *CSMI); // Check if it's profitable to perform this CSE. bool DoCSE = true; unsigned NumDefs = MI->getDesc().getNumDefs() + MI->getDesc().getNumImplicitDefs(); for (unsigned i = 0, e = MI->getNumOperands(); NumDefs && i != e; ++i) { MachineOperand &MO = MI->getOperand(i); if (!MO.isReg() || !MO.isDef()) continue; unsigned OldReg = MO.getReg(); unsigned NewReg = CSMI->getOperand(i).getReg(); // Go through implicit defs of CSMI and MI, if a def is not dead at MI, // we should make sure it is not dead at CSMI. if (MO.isImplicit() && !MO.isDead() && CSMI->getOperand(i).isDead()) ImplicitDefsToUpdate.push_back(i); if (OldReg == NewReg) { --NumDefs; continue; } assert(TargetRegisterInfo::isVirtualRegister(OldReg) && TargetRegisterInfo::isVirtualRegister(NewReg) && "Do not CSE physical register defs!"); if (!isProfitableToCSE(NewReg, OldReg, CSMI, MI)) { DEBUG(dbgs() << "*** Not profitable, avoid CSE!\n"); DoCSE = false; break; } // Don't perform CSE if the result of the old instruction cannot exist // within the register class of the new instruction. const TargetRegisterClass *OldRC = MRI->getRegClass(OldReg); if (!MRI->constrainRegClass(NewReg, OldRC)) { DEBUG(dbgs() << "*** Not the same register class, avoid CSE!\n"); DoCSE = false; break; } CSEPairs.push_back(std::make_pair(OldReg, NewReg)); --NumDefs; } // Actually perform the elimination. if (DoCSE) { for (unsigned i = 0, e = CSEPairs.size(); i != e; ++i) { MRI->replaceRegWith(CSEPairs[i].first, CSEPairs[i].second); MRI->clearKillFlags(CSEPairs[i].second); } // Go through implicit defs of CSMI and MI, if a def is not dead at MI, // we should make sure it is not dead at CSMI. for (unsigned i = 0, e = ImplicitDefsToUpdate.size(); i != e; ++i) CSMI->getOperand(ImplicitDefsToUpdate[i]).setIsDead(false); if (CrossMBBPhysDef) { // Add physical register defs now coming in from a predecessor to MBB // livein list. while (!PhysDefs.empty()) { unsigned LiveIn = PhysDefs.pop_back_val(); if (!MBB->isLiveIn(LiveIn)) MBB->addLiveIn(LiveIn); } ++NumCrossBBCSEs; } MI->eraseFromParent(); ++NumCSEs; if (!PhysRefs.empty()) ++NumPhysCSEs; if (Commuted) ++NumCommutes; Changed = true; } else { VNT.insert(MI, CurrVN++); Exps.push_back(MI); } CSEPairs.clear(); ImplicitDefsToUpdate.clear(); } return Changed; }
-
让我们也来看看合法性和盈利函数,以确定 CSE 候选者:
bool MachineCSE::isCSECandidate(MachineInstr *MI) { // If Machine Instruction is PHI, or inline ASM or implicit defs, it is not a candidate for CSE. if (MI->isPosition() || MI->isPHI() || MI->isImplicitDef() || MI->isKill() || MI->isInlineAsm() || MI->isDebugValue()) return false; // Ignore copies. if (MI->isCopyLike()) return false; // Ignore instructions that we obviously can't move. if (MI->mayStore() || MI->isCall() || MI->isTerminator() || MI->hasUnmodeledSideEffects()) return false; if (MI->mayLoad()) { // Okay, this instruction does a load. As a refinement, we allow the target // to decide whether the loaded value is actually a constant. If so, we can // actually use it as a load. if (!MI->isInvariantLoad(AA)) return false; } return true; }
-
盈利函数编写如下:
bool MachineCSE::isProfitableToCSE(unsigned CSReg, unsigned Reg, MachineInstr *CSMI, MachineInstr *MI) { // If CSReg is used at all uses of Reg, CSE should not increase register // pressure of CSReg. bool MayIncreasePressure = true; if (TargetRegisterInfo::isVirtualRegister(CSReg) && TargetRegisterInfo::isVirtualRegister(Reg)) { MayIncreasePressure = false; SmallPtrSet<MachineInstr*, 8> CSUses; for (MachineInstr &MI : MRI->use_nodbg_instructions(CSReg)) { CSUses.insert(&MI); } for (MachineInstr &MI : MRI->use_nodbg_instructions(Reg)) { if (!CSUses.count(&MI)) { MayIncreasePressure = true; break; } } } if (!MayIncreasePressure) return true; // Heuristics #1: Don't CSE "cheap" computation if the def is not local or in // an immediate predecessor. We don't want to increase register pressure and // end up causing other computation to be spilled. if (TII->isAsCheapAsAMove(MI)) { MachineBasicBlock *CSBB = CSMI->getParent(); MachineBasicBlock *BB = MI->getParent(); if (CSBB != BB && !CSBB->isSuccessor(BB)) return false; } // Heuristics #2: If the expression doesn't not use a vr and the only use // of the redundant computation are copies, do not cse. bool HasVRegUse = false; for (unsigned i = 0, e = MI->getNumOperands(); i != e; ++i) { const MachineOperand &MO = MI->getOperand(i); if (MO.isReg() && MO.isUse() && TargetRegisterInfo::isVirtualRegister(MO.getReg())) { HasVRegUse = true; break; } } if (!HasVRegUse) { bool HasNonCopyUse = false; for (MachineInstr &MI : MRI->use_nodbg_instructions(Reg)) { // Ignore copies. if (!MI.isCopyLike()) { HasNonCopyUse = true; break; } } if (!HasNonCopyUse) return false; } // Heuristics #3: If the common subexpression is used by PHIs, do not reuse // it unless the defined value is already used in the BB of the new use. bool HasPHI = false; SmallPtrSet<MachineBasicBlock*, 4> CSBBs; for (MachineInstr &MI : MRI->use_nodbg_instructions(CSReg)) { HasPHI |= MI.isPHI(); CSBBs.insert(MI.getParent()); } if (!HasPHI) return true; return CSBBs.count(MI->getParent()); }
它是如何工作的…
MachineCSE
传递在机器函数上运行。它获取 DomTree
信息,然后以 DFS 方式遍历 DomTree
,创建一个由基本 MachineBasicBlocks
构成的节点作业列表。然后它对每个块进行 CSE 处理。在每个块中,它迭代所有指令并检查是否有任何指令是 CSE 候选者。然后它检查消除已识别表达式是否具有盈利性。一旦它发现已识别的 CSE 消除具有盈利性,它就从 MachineBasicBlock
类中消除 MachineInstruction
类。它还执行机器指令的简单复制传播。在某些情况下,MachineInstruction
在初始运行时可能不是 CSE 的候选者,但在复制传播后可能成为候选者。
查看更多
要查看更多以 SSA 形式呈现的机器代码优化,请查看 lib/CodeGen/DeadMachineInstructionElim.cpp
文件中机器死代码消除传递的实现。
分析活跃区间
在本章的后续内容中,我们将探讨寄存器分配。然而,在我们前往那里之前,你必须理解活动变量和活动区间的概念。通过活动区间,我们指的是变量活跃的范围,即从变量定义点到其最后使用点。为此,我们需要计算指令(变量的最后使用)之后立即失效的寄存器集合,以及指令使用但不在指令之后使用的寄存器集合。我们为函数中的每个虚拟寄存器和物理寄存器计算活动变量信息。使用 SSA(Static Single Assignment)来稀疏计算虚拟寄存器的生命周期信息,使我们能够仅跟踪块内的物理寄存器。在寄存器分配之前,LLVM 假设物理寄存器仅在单个基本块内活跃。这使得它能够执行单个、局部的分析,以在每个基本块内解决物理寄存器的生命周期。在执行活动变量分析后,我们拥有了执行活动区间分析和构建活动区间的所需信息。为此,我们开始对基本块和机器指令进行编号。之后,处理 live-in 值,通常是寄存器中的参数。对于虚拟寄存器的活动区间,我们将根据机器指令的某种顺序(1,N)进行计算。活动区间是一个区间(i,j),其中变量是活跃的,其中1 >= i >= j > N。
在这个菜谱中,我们将取一个示例程序,看看我们如何列出该程序的活动区间。我们将查看 LLVM 是如何计算这些区间的。
准备工作
要开始,我们需要一段用于执行活动区间分析的测试代码。为了简单起见,我们将使用 C 代码并将其转换为 LLVM IR:
-
编写一个包含
if
-else
块的测试程序:$ cat interval.c void donothing(int a) { return; } int func(int i) { int a = 5; donothing(a); int m = a; donothing(m); a = 9; if (i < 5) { int b = 3; donothing(b); int z = b; donothing(z); } else { int k = a; donothing(k); } return m; }
-
使用 Clang 将 C 代码转换为 IR,然后使用
cat
命令查看生成的 IR:$ clang -cc1 -emit-llvm interval.c $ cat interval.ll ; ModuleID = 'interval.c' target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; Function Attrs: nounwind define void @donothing(i32 %a) #0 { %1 = alloca i32, align 4 store i32 %a, i32* %1, align 4 ret void } ; Function Attrs: nounwind define i32 @func(i32 %i) #0 { %1 = alloca i32, align 4 %a = alloca i32, align 4 %m = alloca i32, align 4 %b = alloca i32, align 4 %z = alloca i32, align 4 %k = alloca i32, align 4 store i32 %i, i32* %1, align 4 store i32 5, i32* %a, align 4 %2 = load i32, i32* %a, align 4 call void @donothing(i32 %2) %3 = load i32, i32* %a, align 4 store i32 %3, i32* %m, align 4 %4 = load i32, i32* %m, align 4 call void @donothing(i32 %4) store i32 9, i32* %a, align 4 %5 = load i32, i32* %1, align 4 %6 = icmp slt i32 %5, 5 br i1 %6, label %7, label %11 ; <label>:7 ; preds = %0 store i32 3, i32* %b, align 4 %8 = load i32, i32* %b, align 4 call void @donothing(i32 %8) %9 = load i32, i32* %b, align 4 store i32 %9, i32* %z, align 4 %10 = load i32, i32* %z, align 4 call void @donothing(i32 %10) br label %14 ; <label>:11 ; preds = %0 %12 = load i32, i32* %a, align 4 store i32 %12, i32* %k, align 4 %13 = load i32, i32* %k, align 4 call void @donothing(i32 %13) br label %14 ; <label>:14 ; preds = %11, %7 %15 = load i32, i32* %m, align 4 ret i32 %15 } attributes #0 = { nounwind "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-realign-stack" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{!0} !0 = !{!"clang version 3.7.0 (trunk 234045)"}
如何做…
-
要列出活动区间,我们需要修改
LiveIntervalAnalysis.cpp
文件的代码,添加打印活动区间的代码。我们将添加以下行(每行前带有+
符号):void LiveIntervals::computeVirtRegInterval(LiveInterval &LI) { assert(LRCalc && "LRCalc not initialized."); assert(LI.empty() && "Should only compute empty intervals."); LRCalc->reset(MF, getSlotIndexes(), DomTree, &getVNInfoAllocator()); LRCalc->calculate(LI, MRI->shouldTrackSubRegLiveness(LI.reg)); computeDeadValues(LI, nullptr); /**** add the following code ****/ + llvm::outs() << "********** INTERVALS **********\n"; // Dump the regunits. + for (unsigned i = 0, e = RegUnitRanges.size(); i != e; ++i) + if (LiveRange *LR = RegUnitRanges[i]) + llvm::outs() << PrintRegUnit(i, TRI) << ' ' << *LR << '\n'; // Dump the virtregs. + llvm::outs() << "virtregs:"; + for (unsigned i = 0, e = MRI->getNumVirtRegs(); i != e; ++i) { + unsigned Reg = TargetRegisterInfo::index2VirtReg(i); + if (hasInterval(Reg)) + llvm::outs() << getInterval(Reg) << '\n'; + }
-
在修改前面的源文件后构建 LLVM,并将其安装到路径上。
-
现在使用
llc
命令编译测试代码的 IR 形式。你将得到活动区间:$ llc interval.ll ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r %vreg2 [144r,192r:0) 0@144r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r %vreg2 [144r,192r:0) 0@144r %vreg5 [544r,592r:0) 0@544r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r %vreg2 [144r,192r:0) 0@144r %vreg5 [544r,592r:0) 0@544r %vreg6 [352r,368r:0) 0@352r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r %vreg2 [144r,192r:0) 0@144r %vreg5 [544r,592r:0) 0@544r %vreg6 [352r,368r:0) 0@352r %vreg7 [416r,464r:0) 0@416r ********** INTERVALS ********** virtregs:%vreg0 [16r,32r:0) 0@16r %vreg1 [80r,96r:0) 0@80r %vreg2 [144r,192r:0) 0@144r %vreg5 [544r,592r:0) 0@544r %vreg6 [352r,368r:0) 0@352r %vreg7 [416r,464r:0) 0@416r %vreg8 [656r,672r:0) 0@656r
它是如何工作的…
在前面的例子中,我们看到了活区间与每个虚拟寄存器相关联的方式。程序中的活区间的开始和结束位置用方括号标记。生成这些活区间的过程从 lib/CodeGen/LiveVariables.cpp
文件中的 LiveVariables::runOnMachineFunction(MachineFunction &mf)
函数开始,它使用 HandleVirtRegUse
和 HandleVirtRegDef
函数分配寄存器的定义和使用。它使用 getVarInfo
函数获取给定虚拟寄存器的 VarInfo
对象。
LiveInterval
和 LiveRange
类在 LiveInterval.cpp
中定义。该文件中的函数接收每个变量的活跃信息,然后检查它们是否重叠。
在 LiveIntervalAnalysis.cpp
文件中,我们有活区间分析传递的实现,它以深度优先的方式遍历基本块(按线性顺序排列),并为每个虚拟和物理寄存器创建一个活区间。这种分析被寄存器分配器使用,将在下一食谱中讨论。
参见
-
如果您想详细了解不同基本块的虚拟寄存器是如何生成的,以及这些虚拟寄存器的生命周期,请在编译测试用例时使用
llc
工具的–debug-only=regalloc
命令行选项。您需要一个调试版本的 LLVM 才能这样做。 -
要获取关于实时区间的更多详细信息,请查看以下代码文件:
-
Lib/CodeGen/ LiveInterval.cpp
-
Lib/CodeGen/ LiveIntervalAnalysis.cpp
-
Lib/CodeGen/ LiveVariables.cpp
-
分配寄存器
寄存器分配是将物理寄存器分配给虚拟寄存器的任务。虚拟寄存器可以是无限的,但机器的物理寄存器是有限的。因此,寄存器分配的目标是最大化分配给虚拟寄存器的物理寄存器数量。在本食谱中,我们将了解寄存器在 LLVM 中的表示方式,如何修改寄存器信息,执行步骤,以及内置的寄存器分配器。
准备工作
您需要构建和安装 LLVM。
如何操作...
-
要查看寄存器在 LLVM 中的表示方式,请打开
build-folder/lib/Target/X86/X86GenRegisterInfo.inc
文件并查看前几行,这些行显示寄存器以整数的形式表示:namespace X86 { enum { NoRegister, AH = 1, AL = 2, AX = 3, BH = 4, BL = 5, BP = 6, BPL = 7, BX = 8, CH = 9, …
-
对于具有共享相同物理位置的寄存器的架构,请查看该架构的
RegisterInfo.td
文件以获取别名信息。让我们查看lib/Target/X86/X86RegisterInfo.td
文件。通过查看以下代码片段,我们可以看到EAX
、AX
和AL
寄存器是如何别名的(我们只指定最小的寄存器别名):def AL : X86Reg<"al", 0>; def DL : X86Reg<"dl", 2>; def CL : X86Reg<"cl", 1>; def BL : X86Reg<"bl", 3>; def AH : X86Reg<"ah", 4>; def DH : X86Reg<"dh", 6>; def CH : X86Reg<"ch", 5>; def BH : X86Reg<"bh", 7>; def AX : X86Reg<"ax", 0, [AL,AH]>; def DX : X86Reg<"dx", 2, [DL,DH]>; def CX : X86Reg<"cx", 1, [CL,CH]>; def BX : X86Reg<"bx", 3, [BL,BH]>; // 32-bit registers let SubRegIndices = [sub_16bit] in { def EAX : X86Reg<"eax", 0, [AX]>, DwarfRegNum<[-2, 0, 0]>; def EDX : X86Reg<"edx", 2, [DX]>, DwarfRegNum<[-2, 2, 2]>; def ECX : X86Reg<"ecx", 1, [CX]>, DwarfRegNum<[-2, 1, 1]>; def EBX : X86Reg<"ebx", 3, [BX]>, DwarfRegNum<[-2, 3, 3]>; def ESI : X86Reg<"esi", 6, [SI]>, DwarfRegNum<[-2, 6, 6]>; def EDI : X86Reg<"edi", 7, [DI]>, DwarfRegNum<[-2, 7, 7]>; def EBP : X86Reg<"ebp", 5, [BP]>, DwarfRegNum<[-2, 4, 5]>; def ESP : X86Reg<"esp", 4, [SP]>, DwarfRegNum<[-2, 5, 4]>; def EIP : X86Reg<"eip", 0, [IP]>, DwarfRegNum<[-2, 8, 8]>; …
-
要更改可用的物理寄存器数量,请转到
TargetRegisterInfo.td
文件,并手动注释掉一些寄存器,这些寄存器是RegisterClass
的最后一个参数。打开X86RegisterInfo.cpp
文件并删除寄存器AH
、CH
和DH
:def GR8 : RegisterClass<"X86", [i8], 8, (add AL, CL, DL, AH, CH, DH, BL, BH, SIL, DIL, BPL, SPL, R8B, R9B, R10B, R11B, R14B, R15B, R12B, R13B)> {
-
当您构建 LLVM 时,第一步中的
.inc
文件将已更改,并且不会包含AH
、CH
和DH
寄存器。 -
使用之前配方中的测试用例,分析活动区间,其中我们执行了活动区间分析,并运行 LLVM 提供的寄存器分配技术,即
fast
、basic
、greedy
和pbqp
。让我们运行其中两个并比较结果:$ llc –regalloc=basic interval.ll –o intervalregbasic.s
接下来,创建
intervalregbasic.s
文件,如下所示:$ cat intervalregbasic.s .text .file "interval.ll" .globl donothing .align 16, 0x90 .type donothing,@function donothing: # @donothing # BB#0: movl %edi, -4(%rsp) retq .Lfunc_end0: .size donothing, .Lfunc_end0-donothing .globl func .align 16, 0x90 .type func,@function func: # @func # BB#0: subq $24, %rsp movl %edi, 20(%rsp) movl $5, 16(%rsp) movl $5, %edi callq donothing movl 16(%rsp), %edi movl %edi, 12(%rsp) callq donothing movl $9, 16(%rsp) cmpl $4, 20(%rsp) jg .LBB1_2 # BB#1: movl $3, 8(%rsp) movl $3, %edi callq donothing movl 8(%rsp), %edi movl %edi, 4(%rsp) jmp .LBB1_3 .LBB1_2: movl 16(%rsp), %edi movl %edi, (%rsp) .LBB1_3: callq donothing movl 12(%rsp), %eax addq $24, %rsp retq .Lfunc_end1: .size func, .Lfunc_end1-func
接下来,运行以下命令以比较两个文件:
$ llc –regalloc=pbqp interval.ll –o intervalregpbqp.s
创建
intervalregbqp.s
文件:$cat intervalregpbqp.s .text .file "interval.ll" .globl donothing .align 16, 0x90 .type donothing,@function donothing: # @donothing # BB#0: movl %edi, %eax movl %eax, -4(%rsp) retq .Lfunc_end0: .size donothing, .Lfunc_end0-donothing .globl func .align 16, 0x90 .type func,@function func: # @func # BB#0: subq $24, %rsp movl %edi, %eax movl %eax, 20(%rsp) movl $5, 16(%rsp) movl $5, %edi callq donothing movl 16(%rsp), %eax movl %eax, 12(%rsp) movl %eax, %edi callq donothing movl $9, 16(%rsp) cmpl $4, 20(%rsp) jg .LBB1_2 # BB#1: movl $3, 8(%rsp) movl $3, %edi callq donothing movl 8(%rsp), %eax movl %eax, 4(%rsp) jmp .LBB1_3 .LBB1_2: movl 16(%rsp), %eax movl %eax, (%rsp) .LBB1_3: movl %eax, %edi callq donothing movl 12(%rsp), %eax addq $24, %rsp retq .Lfunc_end1: .size func, .Lfunc_end1-func
-
现在,使用
diff
工具并逐行比较两个汇编代码。
它是如何工作的…
虚拟寄存器到物理寄存器的映射可以有两种方式:
-
直接映射:通过使用
TargetRegisterInfo
和MachineOperand
类。这取决于开发者,开发者需要提供加载和存储指令应插入的位置,以便在内存中获取和存储值。 -
间接映射:这依赖于
VirtRegMap
类来插入加载和存储操作,以及从内存中获取和设置值。使用VirtRegMap::assignVirt2Phys(vreg, preg)
函数将虚拟寄存器映射到物理寄存器上。
寄存器分配器扮演的另一个重要角色是在 SSA 形式解构中。由于传统的指令集不支持phi
指令,我们必须用其他指令替换它以生成机器代码。传统的方法是将phi
指令替换为copy
指令。
在此阶段之后,我们进行实际的物理寄存器映射。LLVM 中有四种寄存器分配的实现,它们各自有将虚拟寄存器映射到物理寄存器的算法。这里无法详细涵盖任何这些算法。如果您想尝试理解它们,请参考下一节。
参见
-
要了解更多关于 LLVM 中使用的算法的信息,请查看位于
lib/CodeGen/
的源代码:-
lib/CodeGen/RegAllocBasic.cpp
-
lib/CodeGen/ RegAllocFast.cpp
-
lib/CodeGen/ RegAllocGreedy.cpp
-
lib/CodeGen/ RegAllocPBQP.cpp
-
插入序言-尾声代码
插入序言-尾声代码涉及堆栈展开、最终确定函数布局、保存调用者保留寄存器以及生成序言和尾声代码。它还将抽象帧索引替换为适当的引用。此阶段在寄存器分配阶段之后运行。
如何做…
在PrologueEpilogueInserter
类中定义的骨架和重要函数如下:
-
序言-尾随代码插入传递在机器函数上运行,因此它继承了
MachineFunctionPass
类。它的构造函数初始化传递:class PEI : public MachineFunctionPass { public: static char ID; PEI() : MachineFunctionPass(ID) { initializePEIPass(*PassRegistry::getPassRegistry()); }
-
在这个类中定义了各种辅助函数,这些函数有助于插入序言和尾随代码:
void calculateSets(MachineFunction &Fn); void calculateCallsInformation(MachineFunction &Fn); void calculateCalleeSavedRegisters(MachineFunction &Fn); void insertCSRSpillsAndRestores(MachineFunction &Fn); void calculateFrameObjectOffsets(MachineFunction &Fn); void replaceFrameIndices(MachineFunction &Fn); void replaceFrameIndices(MachineBasicBlock *BB, MachineFunction &Fn, int &SPAdj); void scavengeFrameVirtualRegs(MachineFunction &Fn);
-
主要函数
insertPrologEpilogCode()
的任务是插入序言和尾随代码:void insertPrologEpilogCode(MachineFunction &Fn);
-
在这个过程中首先执行的是
runOnFunction()
函数。代码中的注释显示了执行的各种操作,例如计算调用帧大小、调整栈变量、为修改过的寄存器插入被调用者保存的寄存器的溢出代码、计算实际的帧偏移、为函数插入序言和尾随代码、将抽象帧索引替换为实际偏移量,等等:bool PEI::runOnMachineFunction(MachineFunction &Fn) { const Function* F = Fn.getFunction(); const TargetRegisterInfo *TRI = Fn.getSubtarget().getRegisterInfo(); const TargetFrameLowering *TFI = Fn.getSubtarget().getFrameLowering(); assert(!Fn.getRegInfo().getNumVirtRegs() && "Regalloc must assign all vregs"); RS = TRI->requiresRegisterScavenging(Fn) ? new RegScavenger() : nullptr; FrameIndexVirtualScavenging = TRI->requiresFrameIndexScavenging(Fn); // Calculate the MaxCallFrameSize and AdjustsStack variables for the // function's frame information. Also eliminates call frame pseudo // instructions. calculateCallsInformation(Fn); // Allow the target machine to make some adjustments to the function // e.g. UsedPhysRegs before calculateCalleeSavedRegisters. TFI->processFunctionBeforeCalleeSavedScan(Fn, RS); // Scan the function for modified callee saved registers and insert spill code // for any callee saved registers that are modified. calculateCalleeSavedRegisters(Fn); // Determine placement of CSR spill/restore code: // place all spills in the entry block, all restores in return blocks. calculateSets(Fn); // Add the code to save and restore the callee saved registers if (!F->hasFnAttribute(Attribute::Naked)) insertCSRSpillsAndRestores(Fn); // Allow the target machine to make final modifications to the function // before the frame layout is finalized. TFI->processFunctionBeforeFrameFinalized(Fn, RS); // Calculate actual frame offsets for all abstract stack objects... calculateFrameObjectOffsets(Fn); // Add prolog and epilog code to the function. This function is required // to align the stack frame as necessary for any stack variables or // called functions. Because of this, calculateCalleeSavedRegisters() // must be called before this function in order to set the AdjustsStack // and MaxCallFrameSize variables. if (!F->hasFnAttribute(Attribute::Naked)) insertPrologEpilogCode(Fn); // Replace all MO_FrameIndex operands with physical register references // and actual offsets. replaceFrameIndices(Fn); // If register scavenging is needed, as we've enabled doing it as a // post-pass, scavenge the virtual registers that frame index elimination // inserted. if (TRI->requiresRegisterScavenging(Fn) && FrameIndexVirtualScavenging) scavengeFrameVirtualRegs(Fn); // Clear any vregs created by virtual scavenging. Fn.getRegInfo().clearVirtRegs(); // Warn on stack size when we exceeds the given limit. MachineFrameInfo *MFI = Fn.getFrameInfo(); uint64_t StackSize = MFI->getStackSize(); if (WarnStackSize.getNumOccurrences() > 0 && WarnStackSize < StackSize) { DiagnosticInfoStackSize DiagStackSize(*F, StackSize); F->getContext().diagnose(DiagStackSize); } delete RS; ReturnBlocks.clear(); return true; }
-
插入序言-尾随代码的主要函数是
insertPrologEpilogCode()
函数。这个函数首先获取TargetFrameLowering
对象,然后为该函数生成相应的目标序言代码。之后,对于该函数中的每个基本块,它检查是否存在返回语句。如果有返回语句,则生成该函数的尾随代码:void PEI::insertPrologEpilogCode(MachineFunction &Fn) { const TargetFrameLowering &TFI = *Fn.getSubtarget().getFrameLowering(); // Add prologue to the function. TFI.emitPrologue(Fn); // Add epilogue to restore the callee-save registers in each exiting block for (MachineFunction::iterator I = Fn.begin(), E = Fn.end(); I != E; ++I) { // If last instruction is a return instruction, add an epilogue if (!I->empty() && I->back().isReturn()) TFI.emitEpilogue(Fn, *I); } // Emit additional code that is required to support segmented stacks, if // we've been asked for it. This, when linked with a runtime with support // for segmented stacks (libgcc is one), will result in allocating stack // space in small chunks instead of one large contiguous block. if (Fn.shouldSplitStack()) TFI.adjustForSegmentedStacks(Fn); // Emit additional code that is required to explicitly handle the stack in // HiPE native code (if needed) when loaded in the Erlang/OTP runtime. The // approach is rather similar to that of Segmented Stacks, but it uses a // different conditional check and another BIF for allocating more stack // space. if (Fn.getFunction()->getCallingConv() == CallingConv::HiPE) TFI.adjustForHiPEPrologue(Fn); }
它是如何工作的…
之前的代码调用了 TargetFrameLowering
类中的 emitEpilogue()
和 emitPrologue()
函数,这些将在后续章节中讨论的目标特定帧降低食谱中进行讨论。
代码发射
代码发射阶段将代码从代码生成抽象(如 MachineFunction
类、MachineInstr
类等)降低到机器代码层抽象(如 MCInst
类、MCStreamer
类等)。这个阶段的重要类是目标无关的 AsmPrinter
类、AsmPrinter
的特定目标子类和 TargetLoweringObjectFile
类。
MC 层负责生成包含标签、指令和指令的对象文件;而 CodeGen
层由 MachineFunctions
、MachineBasicBlock
和 MachineInstructions
组成。在这个时候使用的一个关键类是 MCStreamer
类,它由汇编器 API 组成。MCStreamer
类具有 EmitLabel
、EmitSymbolAttribute
、SwitchSection
等函数,这些函数直接对应于上述汇编级指令。
为了在目标上发射代码,需要实现四个重要的事情:
-
为目标定义
AsmPrinter
类的子类。这个类实现了通用降低过程,将MachineFunctions
函数转换为 MC 标签构造。AsmPrinter
基类方法和例程有助于实现特定目标的AsmPrinter
类。TargetLoweringObjectFile
类实现了ELF
、COFF
或MachO
目标的大部分公共逻辑。 -
为目标实现一个指令打印器。指令打印器接受一个
MCInst
类并将其渲染为raw_ostream
类的文本。大部分内容都是自动从.td
文件生成的(当你指定指令中的$dst
、$src1
、$src2
等时),但你需要实现打印操作数的例程。 -
实现将
MachineInstr
类降低到MCInst
类的代码,通常在<target>MCInstLower.cpp
中实现。这个降低过程通常是目标特定的,负责将跳转表条目、常量池索引、全局变量地址等转换为适当的MCLabels
。指令打印器或编码器会接收生成的MCInsts
。 -
实现一个
MCCodeEmitter
子类,将MCInsts
降低到机器代码字节和重定位。如果你想要支持直接.o
文件发射,或者想要为你的目标实现一个汇编器,这是很重要的。
如何做…
让我们访问lib/CodeGen/AsmPrinter/AsmPrinter.cpp
文件中的AsmPrinter
基类的一些重要函数:
-
EmitLinkage()
: 这会发射给定变量或函数的链接:void AsmPrinter::EmitLinkage(const GlobalValue *GV, MCSymbol *GVSym) const ;
-
EmitGlobalVariable()
: 这会将指定的全局变量发射到.s
文件:void AsmPrinter::EmitGlobalVariable(const GlobalVariable *GV);
-
EmitFunctionHeader()
: 这会发射当前函数的头部:void AsmPrinter::EmitFunctionHeader();
-
EmitFunctionBody()
: 这个方法会发射函数的主体和尾部:void AsmPrinter::EmitFunctionBody();
-
EmitJumpTableInfo()
: 这会将当前函数使用的跳转表的汇编表示打印到当前输出流:void AsmPrinter::EmitJumpTableInfo();
-
EmitJumpTableEntry()
: 这会在当前流中为指定的MachineBasicBlock
类发射一个跳转表条目:void AsmPrinter::EmitJumpTableEntry(const MachineJumpTableInfo *MJTI, const MachineBasicBlock *MBB, unsigned UID) const;
-
发射 8 位、16 位或 32 位大小的整型类型:
void AsmPrinter::EmitInt8(int Value) const { OutStreamer.EmitIntValue(Value, 1); } void AsmPrinter::EmitInt16(int Value) const { OutStreamer.EmitIntValue(Value, 2); } void AsmPrinter::EmitInt32(int Value) const { OutStreamer.EmitIntValue(Value, 4); }
对于代码发射的详细实现,请参阅lib/CodeGen/AsmPrinter/AsmPrinter.cpp
文件。需要注意的是,这个类使用OutStreamer
类对象来输出汇编指令。目标特定的代码发射细节将在后面的章节中介绍。
尾调用优化
在这个菜谱中,我们将看到如何在 LLVM 中实现尾调用优化。尾调用优化是一种技术,其中被调用者重用调用者的栈而不是在调用栈中添加一个新的栈帧,因此节省了栈空间和返回次数,尤其是在处理相互递归函数时。
准备就绪
我们需要确保以下事项:
-
llc
工具必须安装到$PATH
-
必须启用
tailcallopt
选项 -
测试代码必须包含尾调用
如何做…
-
编写检查尾调用优化的测试代码:
$ cat tailcall.ll declare fastcc i32 @tailcallee(i32 inreg %a1, i32 inreg %a2, i32 %a3, i32 %a4) define fastcc i32 @tailcaller(i32 %in1, i32 %in2) { %l1 = add i32 %in1, %in2 %tmp = tail call fastcc i32 @tailcallee(i32 inreg %in1, i32 inreg %in2, i32 %in1, i32 %l1) ret i32 %tmp }
-
在测试代码上运行带有
-tailcallopt
选项的llc
工具,以生成带有尾调用优化代码的汇编文件:$ llc -tailcallopt tailcall.ll
-
显示生成的输出:
$ cat tailcall.s .text .file "tailcall.ll" .globl tailcaller .align 16, 0x90 .type tailcaller,@function tailcaller: # @tailcaller .cfi_startproc # BB#0: pushq %rax .Ltmp0: .cfi_def_cfa_offset 16 # kill: ESI<def> ESI<kill> RSI<def> # kill: EDI<def> EDI<kill> RDI<def> leal (%rdi,%rsi), %ecx # kill: ESI<def> ESI<kill> RSI<kill> movl %edi, %edx popq %rax jmp tailcallee # TAILCALL .Lfunc_end0: .size tailcaller, .Lfunc_end0-tailcaller .cfi_endproc .section ".note.GNU-stack","",@progbits
-
使用
llc
工具再次生成汇编,但不使用-tailcallopt
选项:$ llc tailcall.ll -o tailcall1.s
-
使用
cat
命令显示输出:$ cat tailcall1.s .text .file "tailcall.ll" .globl tailcaller .align 16, 0x90 .type tailcaller,@function tailcaller: # @tailcaller .cfi_startproc # BB#0: # kill: ESI<def> ESI<kill> RSI<def> # kill: EDI<def> EDI<kill> RDI<def> leal (%rdi,%rsi), %ecx # kill: ESI<def> ESI<kill> RSI<kill> movl %edi, %edx jmp tailcallee # TAILCALL .Lfunc_end0: .size tailcaller, .Lfunc_end0-tailcaller .cfi_endproc .section ".note.GNU-stack","",@progbits
使用 diff 工具比较两个汇编文件。这里我们使用了 meld 工具:
它是如何工作的...
尾调用优化是一种编译器优化技术,编译器可以使用它来调用一个函数而不占用额外的栈空间;我们不需要为这个函数调用创建一个新的栈帧。如果函数中最后执行的指令是调用另一个函数,就会发生这种情况。需要注意的是,调用函数现在不需要栈空间;它只是调用一个函数(另一个函数或自身)并返回被调用函数将返回的任何值。这种优化可以使递归调用占用恒定和有限的空间。在这个优化中,代码可能不一定总是以尾调用可能的形式存在。它尝试并修改源代码以查看是否可以进行尾调用。
在前面的测试用例中,我们可以看到由于尾调用优化,增加了一条推入和弹出指令。在 LLVM 中,尾调用优化由架构特定的ISelLowering.cpp
文件处理;对于 x86,是X86ISelLowering.cpp
文件:
The code in function SDValue X86TargetLowering::LowerCall (…….)
bool IsMustTail = CLI.CS && CLI.CS->isMustTailCall();
if (IsMustTail) {
// Force this to be a tail call. The verifier rules are enough to ensure
// that we can lower this successfully without moving the return address
// around.
isTailCall = true;
} else if (isTailCall) {
// Check if it's really possible to do a tail call.
isTailCall = IsEligibleForTailCallOptimization(Callee, CallConv,
isVarArg, SR != NotStructReturn,
MF.getFunction()->hasStructRetAttr(), CLI.RetTy,
Outs, OutVals, Ins, DAG);
上述代码用于在传递tailcallopt
标志时调用IsEligibleForTailCallOptimization()
函数。IsEligibleForTailCallOptimization()
函数决定这段代码是否适合尾调用优化。如果是,则代码生成器将进行必要的更改。
兄弟调用优化
在这个菜谱中,我们将看到在 LLVM 中兄弟调用优化是如何工作的。可以将兄弟调用优化看作是优化过的尾调用,唯一的约束是函数应该具有相似的功能签名,即匹配的返回类型和匹配的函数参数。
准备工作
编写一个用于兄弟调用优化的测试用例,确保调用者和被调用者具有相同的调用约定(无论是 C 还是fastcc),并且尾位置的调用是尾调用:
$ cat sibcall.ll
declare i32 @bar(i32, i32)
define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
%0 = tail call i32 @bar(i32 %a, i32 %b)
ret i32 %0
}
如何实现...
-
运行
llc
工具生成汇编:$ llc sibcall.ll
-
使用
cat
命令查看生成的汇编:$ cat sibcall.s .text .file "sibcall.ll" .globl foo .align 16, 0x90 .type foo,@function foo: # @foo .cfi_startproc # BB#0: # %entry jmp bar # TAILCALL .Lfunc_end0: .size foo, .Lfunc_end0-foo .cfi_endproc .section ".note.GNU-stack","",@progbits
它是如何工作的...
兄弟调用优化是尾调用优化的一种受限版本,可以在不传递tailcallopt
选项的情况下对尾调用进行优化。兄弟调用优化的工作方式与尾调用优化类似,不同之处在于兄弟调用会自动检测,并且不需要任何 ABI 更改。在函数签名中需要的相似性是因为当调用函数(调用尾递归函数的函数)试图清理被调用函数的参数时,在被调用函数完成其工作之后,如果被调用函数超出参数空间以执行对需要更多栈空间参数的函数的兄弟调用,这可能会导致内存泄漏。
第八章:编写 LLVM 后端
在本章中,我们将介绍以下内容:
-
定义寄存器和寄存器集
-
定义调用约定
-
定义指令集
-
实现帧降低
-
打印指令
-
选择指令
-
添加指令编码
-
支持子目标
-
降低到多个指令
-
注册目标
简介
编译器的最终目标是生成目标代码,或者可以转换为对象代码并在实际硬件上执行的汇编代码。为了生成汇编代码,编译器需要了解目标机器架构的各个方面——寄存器、指令集、调用约定、流水线等。在这个阶段还可以进行许多优化。
LLVM 有自己定义目标机器的方式。它使用 tablegen
来指定目标寄存器、指令、调用约定等。tablegen
函数简化了我们以编程方式描述大量架构属性的方式。
LLVM 的后端具有流水线结构,指令会经过类似这样的阶段;从 LLVM IR 到 SelectionDAG
,然后到 MachineDAG
,接着到 MachineInstr
,最后到 MCInst
。
IR 转换为 SelectionDAG(DAG 代表 有向无环图)。然后进行 SelectionDAG 合法化,将非法指令映射到目标机器允许的合法操作上。在此阶段之后,SelectionDAG 转换为 MachineDAG,这基本上是后端支持的指令选择。
CPU 执行一系列线性指令。调度步骤的目标是通过为操作分配顺序来线性化 DAG。LLVM 的代码生成器采用巧妙的启发式方法(如寄存器压力降低)来尝试生成一个能够产生更快速代码的调度。寄存器分配策略在生成更好的 LLVM 代码中也起着重要作用。
本章介绍了如何从头开始构建一个 LLVM 玩具后端。到本章结束时,我们将能够为示例玩具后端生成汇编代码。
示例后端
本章考虑的示例后端是一个简单的 RISC 类型架构,有少量寄存器(例如 r0-r3),一个栈指针(sp)和一个链接寄存器(lr),用于存储返回地址。
这个玩具后端的调用约定类似于 ARM 架构——传递给函数的参数将存储在寄存器集 r0-r1 中,返回值将存储在 r0 中。
定义寄存器和寄存器集
这个菜谱展示了如何在 .td
文件中定义寄存器和寄存器集。tablegen
函数将这个 .td
文件转换为 .inc
文件,这些文件将成为我们 .cpp
文件中的 #include
声明,并引用寄存器。
准备工作
我们已经定义了我们的玩具目标机器具有四个寄存器(r0-r3)、一个堆栈寄存器(sp)和一个链接寄存器(lr)。这些可以在TOYRegisterInfo.td
文件中指定。tablegen
函数提供了Register
类,可以扩展以指定寄存器。
如何做到这一点…
要使用目标描述文件定义后端架构,请按照以下步骤进行。
-
在
lib/Target
目录下创建一个名为TOY
的新文件夹:$ mkdir llvm_root_directory/lib/Target/TOY
-
在新创建的
TOY
文件夹中创建一个名为TOYRegisterInfo.td
的新文件:$ cd llvm_root_directory/lib/Target/TOY $ vi 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)>;
它是如何工作的…
tablegen
函数处理这个.td
文件以生成.inc
文件,该文件通常为这些寄存器生成枚举。这些枚举可以在.cpp
文件中使用,其中寄存器可以引用为TOY::R0
。这些.inc
文件将在我们构建 LLVM 项目时生成。
参见
- 要获取更多关于如何为更高级的架构(如 ARM)定义寄存器的详细信息,请参考 LLVM 源代码中的
lib/Target/ARM/ARMRegisterInfo.td
文件。
定义调用约定
调用约定指定了值是如何在函数调用之间传递的。我们的 TOY 架构指定了两个参数将通过两个寄存器(r0 和 r1)传递,其余的将通过堆栈传递。这个配方展示了如何定义调用约定,该约定将通过函数指针在ISelLowering
(第六章中讨论的指令选择降低阶段,目标无关代码生成器)中使用。
调用约定将在TOYCallingConv.td
文件中定义,该文件将主要包含两个部分——一个用于定义返回值约定,另一个用于定义参数传递约定。返回值约定指定了返回值将驻留在何处以及哪些寄存器中。参数传递约定将指定传递的参数将驻留在何处以及哪些寄存器中。在定义玩具架构的调用约定时,将继承CallingConv
类。
如何做到这一点…
要实现调用约定,请按照以下步骤进行:
-
在
lib/Target/TOY
文件夹中创建一个名为TOYCallingConv.td
的新文件:$ vi 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>> ]>;
-
定义调用者保存的寄存器集:
def CC_Save : CalleeSavedRegs<(add R2, R3)>;
它是如何工作的…
在你刚才阅读的.td
文件中,已经指定了 32 位整型的返回值存储在 r0 寄存器中。每当向函数传递参数时,前两个参数将存储在 r0 和 r1 寄存器中。还指定了每当遇到任何数据类型,如 8 位或 16 位的整数时,它将被提升为 32 位整数类型。
tablegen
函数生成一个TOYCallingConv.inc
文件,该文件将在TOYISelLowering.cpp
文件中引用。用于定义参数处理的两个目标hook
函数是LowerFormalArguments()
和LowerReturn()
。
参见
- 要查看高级架构(如 ARM)的详细实现,请查看
lib/Target/ARM/ARMCallingConv.td
文件
定义指令集
架构的指令集根据架构中存在的各种特征而变化。本食谱演示了如何为目标架构定义指令集。
指令目标描述文件中定义了三个内容:操作数、汇编字符串和指令模式。规范包含定义或输出的列表,以及使用或输入的列表。可以有不同类型的操作数类,例如Register
类,以及立即数和更复杂的register + imm
操作数。
在这里,演示了一个简单的加法指令定义,它接受两个寄存器作为操作数。
如何操作...
要使用目标描述文件定义指令集,请按照以下步骤进行。
-
在
lib/Target/TOY
文件夹中创建一个名为TOYInstrInfo.td
的新文件:$ vi TOYInstrInfo.td
-
指定两个寄存器操作数之间
add
指令的操作数、汇编字符串和指令模式:def ADDrr : InstTOY<(outs GRRegs:$dst), (ins GRRegs:$src1, GRRegs:$src2), "add $dst, $src1,z$src2", [(set i32:$dst, (add i32:$src1, i32:$src2))]>;
它是如何工作的…
add
寄存器到寄存器指令指定$dst
作为结果操作数,它属于General Register
类型类;输入$src1
和$src2
作为两个输入操作数,它们也属于General Register
类型类;指令汇编字符串为 32 位整型的"add $dst, $src1, $src2"
。
因此,将生成两个寄存器之间add
操作的汇编代码,如下所示:
add r0, r0, r1
上述代码指示将 r0 和 r1 寄存器的内容相加,并将结果存储在 r0 寄存器中。
参见
- 许多指令将具有相同类型的指令模式——例如
add
、sub
等 ALU 指令。在这种情况下,可以使用多类来定义公共属性。有关高级架构(如 ARM)的各种指令集的更详细信息,请参阅lib/Target/ARM/ARMInstrInfo.td
文件
实现帧降低
本食谱讨论了目标架构的帧降低。帧降低涉及函数调用的前缀和后缀的生成。
准备工作
注意
需要定义两个用于帧降低的函数,即TOYFrameLowering::emitPrologue()
和TOYFrameLowering::emitEpilogue()
。
如何操作…
以下函数定义在lib/Target/TOY
文件夹中的TOYFrameLowering.cpp
文件中:
-
emitPrologue
函数可以定义如下:void TOYFrameLowering::emitPrologue(MachineFunction &MF) const { const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo(); MachineBasicBlock &MBB = MF.front(); MachineBasicBlock::iterator MBBI = MBB.begin(); DebugLoc dl = MBBI != MBB.end() ? MBBI->getDebugLoc() : DebugLoc(); 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); } }
-
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); } }
-
以下是一些用于确定
ADD
栈操作偏移量的辅助函数:static unsigned materializeOffset(MachineFunction &MF, MachineBasicBlock &MBB, MachineBasicBlock::iterator MBBI, unsigned Offset) { const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo(); DebugLoc dl = MBBI != MBB.end() ? MBBI->getDebugLoc() : DebugLoc(); const uint64_t MaxSubImm = 0xfff; if (Offset <= MaxSubImm) { return 0; } else { unsigned OffsetReg = TOY::R2; unsigned OffsetLo = (unsigned)(Offset & 0xffff); unsigned OffsetHi = (unsigned)((Offset & 0xffff0000) >> 16); BuildMI(MBB, MBBI, dl, TII.get(TOY::MOVLOi16), OffsetReg) .addImm(OffsetLo) .setMIFlag(MachineInstr::FrameSetup); if (OffsetHi) { BuildMI(MBB, MBBI, dl, TII.get(TOY::MOVHIi16), OffsetReg) .addReg(OffsetReg) .addImm(OffsetHi) .setMIFlag(MachineInstr::FrameSetup); } return OffsetReg; } }
-
以下是一些用于计算栈大小的辅助函数:
uint64_t TOYFrameLowering::computeStackSize(MachineFunction &MF) const { MachineFrameInfo *MFI = MF.getFrameInfo(); uint64_t StackSize = MFI->getStackSize(); unsigned StackAlign = getStackAlignment(); if (StackAlign > 0) { StackSize = RoundUpToAlignment(StackSize, StackAlign); } return StackSize; }
它是如何工作的…
emitPrologue
函数首先计算栈大小以确定是否需要使用前导代码。然后通过计算偏移量来调整栈指针。对于后导代码,它首先检查是否需要后导代码。然后恢复栈指针到函数开始时的状态。
例如,考虑以下输入 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
参见
- 对于高级架构框架降低,例如在 ARM 中,请参考
lib/Target/ARM/ARMFrameLowering.cpp
文件。
打印指令
打印汇编指令是生成目标代码的重要步骤。定义了各种类,作为流式传输的网关。指令字符串由之前定义的 .td
文件提供。
准备工作
打印指令的第一步是在 .td
文件中定义指令字符串,这在 定义指令集 菜谱中已完成。
如何操作…
执行以下步骤:
-
在
TOY
文件夹内创建一个名为InstPrinter
的新文件夹:$ cd lib/Target/TOY $ mkdir InstPrinter
-
在一个新文件中,称为
TOYInstrFormats.td
,定义AsmString
变量: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
的新文件,并定义printOperand
函数,如下所示: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); }
-
它还要求指定
MCASMinfo
以打印指令。这可以通过定义TOYMCAsmInfo.h
和TOYMCAsmInfo.cpp
文件来完成。TOYMCAsmInfo.h
文件可以定义如下:#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 架构的汇编代码。
例如,以下 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
选择指令
DAG 中的 IR 指令需要降低到特定目标的指令。SDAG 节点包含 IR,需要映射到机器特定的 DAG 节点。选择阶段的输出已准备好进行调度。
准备工作
-
为了选择特定机器的指令,需要定义一个单独的类,
TOYDAGToDAGISel
。要编译包含此类定义的文件,请将文件名添加到TOY
文件夹中的CMakeLists.txt
文件中:$ vi CMakeLists .txt add_llvm_target(... ... TOYISelDAGToDAG.cpp ... )
-
需要在
TOYTargetMachine.h
和TOYTargetMachine.cpp
文件中添加一个遍历入口:$ vi TOYTargetMachine.h const TOYInstrInfo *getInstrInfo() const override { return getSubtargetImpl()->getInstrInfo(); }
-
TOYTargetMachine.cpp
中的以下代码将在指令选择阶段创建一个遍历:class TOYPassConfig : public TargetPassConfig { public: ... virtual bool addInstSelector(); }; ... bool TOYPassConfig::addInstSelector() { addPass(createTOYISelDag(getTOYTargetMachine())); return false; }
如何操作…
要定义一个指令选择函数,请按照以下步骤进行:
-
创建一个名为
TOYISelDAGToDAG.cpp
的文件:$ vi TOYISelDAGToDAG.cpp
-
包含以下文件:
#include "TOY.h" #include "TOYTargetMachine.h" #include "llvm/CodeGen/SelectionDAGISel.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/Debug.h" #include "TOYInstrInfo.h"
-
定义一个名为
TOYDAGToDAGISel
的新类,如下所示,它将继承自SelectionDAGISel
类:class TOYDAGToDAGISel : public SelectionDAGISel { const TOYSubtarget &Subtarget; public: explicit TOYDAGToDAGISel(TOYTargetMachine &TM, CodeGenOpt::Level OptLevel) : SelectionDAGISel(TM, OptLevel), Subtarget(*TM.getSubtargetImpl()) {} };
-
在这个类中需要定义的最重要函数是
Select()
,它将返回一个针对机器指令的特定SDNode
对象:在类中声明它:
SDNode *Select(SDNode *N);
进一步定义如下:
SDNode *TOYDAGToDAGISel::Select(SDNode *N) { return SelectCode(N); }
-
另一个重要的功能是用于定义地址选择函数,该函数将计算加载和存储操作的基础地址和偏移量。
如下所示声明它:
bool SelectAddr(SDValue Addr, SDValue &Base, SDValue &Offset);
如此定义它:
bool TOYDAGToDAGISel::SelectAddr(SDValue Addr, SDValue &Base, SDValue &Offset) { if (FrameIndexSDNode *FIN = dyn_cast<FrameIndexSDNode>(Addr)) { Base = CurDAG->getTargetFrameIndex(FIN->getIndex(), getTargetLowering()- >getPointerTy()); Offset = CurDAG->getTargetConstant(0, MVT::i32); return true; } if (Addr.getOpcode() == ISD::TargetExternalSymbol || Addr.getOpcode() == ISD::TargetGlobalAddress || Addr.getOpcode() == ISD::TargetGlobalTLSAddress) { return false; // direct calls. } Base = Addr; Offset = CurDAG->getTargetConstant(0, MVT::i32); return true; }
-
createTOYISelDag
转换将合法化的 DAG 转换为特定于玩具的 DAG,以便在同一文件中进行指令调度:FunctionPass *llvm::createTOYISelDag(TOYTargetMachine &TM, CodeGenOpt::Level OptLevel) { return new TOYDAGToDAGISel(TM, OptLevel); }
它是如何工作的…
TOYISelDAGToDAG.cpp
中的TOYDAGToDAGISel::Select()
函数用于选择 OP 代码 DAG 节点,而TOYDAGToDAGISel::SelectAddr()
用于选择具有addr
类型的 DATA DAG 节点。请注意,如果地址是全局或外部的,我们返回地址为 false,因为它的地址是在全局上下文中计算的。
参见
- 关于选择复杂架构(如 ARM)的机器指令 DAG 的详细信息,请查看 LLVM 源代码中的
lib/Target/ARM/ARMISelDAGToDAG.cpp
文件。
添加指令编码
如果指令需要根据它们相对于位字段的编码进行特定化,这可以通过在定义指令时在.td
文件中指定位字段来实现。
如何做到这一点…
在定义指令时包含指令编码,请按照以下步骤进行:
-
将用于注册
add
指令的寄存器操作数将有一些定义的指令编码。指令的大小为 32 位,其编码如下:bits 0 to 3 -> src2, second register operand bits 4 to 11 -> all zeros bits 12 to 15 -> dst, for destination register bits 16 to 19 -> src1, first register operand bit 20 -> zero bit 21 to 24 -> for opcode bit 25 to 27 -> all zeros bit 28 to 31 -> 1110
这可以通过在
.td
文件中指定前导位模式来实现。 -
在
TOYInstrFormats.td
文件中,定义一个新变量,称为Inst
:class InstTOY<dag outs, dag ins, string asmstr, list<dag> pattern> : Instruction { field bits<32> Inst; let Namespace = "TOY"; … … let AsmString = asmstr; … … }
-
在
TOYInstrInfo.td
文件中,定义一个指令编码:def ADDrr : InstTOY<(outs GRRegs:$dst),(ins GRRegs:$src1, GRRegs:$src2) ... > { bits<4> src1; bits<4> src2; bits<4> dst; let Inst{31-25} = 0b1100000; let Inst{24-21} = 0b1100; // Opcode let Inst{20} = 0b0; let Inst{19-16} = src1; // Operand 1 let Inst{15-12} = dst; // Destination let Inst{11-4} = 0b00000000; let Inst{3-0} = src2; }
-
在
TOY/MCTargetDesc
文件夹中,在TOYMCCodeEmitter.cpp
文件中,如果机器指令操作数是寄存器,将调用编码函数。unsigned TOYMCCodeEmitter::getMachineOpValue(const MCInst &MI, const MCOperand &MO, SmallVectorImpl<MCFixup> &Fixups, const MCSubtargetInfo &STI) const { if (MO.isReg()) { return CTX.getRegisterInfo()- >getEncodingValue(MO.getReg()); }
-
此外,在同一文件中,指定了一个用于编码指令的函数:
void TOYMCCodeEmitter::EncodeInstruction(const MCInst &MI, raw_ostream &OS, SmallVectorImpl<MCFixup> &Fixups, const MCSubtargetInfo &STI) const { const MCInstrDesc &Desc = MCII.get(MI.getOpcode()); if (Desc.getSize() != 4) { llvm_unreachable("Unexpected instruction size!"); } const uint32_t Binary = getBinaryCodeForInstr(MI, Fixups, STI); EmitConstant(Binary, Desc.getSize(), OS); ++MCNumEmitted; }
它是如何工作的…
在.td
文件中,已经指定了指令的编码——操作数、目的、标志条件和指令操作码的位。机器代码生成器通过从.td
文件生成.inc
文件并通过函数调用获取这些编码。它将这些指令编码并发出相同的指令打印。
参见
- 对于如 ARM 这样的复杂架构,请查看 LLVM 主分支
lib/Target/ARM
目录下的ARMInstrInfo.td
和ARMInstrInfo.td
文件。
支持子目标
目标可能有一个子目标——通常是一个带有指令的变体——处理操作数的方式,以及其他方式。这种子目标特性可以在 LLVM 后端得到支持。子目标可能包含一些额外的指令、寄存器、调度模型等。ARM 有如 NEON 和 THUMB 这样的子目标,而 x86 有如 SSE、AVX 这样的子目标特性。对于子目标特性,指令集是不同的,例如,ARM 的 NEON 和 SSE/AVX 支持向量指令的子目标特性。SSE 和 AVX 也支持向量指令集,但它们的指令各不相同。
如何做...
这个示例将演示如何在后端添加对支持子目标特性的支持。必须定义一个新的类,该类将继承TargetSubtargetInfo
类:
-
创建一个名为
TOYSubtarget.h
的新文件:$ vi TOYSubtarget.h
-
包含以下文件:
#include "TOY.h" #include "TOYFrameLowering.h" #include "TOYISelLowering.h" #include "TOYInstrInfo.h" #include "TOYSelectionDAGInfo.h" #include "TOYSubtarget.h" #include "llvm/Target/TargetMachine.h" #include "llvm/Target/TargetSubtargetInfo.h" #include "TOYGenSubtargetInfo.inc"
-
定义一个名为
TOYSubtarget
的新类,其中包含有关数据布局、目标降低、目标选择 DAG、目标帧降低等信息的一些私有成员:class TOYSubtarget : public TOYGenSubtargetInfo { virtual void anchor(); private: const DataLayout DL; // Calculates type size & alignment. TOYInstrInfo InstrInfo; TOYTargetLowering TLInfo; TOYSelectionDAGInfo TSInfo; TOYFrameLowering FrameLowering; InstrItineraryData InstrItins;
-
声明其构造函数:
TOYSubtarget(const std::string &TT, const std::string &CPU, const std::string &FS, TOYTargetMachine &TM);
这个构造函数初始化数据成员以匹配指定的三元组。
-
定义一些辅助函数以返回类特定的数据:
const InstrItineraryData *getInstrItineraryData() const override { return &InstrItins; } const TOYInstrInfo *getInstrInfo() const override { return &InstrInfo; } const TOYRegisterInfo *getRegisterInfo() const override { return &InstrInfo.getRegisterInfo(); } const TOYTargetLowering *getTargetLowering() const override { return &TLInfo; } const TOYFrameLowering *getFrameLowering() const override { return &FrameLowering; } const TOYSelectionDAGInfo *getSelectionDAGInfo() const override { return &TSInfo; } const DataLayout *getDataLayout() const override { return &DL; } void ParseSubtargetFeatures(StringRef CPU, StringRef FS); TO LC, Please maintain the representation of the above code EXACTLY as seen above.
-
创建一个名为
TOYSubtarget.cpp
的新文件,并按如下方式定义构造函数:TOYSubtarget::TOYSubtarget(const std::string &TT, const std::string &CPU, const std::string &FS, TOYTargetMachine &TM) DL("e-m:e-p:32:32-i1:8:32-i8:8:32-i16:16:32-i64:32- f64:32-a:0:32-n32"), InstrInfo(), TLInfo(TM), TSInfo(DL), FrameLowering() {}
子目标有自己的数据布局定义,以及其他信息,如帧降低、指令信息、子目标信息等。
参见
- 要深入了解子目标实现细节,请参考 LLVM 源代码中的
lib/Target/ARM/ARMSubtarget.cpp
文件
将代码降低到多个指令
让我们以实现一个 32 位立即数加载的高/低对为例,其中 MOVW 表示移动一个 16 位低立即数和一个清除的 16 位高位,而 MOVT 表示移动一个 16 位高立即数。
如何做...
实现这种多指令降低可能有多种方式。我们可以通过使用伪指令或在选择 DAG 到 DAG 阶段来完成。
-
要在不使用伪指令的情况下完成,定义一些约束。这两条指令必须按顺序执行。MOVW 清除了高 16 位。它的输出通过 MOVT 读取以填充高 16 位。这可以通过在 tablegen 中指定约束来实现:
def MOVLOi16 : MOV<0b1000, "movw", (ins i32imm:$imm), [(set i32:$dst, i32imm_lo:$imm)]>; def MOVHIi16 : MOV<0b1010, "movt", (ins GRRegs:$src1, i32imm:$imm), [/* No Pattern */]>;
第二种方式是在
.td
文件中定义伪指令:def MOVi32 : InstTOY<(outs GRRegs:$dst), (ins i32imm:$src), "", [(set i32:$dst, (movei32 imm:$src))]> { let isPseudo = 1; }
-
然后伪指令通过
TOYInstrInfo.cpp
文件中的目标函数降低:bool TOYInstrInfo::expandPostRAPseudo(MachineBasicBlock::iterato r MI) const { if (MI->getOpcode() == TOY::MOVi32){ DebugLoc DL = MI->getDebugLoc(); MachineBasicBlock &MBB = *MI->getParent(); const unsigned DstReg = MI->getOperand(0).getReg(); const bool DstIsDead = MI->getOperand(0).isDead(); const MachineOperand &MO = MI->getOperand(1); auto LO16 = BuildMI(MBB, MI, DL, get(TOY::MOVLOi16), DstReg); auto HI16 = BuildMI(MBB, MI, DL, get(TOY::MOVHIi16)) .addReg(DstReg, RegState::Define | getDeadRegState(DstIsDead)) .addReg(DstReg); MBB.erase(MI); return true; } }
-
编译整个 LLVM 项目:
例如,一个包含 IR 的
ex.ll
文件看起来像这样:define i32 @foo(i32 %a) #0 { %b = add nsw i32 %a, 65537 ; 0x00010001 ret i32 %b }
生成的汇编代码将如下所示:
movw r1, #1 movt r1, #1 add r0, r0, r1 b lr
它是如何工作的...
第一条指令,movw
,将移动低 16 位中的 1 并清除高 16 位。因此,第一条指令将在 r1 中写入0x00000001
。在下一条指令movt
中,将写入高 16 位。因此,在 r1 中,将写入0x0001XXXX
,而不会影响低位。最后,r1 寄存器中将包含0x00010001
。每当遇到.td
文件中指定的伪指令时,其展开函数将被调用以指定伪指令将展开成什么。
在前面的例子中,mov32
立即数将由两条指令实现:movw
(低 16 位)和movt
(高 16 位)。它在.td
文件中被标记为伪指令。当需要发出此伪指令时,其展开函数被调用,它构建两个机器指令:MOVLOi16
和MOVHIi16
。这些映射到目标架构的movw
和movt
指令。
参考也
- 要深入了解实现这种多指令降低的实现,请查看 LLVM 源代码中的 ARM 目标实现,在
lib/Target/ARM/ARMInstrInfo.td
文件中。
注册目标
对于在 TOY 目标架构中运行 llc 工具,它必须与 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, useDwarfDirectory, 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, createTOYMCSubtargetInfo); // 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
在这里,我们指定我们正在为 toy 目标构建 LLVM 编译器。构建完成后,检查是否可以通过llc
命令看到 TOY 目标:
$ llc –version
…
…
Registered Targets :
toy – TOY
参考也
- 对于关于涉及流水线和调度的复杂目标的更详细描述,请参考陈中舒和 Anoushe Jamshidi 所著的教程:为 Cpu0 架构创建 LLVM 后端中的章节。
第九章:使用 LLVM 进行各种有用的项目
在本章中,我们将介绍以下食谱:
-
LLVM 中的异常处理
-
使用 sanitizers
-
使用 LLVM 编写垃圾收集器
-
将 LLVM IR 转换为 JavaScript
-
使用 Clang 静态分析器
-
使用 bugpoint
-
使用 LLDB
-
使用 LLVM 工具传递
简介
到目前为止,你已经学习了如何编写编译器的前端,编写优化并创建后端。在本章,本书的最后一章,我们将探讨 LLVM 基础设施提供的一些其他功能以及我们如何在项目中使用它们。我们不会深入探讨本章主题的细节。主要目的是让你了解这些重要的工具和技术,它们是 LLVM 中的热点。
LLVM 中的异常处理
在这个食谱中,我们将探讨 LLVM 的异常处理基础设施。我们将讨论异常处理信息在 IR 中的外观以及 LLVM 为异常处理提供的内建函数。
准备就绪...
你必须理解异常处理是如何正常工作的,以及 try
、catch
和 throw
等概念。你还需要在路径中安装 Clang 和 LLVM。
如何做到这一点…
我们将通过一个例子来描述在 LLVM 中异常处理是如何工作的:
-
打开一个文件来写下源代码,并输入源代码以测试异常处理:
$ cat eh.cpp class Ex1 {}; void throw_exception(int a, int b) { Ex1 ex1; if (a > b) { throw ex1; } } int test_try_catch() { try { throw_exception(2, 1); } catch(...) { return 1; } return 0; }
-
使用以下命令生成位码文件:
$ clang -c eh.cpp -emit-llvm -o eh.bc
-
要在屏幕上查看 IR,请运行以下命令,它将给出如下所示的输出:
$ llvm-dis eh.bc -o - ; ModuleID = 'eh.bc' target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" %class.Ex1 = type { i8 } @_ZTVN10__cxxabiv117__class_type_infoE = external global i8* @_ZTS3Ex1 = linkonce_odr constant [5 x i8] c"3Ex1\00" @_ZTI3Ex1 = linkonce_odr constant { i8*, i8* } { i8* bitcast (i8** getelementptr inbounds (i8** @_ZTVN10__cxxabiv117__class_type_infoE, i64 2) to i8*), i8* getelementptr inbounds ([5 x i8]* @_ZTS3Ex1, i32 0, i32 0) } ; Function Attrs: uwtable define void @_Z15throw_exceptionii(i32 %a, i32 %b) #0 { %1 = alloca i32, align 4 %2 = alloca i32, align 4 %ex1 = alloca %class.Ex1, align 1 store i32 %a, i32* %1, align 4 store i32 %b, i32* %2, align 4 %3 = load i32* %1, align 4 %4 = load i32* %2, align 4 %5 = icmp sgt i32 %3, %4 br i1 %5, label %6, label %9 ; <label>:6 ; preds = %0 %7 = call i8* @__cxa_allocate_exception(i64 1) #1 %8 = bitcast i8* %7 to %class.Ex1* call void @__cxa_throw(i8* %7, i8* bitcast ({ i8*, i8* }* @_ZTI3Ex1 to i8*), i8* null) #2 unreachable ; <label>:9 ; preds = %0 ret void } declare i8* @__cxa_allocate_exception(i64) declare void @__cxa_throw(i8*, i8*, i8*) ; Function Attrs: uwtable define i32 @_Z14test_try_catchv() #0 { %1 = alloca i32, align 4 %2 = alloca i8* %3 = alloca i32 %4 = alloca i32 invoke void @_Z15throw_exceptionii(i32 2, i32 1) to label %5 unwind label %6 ; <label>:5 ; preds = %0 br label %13 ; <label>:6 ; preds = %0 %7 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) catch i8* null %8 = extractvalue { i8*, i32 } %7, 0 store i8* %8, i8** %2 %9 = extractvalue { i8*, i32 } %7, 1 store i32 %9, i32* %3 br label %10 ; <label>:10 ; preds = %6 %11 = load i8** %2 %12 = call i8* @__cxa_begin_catch(i8* %11) #1 store i32 1, i32* %1 store i32 1, i32* %4 call void @__cxa_end_catch() br label %14 ; <label>:13 ; preds = %5 store i32 0, i32* %1 br label %14 ; <label>:14 ; preds = %13, %10 %15 = load i32* %1 ret i32 %15 } declare i32 @__gxx_personality_v0(...) declare i8* @__cxa_begin_catch(i8*) declare void @__cxa_end_catch() attributes #0 = { 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" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #1 = { nounwind } attributes #2 = { noreturn } !llvm.ident = !{!0} !0 = metadata !{metadata !"clang version 3.6.0 (220636)"}
它是如何工作的…
在 LLVM 中,如果抛出异常,运行时会尽力找到一个处理程序。它会尝试找到与抛出异常的函数相对应的异常帧。这个异常帧包含对异常表的引用,该表包含实现——当编程语言支持异常处理时如何处理异常。当语言不支持异常处理时,如何展开当前激活并恢复先前激活的状态的信息就包含在这个异常帧中。
让我们看看前面的例子,看看如何使用 LLVM 生成异常处理代码。
try
块被翻译为调用 LLVM 的指令:
invoke void @_Z15throw_exceptionii(i32 2, i32 1)
to label %5 unwind label %6
前一行告诉编译器如果 throw_exception
函数抛出异常时应该如何处理异常。如果没有抛出异常,则正常执行将通过 %5
标签进行。但如果抛出异常,它将分支到 %6
标签,即着陆地。这大致对应于 try
/catch
序列中的 catch
部分。当执行在着陆地恢复时,它接收一个异常结构和与抛出异常类型相对应的选择器值。然后选择器被用来确定哪个 catch
函数应该实际处理异常。在这种情况下,它看起来像这样:
%7 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catch i8* null
上一段代码中的 %7
表示异常描述信息。代码中的 { i8*, i32 }
部分描述了信息的类型。代码中的 i8*
部分表示异常指针部分,而 i32
是选择器值。在这种情况下,我们只有一个选择器值,因为 catch
函数接受所有类型的异常对象。@__gxx_personality_v0
函数是 personality
函数。它接收异常的上下文,一个包含异常对象类型和值的异常结构,以及当前函数的异常表引用。当前编译单元的 personality
函数在公共异常帧中指定。在我们的例子中,@__gxx_personality_v0
函数表示我们正在处理 C++ 异常。
因此,%8 = extractvalue { i8*, i32 } %7, 0
将表示异常对象,而 %9 = extractvalue { i8*, i32 } %7, 1
表示选择器值。
以下是一些值得注意的 IR 函数:
-
__cxa_throw
:这是一个用于抛出异常的函数 -
__cxa_begin_catch
:这个函数接受一个异常结构引用作为参数,并返回异常对象的值 -
__cxa_end_catch
:这个函数定位最近捕获的异常,并减少其处理程序计数,如果这个计数器降到零,则从捕获状态中移除异常
相关内容
- 要了解 LLVM 使用的异常格式,请访问
llvm.org/docs/ExceptionHandling.html#llvm-code-generation
。
使用清理器
你可能已经使用过 Valgrind 等工具进行内存调试。LLVM 也为我们提供了内存调试的工具,如地址清理器、内存清理器等。与 Valgrind 相比,这些工具非常快速,尽管它们不如 Valgrind 成熟。大多数这些工具都处于实验阶段,所以如果你愿意,你可以为这些工具的开源开发做出贡献。
准备工作
要使用这些清理器,我们需要从 LLVM SVN 检出 compiler-rt
代码:
cd llvm/projects
svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
按照我们在 第一章 中所做的那样构建 LLVM,LLVM 设计和使用。通过这样做,我们得到所需的运行时库。
如何操作…
现在,我们将对测试代码进行地址清理器的测试。
-
编写一个测试用例来检查地址清理器:
$ cat asan.c int main() { int a[5]; int index = 6; int retval = a[index]; return retval; }
-
使用
fsanitize=address
命令行参数编译测试代码以使用地址清理器:$ clang -fsanitize=address asan.c
-
使用以下命令生成地址清理器的输出:
$ ASAN_SYMBOLIZER_PATH=/usr/local/bin/llvm-symbolizer ./a.out
这是输出:
它是如何工作的…
LLVM 地址清理器基于代码插桩的原则。该工具由一个编译器插桩模块和一个运行时库组成。代码插桩部分是通过 LLVM 的 pass 完成的,它通过传递fsanitize=address
命令行参数来运行,就像前面的例子中那样。运行时库用自定义代码替换代码中的malloc
和free
函数。在我们继续讨论代码插桩的细节之前,我们必须知道虚拟地址空间被分为两个不相交的类别:主应用程序内存,它被常规应用程序代码使用;以及影子内存,它包含影子值(或元数据)。
影子内存和主应用程序内存相互链接。在主内存中污染一个字节意味着将特殊值写入相应的影子内存。
让我们回到地址清理器的主题;由malloc
函数分配的区域周围的内存被污染。由free
函数释放的内存被置于隔离区,并且也被污染。程序中的每次内存访问都会被编译器以以下方式转换。
起初,它看起来是这样的:
*address = ...;
在转换后,它变成了以下内容:
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;
这意味着如果它发现对这块内存的任何无效访问,它会报告一个错误。
在前面的例子中,我们为缓冲区溢出编写了一段代码,访问了一个越界的数组。在这里,代码的仪器是在数组的正前负后地址上进行的。因此,当我们访问数组超出其上界时,我们尝试访问红色区域。因此,地址清理器给我们提供了堆栈缓冲区溢出报告。
参见以下内容…
-
您可以查看
clang.llvm.org/docs/AddressSanitizer.html
上的文档页面以获取更多信息。 -
您还可以通过以下链接查看 LLVM 中的其他清理器:
clang.llvm.org/docs/MemorySanitizer.html
使用 LLVM 编写垃圾回收器
垃圾回收是一种内存管理技术,其中回收器试图回收不再使用的对象占用的内存。这使程序员免于跟踪堆对象的生存期。
在这个菜谱中,我们将了解如何将 LLVM 集成到支持垃圾回收的语言的编译器中。LLVM 本身不提供垃圾回收器,但提供了一个框架来描述垃圾回收器对编译器的要求。
准备工作
必须构建和安装 LLVM。
如何操作…
在下面的配方中,我们将看到带有垃圾回收内置函数的 LLVM IR 代码是如何转换为相应的机器汇编代码的:
-
编写测试代码:
$ cat testgc.ll declare i8* @llvm_gc_allocate(i32) declare void @llvm_gc_initialize(i32) declare void @llvm.gcroot(i8**, i8*) declare void @llvm.gcwrite(i8*, i8*, i8**) define i32 @main() gc "shadow-stack" { entry: %A = alloca i8* %B = alloca i8** call void @llvm_gc_initialize(i32 1048576) ; Start with 1MB heap ;; void *A; call void @llvm.gcroot(i8** %A, i8* null) ;; A = gcalloc(10); %Aptr = call i8* @llvm_gc_allocate(i32 10) store i8* %Aptr, i8** %A ;; void **B; %tmp.1 = bitcast i8*** %B to i8** call void @llvm.gcroot(i8** %tmp.1, i8* null) ;; B = gcalloc(4); %B.upgrd.1 = call i8* @llvm_gc_allocate(i32 8) %tmp.2 = bitcast i8* %B.upgrd.1 to i8** store i8** %tmp.2, i8*** %B ;; *B = A; %B.1 = load i8**, i8*** %B %A.1 = load i8*, i8** %A call void @llvm.gcwrite(i8* %A.1, i8* %B.upgrd.1, i8** %B.1) br label %AllocLoop AllocLoop: %i = phi i32 [ 0, %entry ], [ %indvar.next, %AllocLoop ] ;; Allocated mem: allocated memory is immediately dead. call i8* @llvm_gc_allocate(i32 100) %indvar.next = add i32 %i, 1 %exitcond = icmp eq i32 %indvar.next, 10000000 br i1 %exitcond, label %Exit, label %AllocLoop Exit: ret i32 0 } declare void @__main()
-
使用
llc
工具生成汇编代码,并使用cat
命令查看汇编代码:$ llc testgc.ll $ cat testgc.s .text .file "testgc.ll" .globl main .align 16, 0x90 .type main,@function main: # @main .Lfunc_begin0: .cfi_startproc .cfi_personality 3, __gcc_personality_v0 .cfi_lsda 3, .Lexception0 # BB#0: # %entry pushq %rbx .Ltmp9: .cfi_def_cfa_offset 16 subq $32, %rsp .Ltmp10: .cfi_def_cfa_offset 48 .Ltmp11: .cfi_offset %rbx, -16 movq llvm_gc_root_chain(%rip), %rax movq $__gc_main, 8(%rsp) movq $0, 16(%rsp) movq %rax, (%rsp) leaq (%rsp), %rax movq %rax, llvm_gc_root_chain(%rip) movq $0, 24(%rsp) .Ltmp0: movl $1048576, %edi # imm = 0x100000 callq llvm_gc_initialize .Ltmp1: # BB#1: # %entry.cont3 .Ltmp2: movl $10, %edi callq llvm_gc_allocate .Ltmp3: # BB#2: # %entry.cont2 movq %rax, 16(%rsp) .Ltmp4: movl $8, %edi callq llvm_gc_allocate .Ltmp5: # BB#3: # %entry.cont movq %rax, 24(%rsp) movq 16(%rsp), %rcx movq %rcx, (%rax) movl $10000000, %ebx # imm = 0x989680 .align 16, 0x90 .LBB0_4: # %AllocLoop # =>This Inner Loop Header: Depth=1 .Ltmp6: movl $100, %edi callq llvm_gc_allocate .Ltmp7: # BB#5: # %AllocLoop.cont # in Loop: Header=BB0_4 Depth=1 decl %ebx jne .LBB0_4 # BB#6: # %Exit movq (%rsp), %rax movq %rax, llvm_gc_root_chain(%rip) xorl %eax, %eax addq $32, %rsp popq %rbx retq .LBB0_7: # %gc_cleanup .Ltmp8: movq (%rsp), %rcx movq %rcx, llvm_gc_root_chain(%rip) movq %rax, %rdi callq _Unwind_Resume .Lfunc_end0: .size main, .Lfunc_end0-main .cfi_endproc .section .gcc_except_table,"a",@progbits .align 4 GCC_except_table0: .Lexception0: .byte 255 # @LPStart Encoding = omit .byte 3 # @TType Encoding = udata4 .asciz "\234" # @TType base offset .byte 3 # Call site Encoding = udata4 .byte 26 # Call site table length .long .Ltmp0-.Lfunc_begin0 # >> Call Site 1 << .long .Ltmp7-.Ltmp0 # Call between .Ltmp0 and .Ltmp7 .long .Ltmp8-.Lfunc_begin0 # jumps to .Ltmp8 .byte 0 # On action: cleanup .long .Ltmp7-.Lfunc_begin0 # >> Call Site 2 << .long .Lfunc_end0-.Ltmp7 # Call between .Ltmp7 and .Lfunc_end0 .long 0 # has no landing pad .byte 0 # On action: cleanup .align 4 .type llvm_gc_root_chain,@object # @llvm_gc_root_chain .bss .weak llvm_gc_root_chain .align 8 llvm_gc_root_chain: .quad 0 .size llvm_gc_root_chain, 8 .type __gc_main,@object # @__gc_main .section .rodata,"a",@progbits .align 8 __gc_main: .long 2 # 0x2 .long 0 # 0x0 .size __gc_main, 8 .section ".note.GNU-stack","",@progbits
它是如何工作的…
在前面代码的主函数中,我们正在使用名为 shadow-stack
的内置 GC 收集器策略,该策略维护一个堆栈 roots()
的链表:
define i32 @main() gc "shadow-stack"
它反映了机器堆栈。如果我们想的话,可以通过在此格式中指定函数名称后的名称来提供任何其他技术,即 gc "策略名称"
。这个策略名称可以是内置策略,也可以是我们自己的自定义垃圾回收策略。
要识别根,即对堆对象的引用,LLVM 使用内置函数 @llvm.gcroot
或 .statepoint
重定位序列。llvm.gcroot
内置函数通知 LLVM,一个栈变量引用了堆上的对象,并且它需要被收集器跟踪。在前面代码中,以下行是调用 llvm.gcroot
函数以标记 %tmp.1
栈变量的调用:
call void @llvm.gcroot(i8** %tmp.1, i8* null)
llvm.gcwrite
函数是一个写屏障。这意味着每当对正在执行垃圾回收的程序进行写操作时,它都会将指向堆对象字段的指针写入,收集器会被告知这一点。llvm.gcread
内置函数也存在,它在程序读取指向堆对象字段的指针时通知垃圾回收器。以下代码行将 %A.1
的值写入 %B.upgrd
堆对象:
call void @llvm.gcwrite(i8* %A.1, i8* %B.upgrd.1, i8** %B.1)
注意
注意,LLVM 不提供垃圾回收器。它应该是语言运行时库的一部分。前面的解释处理了 LLVM 为描述垃圾回收器要求向编译器提供的基础设施。
参见
-
查看
llvm.org/docs/GarbageCollection.html
了解垃圾回收的文档。 -
此外,还可以查看
llvm.org/docs/Statepoints.html
了解垃圾回收的另一种方法。
将 LLVM IR 转换为 JavaScript
在本配方中,我们将简要讨论如何将 LLVM IR 转换为 JavaScript。
准备工作
要将 IR 转换为 JavaScript,执行以下步骤:
-
我们将使用
emscripten
LLVM 到 JavaScript 编译器。您需要下载在kripken.github.io/emscripten-site/docs/getting_started/downloads.html
提供的 SDK。您也可以从源代码构建它,但仅用于实验,您可以使用工具链中包含的 SDK。 -
下载 SDK 后,将其解压缩到指定位置,并转到下载的根目录。
-
安装
default-jre
、nodejs
、cmake
、build-essential
和git
依赖项。 -
执行以下命令以安装 SDK:
./emsdk update ./emsdk install latest ./emsdk activate latest
-
查看位于
~/emscripten
的脚本,以检查其是否具有正确的值,如果不是,则相应地更新它。
如何操作…
执行以下步骤:
-
编写转换的测试代码:
$ cat test.c #include<stdio.h> int main() { printf("hi, user!\n"); return 0; }
-
将代码转换为 LLVM IR:
$ clang –S –emit-llvm test.c
-
现在请使用位于
emsdk_portable/emscripten/master
目录中的emcc
可执行文件,将此.ll
文件作为输入并转换为 JavaScript:$ ./emcc test.ll
-
生成的输出文件是
a.out.js
文件。我们可以使用以下命令执行此文件:$ nodejs a.out.js hi, user!
查看更多
- 要了解更多细节,请访问
github.com/kripken/emscripten
使用 Clang 静态分析器
在这个菜谱中,你将了解由 Clang 静态分析器 执行的代码静态分析。它是建立在 Clang 和 LLVM 之上的。Clang 静态分析器使用的静态分析引擎是一个 Clang 库,并且它具有在不同上下文中被不同客户端重用的能力。
我们将以除以零缺陷为例,向你展示 Clang 静态分析器如何处理这个缺陷。
准备工作
你需要构建和安装 LLVM 以及 Clang。
如何操作…
执行以下步骤:
-
创建一个测试文件并在其中编写测试代码:
$ cat sa.c int func() { int a = 0; int b = 1/a; return b; }
-
通过传递以下命令中显示的命令行选项运行 Clang 静态分析器,并在屏幕上获取输出:
$ clang -cc1 -analyze -analyzer-checker=core.DivideZero sa.c sa.c:3:10: warning: Division by zero int b = 1/a; ~^~ 1 warning generated.
它是如何工作的…
静态分析器核心执行程序的符号执行。输入值由符号值表示。表达式的值由分析器使用输入符号和路径计算得出。代码的执行是路径敏感的,因此分析每个可能的路径。
在执行过程中,执行跟踪由一个展开图表示。这个 ExplodedGraph
的每个节点都称为 ExplodedNode
。它由一个 ProgramState
对象组成,该对象表示程序的抽象状态;以及一个 ProgramPoint
对象,该对象表示程序中的相应位置。
对于每种类型的错误,都有一个相关的检查器。这些检查器中的每一个都以一种方式链接到核心,从而为 ProgramState
构造做出贡献。每次分析器引擎探索一个新的语句时,它会通知已注册以监听该语句的每个检查器,给它一个报告错误或修改状态的机会。
每个检查器都会注册一些事件和回调,例如 PreCall
(在函数调用之前)、DeadSymbols
(当符号死亡时),等等。在请求的事件发生时,它们会收到通知,并实现针对此类事件采取的操作。
在这个菜谱中,我们查看了一个除以零检查器,该检查器在发生除以零条件时报告。在这种情况下,检查器在执行语句之前注册了PreStmt
回调。然后它检查将要执行的下一个语句的运算符,如果找到除法运算符,它就寻找零值。如果找到这样的可能值,它就报告一个错误。
参见
- 有关静态分析器和检查器的更详细信息,请访问
clang-analyzer.llvm.org/checker_dev_manual.html
使用 bugpoint
在这个菜谱中,您将了解由 LLVM 基础设施提供的一个有用工具,称为 bugpoint。bugpoint 允许我们缩小 LLVM 工具和传递中问题来源的范围。它在调试优化器崩溃、优化器误编译或生成不良本地代码时非常有用。使用它,我们可以为我们的问题获取一个小测试用例并在此上工作。
准备工作
您需要构建和安装 LLVM。
如何做…
执行以下步骤:
-
使用 bugpoint 工具编写测试用例:
$ cat crash-narrowfunctiontest.ll define i32 @foo() { ret i32 1 } define i32 @test() { call i32 @test() ret i32 %1 } define i32 @bar() { ret i32 2 }
-
在这个测试用例中使用 bugpoint 查看结果:
$ bugpoint -load path-to-llvm/build/./lib/BugpointPasses.so crash-narrowfunctiontest.ll -output-prefix crash-narrowfunctiontest.ll.tmp -bugpoint-cras hcalls -silence-passes Read input file : 'crash-narrowfunctiontest.ll' *** All input ok Running selected passes on program to test for crash: Crashed: Aborted (core dumped) Dumped core *** Debugging optimizer crash! Checking to see if these passes crash: -bugpoint-crashcalls: Crashed: Aborted (core dumped) Dumped core *** Found crashing pass: -bugpoint-crashcalls Emitted bitcode to 'crash-narrowfunctiontest.ll.tmp-passes.bc' *** You can reproduce the problem with: opt crash-narrowfunctiontest.ll.tmp-passes.bc -load /home/mayur/LLVMSVN_REV/llvm/llvm/rbuild/./lib/BugpointPasses.so -bugpoint-crashcalls *** Attempting to reduce the number of functions in the testcase Checking for crash with only these functions: foo test bar: Crashed: Aborted (core dumped) Dumped core Checking for crash with only these functions: foo test: Crashed: Aborted (core dumped) Dumped core Checking for crash with only these functions: test: Crashed: Aborted (core dumped) Dumped core Emitted bitcode to 'crash-narrowfunctiontest.ll.tmp-reduced-function.bc' *** You can reproduce the problem with: opt crash-narrowfunctiontest.ll.tmp-reduced-function.bc -load /home/mayur/LLVMSVN_REV/llvm/llvm/rbuild/./lib/BugpointPasses.so -bugpoint-crashcalls Checking for crash with only these blocks: : Crashed: Aborted (core dumped) Dumped core Emitted bitcode to 'crash-narrowfunctiontest.ll.tmp-reduced-blocks.bc' *** You can reproduce the problem with: opt crash-narrowfunctiontest.ll.tmp-reduced-blocks.bc -load /home/mayur/LLVMSVN_REV/llvm/llvm/rbuild/./lib/BugpointPasses.so -bugpoint-crashcalls Checking for crash with only 1 instruction: Crashed: Aborted (core dumped) Dumped core *** Attempting to reduce testcase by deleting instructions: Simplification Level #1 Checking instruction: %1 = call i32 @test()Success! *** Attempting to reduce testcase by deleting instructions: Simplification Level #0 Checking instruction: %1 = call i32 @test()Success! *** Attempting to perform final cleanups: Crashed: Aborted (core dumped) Dumped core Emitted bitcode to 'crash-narrowfunctiontest.ll.tmp-reduced-simplified.bc' *** You can reproduce the problem with: opt crash-narrowfunctiontest.ll.tmp-reduced-simplified.bc -load /home/mayur/LLVMSVN_REV/llvm/llvm/rbuild/./lib/BugpointPasses.so -bugpoint-crashcalls
-
现在,要查看缩减的测试用例,请使用
llvm-dis
命令将crash-narrowfunctiontest.ll.tmp-reduced-simplified.bc
文件转换为.ll
形式。然后查看缩减的测试用例:$ llvm-dis crash-narrowfunctiontest.ll.tmp-reduced-simplified.bc $ cat $ cat crash-narrowfunctiontest.ll.tmp-reduced-simplified.ll define void @test() { call void @test() ret void }
它是如何工作的…
bugpoint 工具在测试程序上运行命令行中指定的所有传递。如果这些传递中的任何一个崩溃,bugpoint 将启动崩溃调试器。崩溃调试器试图减少导致此崩溃的传递列表。然后它尝试删除不必要的函数。一旦能够将测试程序缩减到单个函数,它就尝试删除控制流图的边以减小函数的大小。之后,它继续删除对失败没有影响的单个 LLVM 指令。最后,bugpoint 给出输出,显示哪个传递导致崩溃,以及一个简化的缩减测试用例。
如果没有指定–output
选项,那么 bugpoint 将在一个"safe"
后端上运行程序并生成参考输出。然后它比较由所选代码生成器生成的输出。如果有崩溃,它将运行崩溃调试器,如前一段所述。除此之外,如果代码生成器生成的输出与参考输出不同,它将启动代码生成器调试器,通过类似于崩溃调试器的技术缩减测试用例。
最后,如果代码生成器生成的输出和参考输出相同,那么 bugpoint 将运行所有的 LLVM 遍历,并将输出与参考输出进行比较。如果有任何不匹配,它将运行误编译调试器。误编译调试器通过将测试程序分成两部分来工作。它在一个部分上运行指定的优化,然后将两部分重新链接在一起,并最终执行结果。它试图从遍历列表中缩小到导致误编译的遍历,然后确定正在被误编译的测试程序的部分。它输出导致误编译的简化案例。
在前面的测试用例中,bugpoint 检查所有函数中的崩溃,并最终确定问题出在测试函数中。它还尝试减少函数内的指令。每个阶段的输出都会在终端上显示,这是不言自明的。最后,它以位码格式生成简化的简化测试用例,我们可以将其转换为 LLVM IR 并获取简化后的测试用例。
参见
- 要了解更多关于 bugpoint 的信息,请访问
llvm.org/docs/Bugpoint.html
使用 LLDB
在这个菜谱中,你将学习如何使用 LLVM 提供的名为LLDB
的调试器。LLDB 是一个下一代、高性能的调试器。它本质上是一个可重用组件的集合,在更大的 LLVM 项目中优于现有的库。你可能会觉得它和gdb
调试工具非常相似。
准备工作
在使用 LLDB 之前,我们需要以下内容:
-
要使用 LLDB,我们需要在
llvm/tools
文件夹中检出 LLDB 源代码:svn co http://llvm.org/svn/llvm-project/lldb/trunk lldb
-
构建并安装 LLVM,这将同时构建 LLDB。
如何操作...
执行以下步骤:
-
使用 LLDB 编写一个简单示例的测试用例:
$ cat lldbexample.c #include<stdio.h> int globalvar = 0; int func2(int a, int b) { globalvar++; return a*b; } int func1(int a, int b) { globalvar++; int d = a + b; int e = a - b; int f = func2(d, e); return f; } int main() { globalvar++; int a = 5; int b = 3; int c = func1(a,b); printf("%d", c); return c; }
-
使用带有
-g
标志的 Clang 编译代码以生成调试信息:$ clang -g lldbexample.c
-
使用 LLDB 调试前一个文件生成的输出文件。要加载输出文件,我们需要将其名称传递给 LLDB:
$ lldb a.out (lldb) target create "a.out" Current executable set to 'a.out' (x86_64).
-
在主函数中设置断点:
(lldb) breakpoint set --name main Breakpoint 1: where = a.out'main + 15 at lldbexample.c:20, address = 0x00000000004005bf
-
要查看设置的断点列表,请使用以下命令:
(lldb) breakpoint list Current breakpoints: 1: name = 'main', locations = 1 1.1: where = a.out'main + 15 at lldbexample.c:20, address = a.out[0x00000000004005bf], unresolved, hit count = 0
-
在遇到断点时添加要执行的命令。在这里,让我们在主函数上的断点被触发时添加回溯
bt
命令:(lldb) breakpoint command add 1.1 Enter your debugger command(s). Type 'DONE' to end. > bt > DONE
-
使用以下命令运行可执行文件。这将触发
main
函数上的断点并执行之前步骤中设置的回溯(bt
)命令:(lldb) process launch Process 2999 launched: '/home/mayur/book/chap9/a.out' (x86_64) Process 2999 stopped * thread #1: tid = 2999, 0x00000000004005bf a.out'main + 15 at lldbexample.c:20, name = 'a.out', stop reason = breakpoint 1.1 frame #0: 0x00000000004005bf a.out'main + 15 at lldbexample.c:20 17 18 19 int main() { -> 20 globalvar++; 21 int a = 5; 22 int b = 3; 23 (lldb) bt * thread #1: tid = 2999, 0x00000000004005bf a.out'main + 15 at lldbexample.c:20, name = 'a.out', stop reason = breakpoint 1.1 * frame #0: 0x00000000004005bf a.out'main + 15 at lldbexample.c:20 frame #1: 0x00007ffff7a35ec5 libc.so.6'__libc_start_main(main=0x00000000004005b0, argc=1, argv=0x00007fffffffda18, init=<unavailable>, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffda08) + 245 at libc-start.c:287 frame #2: 0x0000000000400469 a.out
-
在全局变量上设置
watchpoint
,请使用以下命令:(lldb) watch set var globalvar Watchpoint created: Watchpoint 1: addr = 0x00601044 size = 4 state = enabled type = w declare @ '/home/mayur/book/chap9/lldbexample.c:2' watchpoint spec = 'globalvar' new value: 0
-
当
globalvar
的值变为3
时停止执行,请使用watch
命令:(lldb) watch modify -c '(globalvar==3)' To view list of all watch points: (lldb) watch list Number of supported hardware watchpoints: 4 Current watchpoints: Watchpoint 1: addr = 0x00601044 size = 4 state = enabled type = w declare @ '/home/mayur/book/chap9/lldbexample.c:2' watchpoint spec = 'globalvar' new value: 0 condition = '(globalvar==3)'
-
在主函数之后继续执行,请使用以下命令。可执行文件将在
func2
函数内部globalvar
的值变为3
时停止:(lldb) thread step-over (lldb) Process 2999 stopped * thread #1: tid = 2999, 0x000000000040054b a.out'func2(a=8, b=2) + 27 at lldbexample.c:6, name = 'a.out', stop reason = watchpoint 1 frame #0: 0x000000000040054b a.out'func2(a=8, b=2) + 27 at lldbexample.c:6 3 4 int func2(int a, int b) { 5 globalvar++; -> 6 return a*b; 7 } 8 9 Watchpoint 1 hit: old value: 0 new value: 3 (lldb) bt * thread #1: tid = 2999, 0x000000000040054b a.out'func2(a=8, b=2) + 27 at lldbexample.c:6, name = 'a.out', stop reason = watchpoint 1 * frame #0: 0x000000000040054b a.out'func2(a=8, b=2) + 27 at lldbexample.c:6 frame #1: 0x000000000040059c a.out'func1(a=5, b=3) + 60 at lldbexample.c:14 frame #2: 0x00000000004005e9 a.out'main + 57 at lldbexample.c:24 frame #3: 0x00007ffff7a35ec5 libc.so.6'__libc_start_main(main=0x00000000004005b0, argc=1, argv=0x00007fffffffda18, init=<unavailable>, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffda08) + 245 at libc-start.c:287 frame #4: 0x0000000000400469 a.out
-
要继续执行可执行文件,使用
thread continue
命令,它将执行到结束,因为没有遇到其他断点:(lldb) thread continue Resuming thread 0x0bb7 in process 2999 Process 2999 resuming Process 2999 exited with status = 16 (0x00000010)
-
要退出 LLDB,使用以下命令:
(lldb) exit
参见
- 查阅
lldb.llvm.org/tutorial.html
获取 LLDB 命令的详尽列表。
使用 LLVM 实用传递函数
在这个菜谱中,你将了解 LLVM 的实用传递函数。正如其名所示,它们对希望了解关于 LLVM 的某些不易通过代码理解的事情的用户非常有用。我们将探讨两个代表程序 CFG 的实用传递函数。
准备工作
你需要构建和安装 LLVM,并安装 graphviz
工具。你可以从 www.graphviz.org/Download.php
下载 graphviz
,或者如果它在可用软件包列表中,可以从你的机器的包管理器中安装它。
如何操作...
执行以下步骤:
-
编写运行实用传递函数所需的测试代码。此测试代码包含
if
块,它将在 CFG 中创建一个新的边:$ cat utility.ll declare double @foo() declare double @bar() define double @baz(double %x) { entry: %ifcond = fcmp one double %x, 0.000000e+00 br i1 %ifcond, label %then, label %else then: ; preds = %entry %calltmp = call double @foo() br label %ifcont else: ; preds = %entry %calltmp1 = call double @bar() br label %ifcont ifcont: ; preds = %else, %then %iftmp = phi double [ %calltmp, %then ], [ %calltmp1, %else ] ret double %iftmp }
-
运行
view-cfg-only
传递函数以查看函数的 CFG,而不包括函数体:$ opt –view-cfg-only utility.ll
-
现在,查看使用
graphviz
工具生成的dot
文件: -
运行
view-dom
传递函数以查看函数的 支配树:$ opt –view-dom utility.ll
-
查看使用
graphviz
工具生成的dot
文件:
参见
- 其他实用传递函数的列表可在
llvm.org/docs/Passes.html#utility-passes
获取