Skip to content

Commit 0406fc4

Browse files
committed
add ir feats
1 parent ffa8a88 commit 0406fc4

File tree

1 file changed

+219
-6
lines changed

1 file changed

+219
-6
lines changed

docs/llvm_intro.md

Lines changed: 219 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,12 +401,148 @@ IR 的目的是把大部分通用的计算,例如加减乘除、条件跳转
401401

402402
在进入后端的“指令选择”阶段后,IR 节点中通用的那部分节点,会逐步被替换为和硬件绑定的相应 MachineInstr 节点,在“指令选择”后的阶段看来,就好像前面输出了一大堆内联汇编一样,最终,逐渐变成了完全的目标平台汇编,最终输出。
403403

404-
不仅仅是统一,IR 汇编和目标平台的汇编相比,好处有很多。
404+
### LLVM IR 的特点
405+
406+
以下是一段 C++ 代码:
407+
408+
```cpp
409+
int main() {
410+
int a = 0;
411+
int b = 1;
412+
return a + 1;
413+
}
414+
```
415+
416+
及其所对应的 LLVM IR 汇编:
417+
418+
```wasm
419+
; ModuleID = 'a.cpp'
420+
source_filename = "a.cpp"
421+
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
422+
target triple = "x86_64-pc-linux-gnu"
423+
424+
; Function Attrs: mustprogress noinline norecurse nounwind optnone sspstrong uwtable
425+
define dso_local noundef i32 @main() #0 {
426+
%1 = alloca i32, align 4
427+
%2 = alloca i32, align 4
428+
%3 = alloca i32, align 4
429+
store i32 0, ptr %1, align 4
430+
store i32 0, ptr %2, align 4
431+
store i32 1, ptr %3, align 4
432+
%4 = load i32, ptr %2, align 4
433+
%5 = add nsw i32 %4, 1
434+
ret i32 %5
435+
}
436+
437+
attributes #0 = { mustprogress noinline norecurse nounwind optnone sspstrong uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-p
438+
rotector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
439+
440+
!llvm.module.flags = !{!0, !1, !2, !3, !4}
441+
!llvm.ident = !{!5}
442+
443+
!0 = !{i32 1, !"wchar_size", i32 4}
444+
!1 = !{i32 8, !"PIC Level", i32 2}
445+
!2 = !{i32 7, !"PIE Level", i32 2}
446+
!3 = !{i32 7, !"uwtable", i32 2}
447+
!4 = !{i32 7, !"frame-pointer", i32 2}
448+
!5 = !{!"clang version 18.1.8"}
449+
```
450+
451+
看起来好复杂!让我们一行一行来解读:
452+
453+
```wasm
454+
; ModuleID = 'a.cpp'
455+
```
456+
457+
这种以分号开头的,就是 IR 汇编语法中的注释,分号后面的东西会被无视,不影响实际结果。就和 C 语言的 `//` 一样,属于行注释。
458+
459+
Clang 生成的 IR 汇编有时带有注释,仅仅是提示给人看的,并不影响后端的解析。这里的注释很明显是在提示,该汇编是由哪个源码文件产生的?是一个叫 `a.cpp` 的文件,但他并没有实际效力,只是给读汇编的你我看。
460+
461+
```wasm
462+
source_filename = "a.cpp"
463+
```
464+
465+
这才是真正对 LLVM 中端有效力的东西,他赋值了一个字符串,提示该 IR 汇编由哪个源码文件产生。这个 `source_filename` 属性,是由 Clang 在生成 IR 汇编时,主动加上告知 LLVM 后端的。也是为了方便调试,例如当 LLVM 中端中触发了报错,他可以以这个文件名来提示用户。
466+
467+
```wasm
468+
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
469+
target triple = "x86_64-pc-linux-gnu"
470+
```
471+
472+
这里指定的是关于“目标平台”的一些信息,分为两个部分。
473+
474+
> {{ icon.tip }} 目标平台指的是当前源码要编译到什么硬件上执行,比如在我们的案例中,目标平台就是 x86。
475+
476+
其中 `target triple` 是指定的目标平台的名字,这里我们是 `x86_64-pc-linux-gnu`,表示 64 位 x86 架构,桌面端,Linux 系统,GNU 编译器接口。不同的 `target triple` 会影响最终生成的汇编中函数调用约定、C++ 函数名重组等细节,可以认为这个 `triple` 就是我们常说的 ABI(二进制应用程序接口)。
477+
478+
> {{ icon.story }} 例如,在 Windows 上使用 `clang`,可能得到 `target triple``x86_64-pc-windows-msvc`(如果你是 MSVC 编译器)或者 `x86_64-pc-windows-gnu`(如果你是 MinGW 编译器),这两者产生的 C++ 函数名称重组会有不同。
479+
480+
> {{ icon.tip }} 不同的操作系统和硬件,都会有不同的 ABI,例如 Linux 在 x86_64 上的 ABI 规定第一个参数由 `rdi` 传入,而 Windows 则是 `rcx`。这些细节都是由 `target triple` 确定的。
481+
482+
`target datalayout` 则是指定了目标平台的数据类型大小和布局等信息,例如指针大小、对齐方式等。例如在 x86_64-pc-linux-gnu 这个 ABI 上,`long` 是 64 位。而在 x86_64-pc-windows-msvc 上,`long` 是 32 位。这些信息会影响后端产生汇编的内存布局,因此,IR 是不跨平台的。
483+
484+
`target datalayout` 是一个很长的字符串,里面有多个由 `-` 分隔的字段。每个字段可以分别用来描述指针、整型、浮点型、矢量、数组、结构体、联合体等的大小和内存布局。
485+
486+
例如上面的 `e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128`,我们首先按 `-` 拆分,得到每一个子字段。
487+
488+
```
489+
e // 表示目标平台的字节序,e=小端,E=大端
490+
m:e // 表示 C++ 函数名称重组采用何种机制,e=ELF风格,w=Windows风格……等
491+
p270:32:32 // 垃圾信息,无视
492+
p271:32:32 // 垃圾信息,无视
493+
p272:64:64 // 指针大小 64 位,对齐到 64 位
494+
i64:64 // __int64 采用 64 位来存储
495+
i128:128 // __int128 采用 128 位来存储
496+
f80:128 // long double 采用 128 位来存储
497+
n8:16:32:64 // 指定哪些是目标 CPU 原生的类型大小,对于 64 位 x86 来说,分别有 8 位、16 位、32 位、64 位寄存器,所以都写上
498+
S128 // 栈指针(rsp)对齐到 128 位(16 字节),这是 64 位 x86 ABI 所要求的
499+
```
500+
501+
> {{ icon.tip }} `target datalayout` 的详细语法,可以参考 [LLVM 官方文档](https://llvm.org/docs/LangRef.html#data-layout)
502+
503+
继续看下去:
504+
505+
```wasm
506+
; Function Attrs: mustprogress noinline norecurse nounwind optnone sspstrong uwtable
507+
define dso_local noundef i32 @main() #0 {
508+
...
509+
}
510+
```
511+
512+
首先,以分号开头的 `; Function Attrs: ...` 是注释,可以忽略。
513+
514+
> {{ icon.detail }} 注释里面的 `mustprogress noinline ...` 等,表示的是下面一个函数的“属性”。但是注释并没有实际效果,仅仅是提示作用,真正设置了属性的是更下面的 `attributes #0` 指令,Clang 生成一个注释只是让你看起来方便,不用跑到下面才能看到属性。
515+
516+
那么,只剩下函数的定义了:
517+
518+
```wasm
519+
define dso_local noundef i32 @main() #0 {
520+
...
521+
}
522+
```
523+
524+
- `define` 表示这是一个函数的定义。
525+
- `dso_local``noundef` 是一些无关紧要的修饰,可以暂时无视。
526+
- `i32` 表示函数的返回类型,也就是 32 位整数类型,对应于 C++ 中的 `int`
527+
- `@main` 是函数名字,其中 `@` 是所有函数名的固有前缀,后面的 `main` 就是我们当前定义的函数名称。
528+
- 函数名字后面紧接着的 `()` 表示参数列表,此处我们的 `main` 函数刚好没有任何参数,所以是一个空的括号(稍后我们会看一个有参数的案例)。
529+
- `#0` 表示该函数的编号,和寄存器编号一样。值得注意的是,函数编号和寄存器编号共用一个序列,当函数占用了 `#0` 以后,寄存器就不能再用 `%0` 了,要从 `%1` 开始。
530+
- `{` 中的内容,就是函数块内部的 IR 了。
531+
532+
...
405533

406534
### LLVM IR 的特点
407535

536+
IR 和目标平台的汇编相比,不仅仅是统一,好处有很多。
537+
408538
#### 以函数为单位
409539

540+
每个 C++ 文件编译得到的 IR 汇编均由一系列函数定义(和少量特殊指令,比如 `target triple`)组成,所有的 IR 指令都包在函数中。
541+
542+
函数是 LLVM 优化的基本单位,绝大多数 LLVM pass,都是以单个函数为单位来优化的。只有内联优化和 IPO 优化,可以实现跨函数的优化。
543+
544+
> {{ icon.story }} 这也是为什么内联优化不仅仅是“函数原封不动插入调用位置”。把函数插入调用者体内后,两个函数融合为一个函数了,使优化器的“视野”更大(因为大部分优化不能跨越函数边界)!从而帮助了其他以函数为边界的优化 pass 能够更好的优化。例如在 `main` 中以迭代器遍历一个容器:本来 `main` 函数是调用迭代器的 `operator++`,而 `operator++` 不内联掉的话,`main` 的优化器永远无法意识到 `operator++` 实际上就是一个指针的加法,只能老老实实分配 alloca 然后获取出 this 指针传入 `operator++` 函数。而内联以后,`operator++` 的内容暴露在优化 pass 眼前,他就知道可以把迭代器优化成一个静态寄存器了,根本不用为其分配内存!最终产生的汇编一下子从必须分配在栈内存上,到可以把迭代器指针放在寄存器里了。而 IPO (Inter-procedure Optimization) 则是根据函数参数为常数的情况做分发,分发后可以针对不同常数参数的结果做特定优化,但不合并函数,无法赋能其他优化 pass,效果看起来就没有内联强了。但是要注意很多优化 pass 的开销是和函数的大小(IR 指令数量)成正比的,所以内联太多层导致一个函数很大的话,编译会变得比较慢。
545+
410546
#### 任意多个寄存器
411547

412548
寄存器无限量供应!一个函数体内可以定义任意多个寄存器,无需考虑硬件具体提供了多少个寄存器。因为和硬件寄存器脱钩,所以我们可以称之为虚拟寄存器,只在 LLVM 中端里出现,为了便于优化和处理采取的简化策略。
@@ -510,14 +646,18 @@ return %y;
510646
511647
#### 三操作数指令
512648

649+
650+
513651
#### 定义与使用
514652

515653
这里我们有必要明确一下“定义(define)”和“使用(use)”。这是两个编译器领域的术语,用于描述 SSA IR 的寄存器及其用况。
516654

517655
定义:一个寄存器被赋值的地方,就是他的定义。
518656

519657
```wasm
520-
658+
%1 = 1 ; %1 的定义
659+
%2 = 2 ; %2 的定义
660+
%3 = add %1, %2 ; %3 的定义
521661
```
522662

523663
因为 LLVM IR 是 SSA 的,所有寄存器都是常量,只能在初始化时被赋值一次。所以寄存器的定义是唯一的,也就是初始化的那一次赋值的地方。
@@ -541,7 +681,7 @@ return %y;
541681
- “定义-使用”关系:通过一个寄存器的“定义”,找到他被哪些寄存器“使用”了。
542682
- “使用-定义”关系:通过一个寄存器的“使用”,找到他是在哪里被“定义”的。
543683

544-
> {{ icon.tip }} 就是两个互逆的 map 映射。要注意的是,他们都不是一一映射:一个寄存器可以被很多其他寄存器的定义使用,一个寄存器的定义也可能使用到了多个其他寄存器。
684+
> {{ icon.tip }} “定义-使用”和“使用定义”是两个互逆的映射。不过要注意,他们都不是一一映射:一个寄存器可以被多个其他寄存器重复使用,一个寄存器的定义也可能使用到了多个其他寄存器。
545685
546686
##### “使用-定义”映射
547687

@@ -565,15 +705,15 @@ for (llvm::Use &U: pi->operands()) {
565705

566706
LLVM 中的 pass,是指一组对 IR 进行操作的函数。pass 分为分析类 pass 和优化类 pass。
567707

568-
- 分析类 pass 只是帮助我们观察 IR,获得某些概括信息,并不修改 IR,这里我们要用的 `def-use` pass 就属于此类,他通过一次遍历找到所有的“定义-使用”映射关系只要 IR 不修改,就可以缓存之前的结果,供后来者重复使用。
708+
- 分析类 pass 只是帮助我们观察 IR,获得某些概括信息,并不修改 IR。现在我们要用的 `def-use` pass 就属于分析类 pass,他通过一次遍历找到所有的“定义-使用”映射关系只要 IR 不修改,就可以缓存之前的结果,供后来者重复使用。
569709
- 优化类 pass 可以修改 IR 的节点,修改 IR 原有的结构,例如之前提到的 `mem2reg` pass 就属于此类,他会把所有能优化的 alloca + store 修改成静态单赋值的寄存器。由于会修改 IR,可能导致某些分析 pass 的结果失效,下次再用到时需要重跑。
570710

571711
区别:
572712

573713
- 分析类 pass 的输入是一段 IR,输出是一个用户自定义的分析结果类型。例如对于 `def-use` pass,输出是一个 `Analysis` 类型的对象,这个对象中维护了一个 “定义-使用” 的双向映射,我们 `def-use` pass 分析得到结果后就可以使用这个映射来查询。
574714
- 优化类 pass 的输入是一段 IR,输出也是一段 IR,被改变后的 IR。LLVM 实现优化,就是通过一系列优化 pass 的组合完成的。有时,优化 pass 会需要一些分析的结果,才能进行,因此优化 pass 有时会请求一些分析 pass 的结果,LLVM 会检查这个分析之前有没有进行过,如果有,就会复用上次分析的结果,不会重新分析浪费时间。优化 pass 在修改了 IR 后,需要返回一个标志位,表示 IR 修改后,哪些分析 pass 的结果可能会失效。如果不确定,就返回 `all` 吧:本优化 pass 修改过 IR 后所有之前分析 pass 缓存的结果都会失效。
575715

576-
如何判断一个虚拟寄存器是否可以被优化掉?检测他有没有被别人“使用”也就是查询他的“定义-使用”映射,如果发现“使用者列表”为空,就说明没人使用,可以优化掉。
716+
如何判断一个虚拟寄存器是否可以被优化掉?检测他有没有被别人“使用”也就是查询他的“定义-使用”映射,如果发现“使用者列表”为空,就说明该寄存器的“定义”没人使用,可以优化掉。
577717

578718
### Clang 生成 IR 汇编
579719

@@ -633,7 +773,7 @@ Clang 编译时是什么平台就是什么平台了,不同目标平台的 IR
633773

634774
IR 字节码中的每个(或多个)字节都可以和 IR 汇编中的一行 IR 指令一一对应。
635775

636-
### IR 汇编 vs IR 字节码
776+
### IR 汇编和 IR 字节码的不同之处
637777

638778
#### 后缀名不同
639779

@@ -772,6 +912,79 @@ llvm-link test1.bc test2.bc -o test.bc
772912
773913
> {{ icon.fun }} 把 C++ 文件编译生成的字节码模块链接起来,就像很多个 C++ 文件突然国宝特工合体一样。
774914
915+
### 调用 LLVM pass 优化
916+
917+
LLVM 提供了 `opt` 这个方便的命令行工具,可以调用 LLVM 中指定名称的优化类 pass,用来优化传入的 IR 文件(可以是 IR 汇编或 IR 字节码)。
918+
919+
默认情况下输出的是二进制的 IR 字节码,如需输出人类可读的 IR 汇编,可以指定 `-S` 选项(就和刚才我们使用 `clang -S``-emit-llvm` 生成 IR 汇编而不是 IR 字节码一样)。
920+
921+
```bash
922+
# 输入 a.ll,使用 mem2reg 这个优化 pass 后,结果输出到 a-opt.ll(IR 汇编)
923+
opt -S -p mem2reg a.ll -o a-opt.ll
924+
cat a-opt.ll
925+
```
926+
927+
```bash
928+
# 输入 a.bc,使用 mem2reg 这个优化 pass 后,结果输出到 a-opt.bc(IR 字节码)
929+
opt -p mem2reg a.bc -o a-opt.bc
930+
llvm-dis a-opt.bc -o a-opt.ll # 如果输出字节码格式,还需要 llvm-dis 才能让人类看懂
931+
```
932+
933+
#### 案例
934+
935+
还是这段 C++ 代码:
936+
937+
```cpp
938+
int main() {
939+
int a = 0;
940+
int b = 1;
941+
return a + 1;
942+
}
943+
```
944+
945+
使用 `clang` 编译,生成 IR 汇编:
946+
947+
```bash
948+
clang -S -emit-llvm a.cpp -o a.ll
949+
```
950+
951+
得到 a.ll:
952+
953+
```wasm
954+
; ModuleID = 'a.cpp'
955+
source_filename = "a.cpp"
956+
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
957+
target triple = "x86_64-pc-linux-gnu"
958+
959+
; Function Attrs: mustprogress noinline norecurse nounwind optnone sspstrong uwtable
960+
define dso_local noundef i32 @main() #0 {
961+
%1 = alloca i32, align 4
962+
%2 = alloca i32, align 4
963+
%3 = alloca i32, align 4
964+
store i32 0, ptr %1, align 4
965+
store i32 0, ptr %2, align 4
966+
store i32 1, ptr %3, align 4
967+
%4 = load i32, ptr %2, align 4
968+
%5 = add nsw i32 %4, 1
969+
ret i32 %5
970+
}
971+
972+
attributes #0 = { mustprogress noinline norecurse nounwind optnone sspstrong uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-p
973+
rotector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
974+
975+
!llvm.module.flags = !{!0, !1, !2, !3, !4}
976+
!llvm.ident = !{!5}
977+
978+
!0 = !{i32 1, !"wchar_size", i32 4}
979+
!1 = !{i32 8, !"PIC Level", i32 2}
980+
!2 = !{i32 7, !"PIE Level", i32 2}
981+
!3 = !{i32 7, !"uwtable", i32 2}
982+
!4 = !{i32 7, !"frame-pointer", i32 2}
983+
!5 = !{!"clang version 18.1.8"}
984+
```
985+
775986
## 汇编语言(ASM)
776987

777988
## 汇编语言的终局:机器码
989+
990+
## 构建好了吗

0 commit comments

Comments
 (0)