IR的组织方式和抽象大致都在下图:
- Module:最high level的概念,一个Module相当于一个程序。
- Target information:描述目标平台的特性,比如大小端,操作系统等。
- Function:一个module里面可以包含多个Function,用一个Function List表示。
- Instruction:类似于汇编指令。LLVM IR中有很多种不同的Instruction,比如load,store,add,call等等,每种指令用于实现特定的功能。
- Basic Blocks:一个Instruction容器,简称做BB,其中可以包含多条串行执行的Instruction。每一个BB有一个唯一的label。一个LLVM Function可以包含多个BB。BB之间可以跳转,所以一个Function可以表示成由BB构成的Control Flow Graph。
- Global Variables:简单的看,就是全局变量
All things are Value (应该说module以下吧,module又不是value)
其次就是use的概念,其实就是链表
举个例子,IR指令类型中BinaryOperator对象的内存布局:
引用:https://www.zhihu.com/question/41999500/answer/110220607
在创建一个新的BinaryOperator指令的时候,Create() -> BinaryOperator() -> init() ,在init()函数中构造一个新的Use对象来维护def-use关系。前面曾经提到过,每次创建一个新的Use对象时,就通过Use的构造函数将Use对象挂载到Value的Uses链表上。如下示例代码:
int add()
{
int start = 10;
int end = 10;
return start + end;
}
%start会被用到两次,一次是赋值"int start = 10;",一次是加法操作"return start + end;"
%start = alloca i32, align 4 ; <i32*> [#uses=2]
%end = alloca i32, align 4 ; <i32*> [#uses=2]
store i32 10, i32* %start ; use %start
store i32 10, i32* %end ; use %end
%tmp = load i32* %start ; use %start
%tmp1 = load i32* %end ; use %end
%add = add nsw i32 %tmp, %tmp1
LLVM IR借助“memory不是SSA value”的特点来开了个后门来妥协:前端在生成LLVM IR时,可以选择不生成真正的SSA形式,而是把局部变量生成为alloca/load/store形式:
- 用alloca来“声明”变量,得到一个指向该变量的指针;
- 用store来把值存进变量里;
- 用load来把值读出为SSA value。
引用:https://www.zhihu.com/question/41999500/answer/93243408
ir之间,因为是ssa形式,每一个变量的创建和被变成operand使用都有一个uses结构(数组或者链表),在定义时,该变量就继承并初始化了uses,这对以编译器掌握每个变量的生存周期是重要的基础。
Clang把生成SSA形式的任务交给LLVM处理的:Clang的前端只会把某些临时值生成为SSA value;对显式的局部变量,Clang前端都只是生成alloca/load/store形式的LLVM IR。
在LLVM IR中,局部变量并不总是直接映射为普通的SSA values。SSA(静态单赋值)形式要求每个变量只被赋值一次,这在编译器的优化阶段非常有用,因为它简化了数据流分析和其他优化技术。
然而,局部变量在源代码中通常是可以被多次赋值的,这意味着它们不直接符合SSA的形式。为了解决这个问题,当编译器前端生成LLVM IR时,它通常会使用alloca
指令为局部变量在栈上分配内存,并使用load
和store
指令来读取和修改这些变量的值。这样,局部变量就通过指针访问变成了内存位置,而不是直接的SSA values。
但如果您说的是在某些情况下,局部变量可以被优化成普通的SSA values而不再需要alloca/load/store
,这是正确的。编译器在进行优化时,如果它能确定局部变量的生命周期和赋值情况,就有可能将这些变量转换为SSA形式的直接值。这种转换通常发生在如下情况:
-
变量只被赋值一次:如果局部变量只被赋值一次并且不再改变,那么它就可以被看作是一个SSA value。
-
优化导致的常量传播:如果局部变量被赋值为一个常量,并且这个常量在后续代码中没有被修改,那么编译器可能会直接使用该常量值,而不是通过内存位置来访问它。
-
变量被提升到寄存器:在某些情况下,如果编译器认为将局部变量保持在寄存器中更为高效,它可能会选择这样做,并直接操作寄存器中的值而不是通过内存。
在这些情况下,原本需要通过alloca/load/store
来处理的局部变量可以被优化成更符合SSA形式的直接操作,从而提高了代码的性能和可优化性。然而,这并不意味着所有局部变量都会被这样处理;只有在编译器能够证明这样做是安全且有效的情况下,才会发生这种优化。
交给LLVM之后,经过mem2reg pass,IR才真正进入了普通的SSA形式。
函数:
int foo(int x, bool cond) {
int inc;
if (cond) {
inc = 1;
} else {
inc = -1;
}
return x + inc;
}
Clang的前端生成的LLVM IR是:
; Function Attrs: nounwind uwtable
define i32 @_Z3fooib(i32 %x, i1 zeroext %cond) #0 {
entry:
%x.addr = alloca i32, align 4
%cond.addr = alloca i8, align 1
%inc = alloca i32, align 4
store i32 %x, i32* %x.addr, align 4
%frombool = zext i1 %cond to i8
store i8 %frombool, i8* %cond.addr, align 1
%0 = load i8, i8* %cond.addr, align 1
%tobool = trunc i8 %0 to i1
br i1 %tobool, label %if.then, label %if.else
if.then: ; preds = %entry
store i32 1, i32* %inc, align 4
br label %if.end
if.else: ; preds = %entry
store i32 -1, i32* %inc, align 4
br label %if.end
if.end: ; preds = %if.else, %if.then
%1 = load i32, i32* %x.addr, align 4
%2 = load i32, i32* %inc, align 4
%add = add nsw i32 %1, %2
ret i32 %add
}
可以看到局部变量都在函数的最开头(entry block)有对应的alloca来“声明”它们——申请栈上空间。后面赋值的地方用store、取值的地方用load指令,就跟操作普通内存一样。
而经过mem2reg之后它才真正进入了SSA形式:
; Function Attrs: nounwind uwtable
define i32 @_Z3fooib(i32 %x, i1 zeroext %cond) #0 {
entry:
%frombool = zext i1 %cond to i8
%tobool = trunc i8 %frombool to i1
br i1 %tobool, label %if.then, label %if.else
if.then: ; preds = %entry
br label %if.end
if.else: ; preds = %entry
br label %if.end
if.end: ; preds = %if.else, %if.then
%inc.0 = phi i32 [ 1, %if.then ], [ -1, %if.else ]
%add = add nsw i32 %x, %inc.0
ret i32 %add
}
可以看到进入SSA形式后的LLVM IR里,那些局部变量就变成了普通的SSA value,而不再需要alloca/load/store了。我看代码不同的直观感受就是,没有内存操作了,参数和临时变量都表达为临时寄存器的形式。所以LLVM的mem2reg pass本质上就是识别“局部变量”的模式,并对它们构建SSA形式。顾名思义。
phi指令的解释可能不是很好,重新理一下它的作用:
在LLVM IR中,Phi(φ)指令是SSA(静态单赋值)形式的关键组成部分。Phi指令用于在控制流的合并点(如基本块的开头)合并来自不同前驱基本块的值。它表示了一个条件选择:对于每个前驱基本块,Phi指令都有一个对应的输入值,当控制流从该前驱块到达当前块时,就会选择该输入值。
Phi指令的语法通常如下所示:
%result = phi <type> [ %value1, %block1 ], [ %value2, %block2 ], ... |
这里,%result
是Phi指令定义的SSA值,<type>
是该值的类型,%value1
, %value2
, ... 是来自不同前驱基本块的值,而%block1
, %block2
, ... 是这些值所在的前驱基本块。
Phi指令在构建程序的控制流图(CFG)和进行数据流分析时非常重要。它们允许在保持SSA形式的同时,合并来自不同路径的值。在编译器优化过程中,Phi指令也经常被用于传播常量、消除冗余计算等。Phi指令并不直接对应任何具体的机器指令。它是LLVM IR中为了表示SSA形式而引入的一种抽象指令。在实际的代码生成阶段,Phi指令会被转换成具体的机器指令序列,这些指令会根据控制流条件来选择正确的值。
引用:RednaxelaFX