LLVM概念

LLVM编译器是基于模块化、可扩展化的设计,将编译器过程分为多个阶段,而gcc编译器则是集成了多个前端和后端的传统编译器,设计更紧密一体化。

LLVM编译器因具有高度模块化的中间表示IR为基础,具有能实现更细粒度的优化。

LLVM结构

前端解析源代码,检查错误,并构建特定语言的抽象语法树(AST)来表示输入代码。AST额可以选择转换为新的表示形式来进行优化。

优化器负责进行各种转换以尝试提高代码的运行时间,例如消除冗余计算

后端将代码映射到目标指令集。除了编写正确代码之外,它还负责生成利用受支持架构的不寻常的功能的良好代码。后端常见部分包括指令选择、寄存器分配和指令调度。

屏幕截图 2024 11 28 114157

编译构建LLVM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
##git克隆LLVM-project
git clone --depth 10 https://github.com/llvm/llvm-project.git
git chectout 19.1.3
mkdir build
cd build
cmake -G "Unix Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS="clang;lld;mlir" \
-DCMAKE_INSTALL_PREFIX=/opt/llvm19 \
/home/riscv/llvm-project/llvm ##工程下的llvm目录
make -j$(nproc)
make install
sudo vi ~/.bashrc
##add
export PATH=$PATH:/opt/llvm19/bin
source ~/.bashrc
## version find
llvm-config --version

LLVM常用命令

xiangguanput

.ll 文件是 LLVM汇编语言格式的源代码文件,它是LLVM中间表示的一种文本格式,通常用于人类阅读和调试。它与机器代码无关,旨在展示程序的逻辑和结构,适用于编译、优化以及生成目标代码的各个阶段。

.bc 文件是 LLVM Bitcode 文件,它是LLVM IR的二进制表示格式。.bc 是平台无关的二进制格式,适用于编译过程中的中间步骤。.bc 文件通常用于优化、链接和跨平台处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
##llvm-as: 将IR文件编译为二进制文件,默认生成后缀名为.bc的文件,也可以使用-o指定输出:
llvm-as -o hello hello.ll
##llvm-dis 将二进制生成IR文件生成.ll后缀文件
llvm-dis hello
##llc将.ll或.bc文件编译成汇编文件,输出.s文件
llc hello.ll
##将.ll或.bc文件编译为汇编文件,输出后缀名为.s文件
lli hello.bc
##llvm-link将多个LLVM的二进制文件合并为一个二进制文件
llvm-link -o hello hello.bc bye.bc
##llvm-diff对比两个文件的区别,可以用来比较.ll文件和LLVM的二进制文件
llvm-diff hello1.ll hello2.ll
##llvm-nm:列出LLVM二进制文件或静态库文件的符号表
llvm-nm hello.a
##llvm-ar:创建静态库,值得注意的是llvm-ar会创建一个新的符号表来统一记录静态库中所有的成员,这有助于提升速度
llvm-ar r hello.a hello.bc bye.bc
##opt是LLVM的优化器和分析器,输入LLVM源文件,会对其进行优化或分析,然后输出优化文件或分析结果。
opt [options] <input-file> -o <output-file>
##[options]:指定一个或多个要应用的优化 Pass。
##<input-file>:输入的 LLVM IR 文件(可以是 .bc 或 .ll 格式)。
##-o <output-file>:指定优化后的输出文件。

Clang前端

目录:llvm-project/clang/

  • 预处理:头文件以及宏的处理
  • 词法分析:词法分析器的任务是从左向右逐行扫描源程序的字符,识别除哥哥单词并确定单词的类型,将识别出的单词转换成同意的机内表示————词法单元形式。
  • 语法分析:主要任务是从词法分析器输出的token序列中识别除各类短语,并构造语法分析树(AST)。如果输入字符串的哥哥单词敲好自左至右地站在分析树的各个节点上,那么这个词串就是该语言的一个句子,语法分析树描述了句子的语法结构
  • 语义分析:收集标识符的属性信息与语义检查。表示符的属性包括种属,类型,存储位置和长度、值、作用域、参数和返回值类型。语义检查包括变量或过程未经声明就是用,重复声明、运算分量类型不匹配、操作符与操作数之间类型不匹配。
  • 代码生成:将AST转换成相依的LLVM代码。

AST

clang提供了现成的工具来直接生成AST

