LLVM安装和LLVM包括LLVM tablegen学习笔记

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语言的一个快速入门

LLVM的doxygen

中间表示IR的语法书

开发者手册

开发者手册中文,但是还是要看英文的,比如里面把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的优点.

  1. 通用, 任意语言都能转换为IR, 同一IR能转换为任意架构汇编.
  2. 可移植性好, 容易定位问题, 只要保证IR正确性就能确定问题范围(前端还是后端还是某个优化pass).
  3. 支持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";
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值