@@ -401,12 +401,148 @@ IR 的目的是把大部分通用的计算,例如加减乘除、条件跳转
401
401
402
402
在进入后端的“指令选择”阶段后,IR 节点中通用的那部分节点,会逐步被替换为和硬件绑定的相应 MachineInstr 节点,在“指令选择”后的阶段看来,就好像前面输出了一大堆内联汇编一样,最终,逐渐变成了完全的目标平台汇编,最终输出。
403
403
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
+ ...
405
533
406
534
### LLVM IR 的特点
407
535
536
+ IR 和目标平台的汇编相比,不仅仅是统一,好处有很多。
537
+
408
538
#### 以函数为单位
409
539
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
+
410
546
#### 任意多个寄存器
411
547
412
548
寄存器无限量供应!一个函数体内可以定义任意多个寄存器,无需考虑硬件具体提供了多少个寄存器。因为和硬件寄存器脱钩,所以我们可以称之为虚拟寄存器,只在 LLVM 中端里出现,为了便于优化和处理采取的简化策略。
@@ -510,14 +646,18 @@ return %y;
510
646
511
647
#### 三操作数指令
512
648
649
+
650
+
513
651
#### 定义与使用
514
652
515
653
这里我们有必要明确一下“定义(define)”和“使用(use)”。这是两个编译器领域的术语,用于描述 SSA IR 的寄存器及其用况。
516
654
517
655
定义:一个寄存器被赋值的地方,就是他的定义。
518
656
519
657
``` wasm
520
-
658
+ %1 = 1 ; %1 的定义
659
+ %2 = 2 ; %2 的定义
660
+ %3 = add %1, %2 ; %3 的定义
521
661
```
522
662
523
663
因为 LLVM IR 是 SSA 的,所有寄存器都是常量,只能在初始化时被赋值一次。所以寄存器的定义是唯一的,也就是初始化的那一次赋值的地方。
@@ -541,7 +681,7 @@ return %y;
541
681
- “定义-使用”关系:通过一个寄存器的“定义”,找到他被哪些寄存器“使用”了。
542
682
- “使用-定义”关系:通过一个寄存器的“使用”,找到他是在哪里被“定义”的。
543
683
544
- > {{ icon.tip }} 就是两个互逆的 map 映射。要注意的是 ,他们都不是一一映射:一个寄存器可以被很多其他寄存器的定义使用 ,一个寄存器的定义也可能使用到了多个其他寄存器。
684
+ > {{ icon.tip }} “定义-使用”和“使用定义”是两个互逆的映射。不过要注意 ,他们都不是一一映射:一个寄存器可以被多个其他寄存器重复使用 ,一个寄存器的定义也可能使用到了多个其他寄存器。
545
685
546
686
##### “使用-定义”映射
547
687
@@ -565,15 +705,15 @@ for (llvm::Use &U: pi->operands()) {
565
705
566
706
LLVM 中的 pass,是指一组对 IR 进行操作的函数。pass 分为分析类 pass 和优化类 pass。
567
707
568
- - 分析类 pass 只是帮助我们观察 IR,获得某些概括信息,并不修改 IR,这里我们要用的 ` def-use ` pass 就属于此类 ,他通过一次遍历找到所有的“定义-使用”映射关系, 只要 IR 不修改,就可以缓存之前的结果,供后来者重复使用。
708
+ - 分析类 pass 只是帮助我们观察 IR,获得某些概括信息,并不修改 IR。现在我们要用的 ` def-use ` pass 就属于分析类 pass ,他通过一次遍历找到所有的“定义-使用”映射关系。 只要 IR 不修改,就可以缓存之前的结果,供后来者重复使用。
569
709
- 优化类 pass 可以修改 IR 的节点,修改 IR 原有的结构,例如之前提到的 ` mem2reg ` pass 就属于此类,他会把所有能优化的 alloca + store 修改成静态单赋值的寄存器。由于会修改 IR,可能导致某些分析 pass 的结果失效,下次再用到时需要重跑。
570
710
571
711
区别:
572
712
573
713
- 分析类 pass 的输入是一段 IR,输出是一个用户自定义的分析结果类型。例如对于 ` def-use ` pass,输出是一个 ` Analysis ` 类型的对象,这个对象中维护了一个 “定义-使用” 的双向映射,我们 ` def-use ` pass 分析得到结果后就可以使用这个映射来查询。
574
714
- 优化类 pass 的输入是一段 IR,输出也是一段 IR,被改变后的 IR。LLVM 实现优化,就是通过一系列优化 pass 的组合完成的。有时,优化 pass 会需要一些分析的结果,才能进行,因此优化 pass 有时会请求一些分析 pass 的结果,LLVM 会检查这个分析之前有没有进行过,如果有,就会复用上次分析的结果,不会重新分析浪费时间。优化 pass 在修改了 IR 后,需要返回一个标志位,表示 IR 修改后,哪些分析 pass 的结果可能会失效。如果不确定,就返回 ` all ` 吧:本优化 pass 修改过 IR 后所有之前分析 pass 缓存的结果都会失效。
575
715
576
- 如何判断一个虚拟寄存器是否可以被优化掉?检测他有没有被别人“使用”, 也就是查询他的“定义-使用”映射,如果发现“使用者列表”为空,就说明没人使用 ,可以优化掉。
716
+ 如何判断一个虚拟寄存器是否可以被优化掉?检测他有没有被别人“使用”: 也就是查询他的“定义-使用”映射,如果发现“使用者列表”为空,就说明该寄存器的“定义”没人使用 ,可以优化掉。
577
717
578
718
### Clang 生成 IR 汇编
579
719
@@ -633,7 +773,7 @@ Clang 编译时是什么平台就是什么平台了,不同目标平台的 IR
633
773
634
774
IR 字节码中的每个(或多个)字节都可以和 IR 汇编中的一行 IR 指令一一对应。
635
775
636
- ### IR 汇编 vs IR 字节码
776
+ ### IR 汇编和 IR 字节码的不同之处
637
777
638
778
#### 后缀名不同
639
779
@@ -772,6 +912,79 @@ llvm-link test1.bc test2.bc -o test.bc
772
912
773
913
> {{ icon.fun }} 把 C++ 文件编译生成的字节码模块链接起来,就像很多个 C++ 文件突然国宝特工合体一样。
774
914
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
+
775
986
## 汇编语言(ASM)
776
987
777
988
## 汇编语言的终局:机器码
989
+
990
+ ## 构建好了吗
0 commit comments