1
clang -fmodules - fsyntax-only -Xclang -ast-dump test.cpp
  • -fmodules :允许modules的语言特性
  • -fsyntax-only:只解析语法,不进行编译和链接
  • -Xclang :使用Xclang向编译器传递参数
  • -ast-dump:构建AST并打印

抽象语法树(AST):源代码语法结构的抽象表示。

以树状形式表现程序语言的语法结构,每个节点都代表源代码中的一种结构。

抽象:并不会表示出真实语法中的每个细节。

Clang AST三大核心基本类,每个节点都表示为它们的一个实例。

  • Decl:表明,包括很多子类来标识不同声明类型,参考链接
  • Stmt:表明语句,包含很多子类来标识不同语句类型,参考链接
  • Type:表示类型,参考链接)

IR

高级语言经过Clang等前端解析为平台无关的中间表示(IR),使编译器能够在编译、链接以及代码生成的各个阶段忽略语言特性,进行全面优化分析。LLVM的各种pass都是作用在LLVM IR上的,通常设计一门语言就是生成一个语言的编译器前端Clang即可。

LLVM IR三种表示形式:

  • 内存中的表示形式,如BasicBlock,Instruction这种cpp类;

  • bitcode形式,这是序列化的二进制表示形式

  • LLVM汇编文件形式,也是序列化的表达形式。

IR表达:

  • Module类,Module可以理解一个完整的编译单元。一般来说,这个编译单元就是个源码文件,如一个后缀为cpp的源文件
  • Function类,对一个函数单元,两种情况分别是函数定义和函数声明。
  • BasticBlock类,表示一个基本代码块(就是一段没有控制流逻辑的基本流程)
  • Instruction类,指令类就是LLVM中定义的基本操作。

控制流程图

在LLVM的编译和优化过程中,控制流图用来描述函数或程序的执行顺序和控制流结构。它帮助LLVM分析每个基本块(Basic Block)之间的跳转关系,从而进行各种优化,如死代码消除、循环展开、常量传播等。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int foo(int x) {
if (x == 0) {
return 1;
} else {
return 2;
}
}
int main() {
int result = foo(0);
printf("Result:%d\n",result);
return 0;
}
  1. 编译器生成LLVM IR
1
clang -S -emit-llvm program.c -o program.ll

-S 选项表示编译为汇编语言(但这里是LLVM IR汇编格式)。

-emit-llvm 告诉 clang 生成LLVM IR,而不是机器代码。

program.c 是你编写的C源代码文件。

program.ll 是输出的LLVM IR文件。

  1. 使用llvm-opt 工具查看LLVM IR 控制流的结构图(每个版本的都不一样,使用的是LLVM19.1.4)
1
2
3
opt --dot-regions program.ll

dot -Tpng program.dot -o program.png

LLVM19.1.4的opt工具是有问题的,生成多个dot图形文件时候都报错,这版的开发工具并不完善。唯一不报错的就是–dot-regions 选项了

中间优化遍

目录:llvm-project/llvm/lib/Transforms/

  • Analysis Passes
  • Transform Passes
  • Unility Passes

https://llvm.org/docs/Passes.html

Pass管理器

https://llvm.org/docs/NewPassManager.html

LLVM后端

目录位置:llvm-project/llvm/lib/Target/

LLVM后端主要功能是代码生成,所以也叫代码生成器,负责将LLVM IR 转换特定目标架构的机器代码。

用于实现本机代码生成的各个阶段的目标无关算法。此代码位于lib/CodeGen中。

目标独立JIT组件。LLVM JIT完全独立于目标。

在代码生成过程中,LLVM 后端会根据目标硬件平台的特性和要求,将 LLVM IR 转换为适合该平台的机器码或汇编语言。这个过程涉及到指令选择(Instruction Selection)、寄存器分配(Register Allocation)、指令调度(Instruction Scheduling)等关键步骤,以确保生成的目标代码在目标平台上能够高效运行。

LLVM 后端 Pass

整个后端流水线涉及到四种不同层次的指令表示,包括:

  • 内存中的 LLVM IR:LLVM 中间表现形式,提供了高级抽象的表示,用于描述程序的指令和数据流。
  • SelectionDAG 节点:在编译优化阶段生成的一种抽象的数据结构,用以表示程序的计算过程,帮助优化器进行高效的指令选择和调度。
  • Machinelnstr:机器相关的指令格式,用于描述特定目标架构下的指令集和操作码。
  • MCInst:机器指令,是具体的目标代码表示,包含了特定架构下的二进制编码指令。

