LLVM和LLVM tablegen学习笔记
LLVM介绍
LLVM 是一个跨平台(可在 Linux、Windows 和 Mac 上使用)C/C++ 编译器工具集,像 GCC一样。 LLVM 可以编译用 C、C++ 和 Objective-C 编写的代码。 LLVM 通过 libc++ 和 libc++ ABI 项目支持 C++11、C++14 和 C++17。 LLVM 还部分支持最新的 C++20 和 C++2b 标准。LLVM之所以优秀,在于以下几点:
- LLVM的中间表达(IR)是可以阅读的文本形式的,其他很多编译器却只有内存中的数据结构,使得学习调试难度大增。
- LLVM 工具集提供的 Clang 可以比 GCC 更快地编译 C 和 C++ 代码。与 GCC 相比,LLVM 调试器 LLDB 的内存效率更高,加载符号的速度更快。
- 始于学术项目一个博士的项目,但LLVM一直受到工业界Apple的支持,clang(是llvm的前端)和LLVM都是apple搞出来的,因为gcc他们不满意,后来apple研发的swift也是基于llvm作为编译器,后来研发llvm的这个人去了Tesla,google,tensorflow。LLVM不仅好用,而且开源可定制。避免了在Java中类似面临选择HotSpot和jikes的困境。
**你可以基于LLVM提供的功能开发自己的模块,并集成在LLVM系统上,增加它的功能,或者就单纯自己开发软件工具,而利用LLVM来支撑底层实现。**LLVM是一个编译器框架。LLVM作为编译器框架,是需要各种功能模块支撑起来的,你可以将clang和lld都看做是LLVM的组成部分
LLVM如何工作
看起来就是三个步骤:
- 前端:获取源代码并将其转换为中间表示或 IR。这种翻译简化了编译器其余部分的工作,它不想处理 C++ 源代码的全部复杂性。比如Clang。LLVM IR是LLVM的中间表示,这是LLVM中很重要的一个东西,介绍它的文档就一个,LLVM Language Reference Manual
- 将IR 转换为 IR的Pass:在一般情况下,pass 通常会优化代码:生成另一个 IR 程序作为输出。新生成的IR与上一个IR效果相同,只是它更快更优。如果说要把一个语言编译好的**整个编译过程中使用相同的 IR 。在其他编译器中,每次传递都可能以独特的形式生成代码。
- 后端:生成实际的机器码。很多时候不需要接触这部分。
我要做什么?
我需要给某个库添加一个算子,这个算子是深度学习里面的一个计算方法addlayernorm,我要把这个计算方法变成mindspore里面的一个运算符,就像mindspore的编译器已经有+、-、*,我要基于已有的运算符编写一个新的运算符。这样之后的人用到addlayernorm可以直接用我写的,然后编译器自动编译好,而不是还需要先add再layernorm
LLVM安装
apt安装
# 这是一个mindspore推荐的快速安装方式
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo add-apt-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-12 main"
sudo apt-get update
sudo apt-get install llvm-12-dev -y #但是这样系统找不到llvm,cmake也找不到
# 注意llvm-config --version找不到,因为环境中可能有llvm-config-12和llvm-config-14,但没设置默认的
llvm-config-12 --version
# 以下是另一个官方的安装方法
# clang 和 clang++ 程序是 LLVM 工具集的一部分。 clang用于编译C程序,clang++用于编译C++程序。
sudo apt install clang lldb lld
clang --version #安装号之后就可以clang hello.c -o hello_c 生成可执行文件hello_c
clang++ --version #安装号之后就可以clang++ hello.cpp -o hello_cpp生成可执行文件hello_cpp
# 安装llvm 默认情况下不会自动安装Clang
sudo apt-get install llvm-12
# 查看llvm版本
llvm-config-12 --version
# 但是llvm和clang都有版本的问题,有时候需要制定版本,比如gcc9也是这样的
# 首先,添加所有可用的 llvm-config 版本到 update-alternatives:一般就12和14两个
sudo update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-12 20
sudo update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-14 10
# 选择默认版本
sudo update-alternatives --config llvm-config
# 这样就可以找到llvm-config --version命令了,而不是非写llvm-config-12
llvm-config --version
# 添加所有可用的 clang 版本到 update-alternatives
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-12 20
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-14 10
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-12 20
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-14 10
# 选择默认版本
sudo update-alternatives --config clang
sudo update-alternatives --config clang++
#但是这样cmake还是找不到
#llvmCMake Error at CMakeLists.txt:13 (include):
# include could not find requested file: AddLLVM
# 要找到llvm的安装位置,然后export了
which llvm-config
/usr/bin/llvm-config #发现可执行文件在bin里
#头文件/usr/include/llvm-12/
#库文件/usr/lib/llvm-<version>/
#则cmake链接到库文件/usr/lib/llvm-<version>/lib/cmake/llvm/这是 CMake 查找 LLVMConfig.cmake 和 AddLLVM.cmake 的关键路径。
export LLVM_DIR=/usr/lib/llvm-12/lib/cmake/llvm/
# 但是我很奇怪12版本的找不到路径,addllvm死活找不到,,所以改成14了
export LLVM_DIR=/usr/lib/llvm-14/lib/cmake/llvm/
# set(LLVM_DIR "/usr/lib/llvm-14/lib/cmake/llvm/") #在cmake里面可以加
Cmake编译安装(还没必要)
LLVM语言的一个快速入门
开发者手册中文,但是还是要看英文的,比如里面把class直接翻译成类,而class我觉得在这里面不能翻译
-
pass:编写一个LLVM的规则,比如把一个函数里面的所有+法变成*法,被称为一个pass。在LLVM中优化以pass形式实现, 每一个pass代表一种优化. pass分为两类,
- 一类是分析(analysis)pass, 算相关IR单元的高层信息,但不对其进行修改。这些信息可以被其他Pass使用,或用于调试和程序可视化。换言之,Analysis Pass会从对应的IR单元中挖掘出需要的信息,然后进行存储,并提供查询的接口,让其它Pass去访问其所存储的信息。
- 另一类是变换(transform)pass, 可以使用Analysis Pass的分析结果,然后以某种方式改变和优化IR。也就是说,这类Pass是会改变IR的内容的,可能会改变IR中的指令,也可能会改变IR中的控制流。
还有一种Utility Pass不算进去了,LLVM中实现了几十种优化pass, 其中许多pass运行不止一次. analysis pass存放在lib/Analysis下, transform pass存放在lib/Transforms下
-
dump:一种语言编译成其他语言的过程,在llvm指的是一种语言变成IR(中间表示)的过程,这个也是llvm的一个函数,可以实现输出IR
读取并打印函数名称
首先LLVM用C++编写,然后用cmake进行编译,变成成一个pass
这里是仓库 git clone git@github.com:sampsyo/llvm-pass-skeleton.git
# Skeleton.cpp中
#include "llvm/Pass.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
struct SkeletonPass : public PassInfoMixin<SkeletonPass> {
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM) {
for (auto &F : M) {
# errs()是一个LLVM提供的C++输出流,我们可以用它来输出到控制台
# 这个程序的意义是在编译过程中把每个函数的名称打印出来
errs() << "I saw a function called " << F.getName() << "!\n";
}
return PreservedAnalyses::all();
};
};
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "Skeleton pass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB) {
PB.registerPipelineStartEPCallback(
[](ModulePassManager &MPM, OptimizationLevel Level) {
MPM.addPass(SkeletonPass());
});
}
};
}
# cmakelists里面编写生成一个pass,最终会获得build/skeleton/SkeletonPass.so
# Load LLVMConfig.cmake. If this fails, consider setting `LLVM_DIR` to point
# to your LLVM installation's `lib/cmake/llvm` directory.
# set(LLVM_DIR "/usr/lib/llvm-14/lib/cmake/llvm/") #找不到可以加
find_package(LLVM REQUIRED CONFIG)
# Include the part of LLVM's CMake libraries that defines
# `add_llvm_pass_plugin`.
include(AddLLVM)
# Use LLVM's preprocessor definitions, include directories, and library search
# paths.
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
# 要includeaddllvm才能用下面这个命令
add_llvm_pass_plugin(SkeletonPass
# List your source files here.
Skeleton.cpp
)
获得了这个build/skeleton/SkeletonPass.so,就可以在clang的时候永乐
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.* 某个c程序.c
I saw a function called main!# 这里就会打印函数名称
# -Xclang -load -Xclang path/to/lib.so这是你在Clang中载入并激活你的流程所用的所有代码。
#所以当你处理较大的项目的时候,你可以直接把这些参数加到Makefile的CFLAGS里或者你构建系统的对应的地方。
如何查看IR
LLVM IR有三种表现形式:
- 在编译器内部的IR
- 在磁盘中存储的bitcode(用于JIT编译器)
- 最常见的易于阅读的LLVM IR汇编. 三种格式的IR是等价的(可互相转化), 因此LLVM IR提供了高效的编译器优化手段的同时又保证了方便调试与定位问题.
使用IR的优点.
- 通用, 任意语言都能转换为IR, 同一IR能转换为任意架构汇编.
- 可移植性好, 容易定位问题, 只要保证IR正确性就能确定问题范围(前端还是后端还是某个优化pass).
- 支持LTO(link time optimization).LLVM编译的时候会顺序读取程序的每个指令,一套程序可以这样组成
- 模块module表示了一个源文件
- 源文件里面都是函数function
- 函数主要会做为代码块BasicBlock的容器
- 指令就是一条单独的代码命令Instruction
如何生成IR
在编译时添加选项-emit-llvm即可生成IR, 此时的IR为bitcode格式(默认文件名后缀为bc), 若要生成汇编格式还需添加-S选项(默认文件名后缀为ll).
clang -emit-llvm -S -o - 某个c程序.c #这个就可以把c程序变成IR进行阅读,-emit-llvm
cat ~/test.c
2 int test(int a, int b)
3 {
4 int c = 0;
5 if (a) {
6 c = b;
7 a = c;
8 }
9 return c;
10 }
../llvm_build/bin/clang ~/test.c -O0 -emit-llvm -S -o ~/test.ll
cat ~/test.ll #查看ll这个IR语言的
%5 = add i32 %4, 2 #这个指令将两个32位整数相加(可以通过类型i32推断出来)。它将4号寄存器(写作%4)中的数和字面值2(写作2)求和,然后放到5号寄存器中。
// dump()。它会打印出人可读的IR对象的表示。
namespace {
struct SkeletonPass : public PassInfoMixin<SkeletonPass> {
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM) {
for (auto &F : M.functions()) {
errs() << "In a function called " << F.getName() << "!\n";
errs() << "Function body:\n";
F.print(errs());
for (auto &B : F) {
errs() << "Basic block:\n";
B.print(errs());
for (auto &I : B) {
errs() << "Instruction: \n";
I.print(errs(), true);
errs() << "\n";
}
}
errs() << "I saw a function called " << F.getName() << "!\n";
}
return PreservedAnalyses::all();
};
};
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "Skeleton pass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB) {
PB.registerPipelineStartEPCallback(
[](ModulePassManager &MPM, OptimizationLevel Level) {
MPM.addPass(SkeletonPass());
});
}
};
}
对一个运算符进行重新编写
把函数里第一个二元操作符(比如+,-)改成乘号。
namespace {
struct SkeletonPass : public PassInfoMixin<SkeletonPass> {
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM) {
for (auto &F : M.functions()) {
for (auto &B : F) {
for (auto &I : B) {
// dyn_cast<T>(p)构造函数是LLVM类型检查工具的应用。如果I不是“二元操作符”,这个构造函数返回一个空指针。
if (auto *op = dyn_cast<BinaryOperator>(&I)) {
// Insert at the point where the instruction `op`
// appears.
// IRBuilder用于构造代码。
IRBuilder<> builder(op);
// Make a multiply with the same operands as `op`.
Value *lhs = op->getOperand(0);
Value *rhs = op->getOperand(1);
Value *mul = builder.CreateMul(lhs, rhs);
// Everywhere the old instruction was used as an
// operand, use our new multiply instruction instead.
for (auto &U : op->uses()) {
// A User is anything with operands.
User *user = U.getUser();
user->setOperand(U.getOperandNo(), mul);
}
// We modified the code.
return PreservedAnalyses::none();
}
}
}
}
return PreservedAnalyses::all();
};
};
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "Skeleton pass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB) {
PB.registerPipelineStartEPCallback(
[](ModulePassManager &MPM, OptimizationLevel Level) {
MPM.addPass(SkeletonPass());
});
}
};
}
#include <stdio.h>
int main(int argc, const char** argv) {
int num;
scanf("%i", &num);
printf("%i\n", num + 2);
return 0;
}
$ cc example.c
$ ./a.out
10
12
$ clang -Xclang -load -Xclang build/skeleton/libSkeletonPass.so example.c
$ ./a.out
10
20
LLVM tablegen 的入门
使用 LLVM 时,你会选择一个“目标”,即你想要为其生成指令的处理器体系结构。TableGen 的等效项是“后端”。这些后端不生成指令,而是输出特定于该后端用例的格式。 tablegen是llvm用于开发和维护编译器中公共特性的条目(e.g. 指令描述, 寄存器描述)的代码, ,其本质是一个parser, **说白了就是把一套公用的指令翻译成不同架构的语言,比如一个加法分别翻译成x86、arm的,**将输入的td文件转化为特定的数据结构后再输出为易于阅读的cpp代码,实现td文件到cpp代码
使用方式
在llvm下载目录里/bin/可执行文件中有一个llvm-tblgen工具,读入一个td文件, 并将结果输出至一个inc文件中,生成的inc文件实质为cpp代码,
tablegen代码包含两块:
- 对td文件的处理, 在lib/TableGen/目录下, 包含lexer与parser, 负责解析tablegen的语法并转换为内部数据结构;
- 输出cpp代码, 在utils/TableGen/目录下, 用于生成我们需要的cpp代码, 这块与llvm代码逻辑强相关, 基本上一个cpp文件对应一类信息.
td文件的语法
TableGen的语法与C++相似,具有内置类型和规范。此外,TableGen的语法还引入了一些自动化概念,如multiclass、foreach、let等在td中使用两个关键字定义数据结构:
class与def,这两个在llvm中被称为records,下面分别介绍
- classes类似于模板, 用于描述一类抽象的records,说白了就是那个公用的指令是什么
- definitions用于表达一个具体的records(可以理解为一个特定的类),说白了就是这个公用的指令在特定平台是怎么写的
- 每个records包含若干数据成员, 这些成员的类型有bit(布尔量), int(整型), string(字符串), code(代码段, 包含一行或多行的字符串), bit(位段)等类型.
- 数据成员使用let关键字进行赋值,
- 对于tablegen中解析的成员必须都初始化(为定义的值可以使用?初始化为’未初始化值’), 否则会导致编译失败. 若一个definition record包含一个未初始化成员, 其值将从该definition的superclass中获取. 若tablegen中未解析该成员则不赋值也不会报错.
// 以下是class,是一个比如在x86,arm,acend都会用到的一个指令
class AsmParser {
string AsmParserClassName = "AsmParser";
string AsmParserInstCleanup = "";
bit ShouldEmitMatchRegisterName = 1;
bit ShouldEmitMatchRegisterAltName = 0;
bit AllowDuplicateRegisterNames = 0;
bit HasMnemonicFirst = 1;
bit ReportMultipleNearMisses = 0;
}
// Hexagon架构的Asmarser,然后在高通架构下对上面的class重新写,因为在不同的后端比如x86,arm,acend,他们的指令操作码不一样
// Hexagon架构的Asmarser如下(defined in lib/Target/Hexagon/Hexagon.td):
def HexagonAsmParser : AsmParser {
// 使用`let`作为赋值语句。
let ShouldEmitMatchRegisterAltName = 1;
bit HasMnemonicFirst = 0;
}
llvm-tblgen test.td # 命令行中运行,在默认参数下会输出所有的class和defs。在每条记录定义后的注释表明了ADD所有的类别。
// 再举一个例子
// cat register.td
class Register<int _size, string _alias=""> {
int size = _size;
string alias = _alias;
}
// 64 bit general purpose registers are X<N>.
def X0: Register<8> {}
// Some have special alternate names.
def X29: Register<8, "frame pointer"> {}
// Some registers omitted...
执行tablegen命令./bin/llvm-tblgen register.td
------------- Classes -----------------
class Register<int Register:_size = ?, string Register:_alias = ""> {
int size = Register:_size;
string alias = Register:_alias;
}
------------- Defs -----------------
def X0 { // Register
int size = 8;
string alias = "";
}
def X29 { // Register
int size = 8;
string alias = "frame pointer";
}