不是重点操作目标,详情请看【AI系统】LLVM 后端代码生成 - ZOMI酱酱 - 博客园

总结

  1. 前端阶段
  • 词法分析(Lexical Analysis):源代码被分解为词法单元,如标识符、关键字和常量。
  • 语法分析(Syntax Analysis):词法单元被组织成语法结构,构建抽象语法树(AST)。
  • 语义分析(Semantic Analysis):AST 被分析以确保语义的正确性和一致性。
  1. 中间表示(IR)阶段
  • 将 AST 转化为中间表示(IR),采用 SSA 形式的三地址指令表示代码结构。
  • 通过多段 pass 进行代码优化,包括常量传播、死代码消除、循环优化等,以提高代码性能和效率。
  • IR 进一步转化为 DAG 图,其中每个节点代表一个指令,边表示数据流动。
  1. 后端阶段
  • 指令选择(Instruction Selection):根据目标平台特性选择合适的指令。
  • 寄存器分配(Register Allocation):分配寄存器以最大程度减少内存访问。
  • 指令调度(Instruction Scheduling):优化指令执行顺序以减少延迟。

最终生成目标代码,用于目标平台的执行。

Pass 管理:

在编译器的每个模块和 Pass 均可通过 Pass manager 进行管理,可以动态添加、删除或调整 Pass 来优化编译过程中的各个阶段。

RVV实现推论

要将通过 clang 编译生成的 C 语言 IR 转换为 MLIR,并使用自定义方言对其进行优化,整个流程大致可以分为以下几步:

  1. 使用 Clang 编译生成 IR:将 C 语言代码通过 clang 编译为 LLVM IR。
  2. 将 LLVM IR 转换为 MLIR IR:使用 MLIR 提供的工具将 LLVM IR 转换为 MLIR IR。
  3. 定义自定义方言及操作:在 MLIR 中定义新的方言及操作,例如 RISC-V 矢量扩展。
  4. 编写优化 Pass:针对自定义方言编写优化 Pass。
  5. 应用优化 Pass 并生成新的 IR:在 MLIR 中应用优化 Pass,将生成的 IR 进行优化。

具体步骤:

1. 使用 Clang 编译生成 IR

首先,你需要将 C 语言源代码编译成 LLVM IR,可以使用 clang 工具链中的 -S-emit-llvm 标志来实现这一目标:

1
2
3
4
5
bash


复制代码
clang -S -emit-llvm input.c -o input.ll

这会将 C 语言源文件 input.c 编译成 LLVM IR 文件 input.ll

2. 将 LLVM IR 转换为 MLIR IR

在 MLIR 中,我们通常使用 mlir-translate 工具来将 LLVM IR 转换为 MLIR IR。你可以运行以下命令:

1
2
3
4
5
bash


复制代码
mlir-translate --mlir-to-llvmir input.ll -o input.mlir

该命令将 LLVM IR 文件 input.ll 转换为 MLIR 格式,并生成 input.mlir 文件。

3. 定义自定义方言及操作

接下来,您需要在 MLIR 中定义自定义方言及相关操作。假设您已经定义了一个 Simple 方言,它包括一个加法操作 simple.add,您可以在 SimpleDialect 中定义操作并实现方言。

在方言中定义操作的一般步骤如下:

  • 定义方言(例如:SimpleDialect
  • 定义操作(例如:simple.add

方言定义(SimpleDialect.h)

操作定义(SimpleOps.h)

4. 编写优化 Pass

在 MLIR 中,您可以编写 Pass 来对 IR 进行优化。假设您想对 simple.add 操作执行某些优化(例如,常量折叠),您可以编写一个 Pass 来执行优化。

这个 Pass 会遍历函数中的所有操作,查找 simple.add 操作,并执行常量折叠优化。

5. 应用优化 Pass 并生成新的 IR

1
mlir-opt input.mlir -pass-name=optimize-add

在执行优化 Pass 后,您将得到经过优化的 IR。你可以通过命令行进一步将其转换为 LLVM IR 或者机器码。

1
mlir-translate --mlir-to-llvmir input.mlir -o optimized_input.ll

未命名绘图.drawio