llvm IR指令

本文介绍了LLVMIR中的模块、函数、指令、基本块等概念,强调了内存操作如何通过alloca、store和load实现,并解释了SSA(静态单赋值)结构在处理局部变量时的作用,以及phi指令在合并控制流中的关键作用。同时提到了Clang前端如何生成LLVMIR并进行优化的过程。
摘要由CSDN通过智能技术生成

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指令为局部变量在栈上分配内存,并使用loadstore指令来读取和修改这些变量的值。这样,局部变量就通过指针访问变成了内存位置,而不是直接的SSA values。

但如果您说的是在某些情况下,局部变量可以被优化成普通的SSA values而不再需要alloca/load/store,这是正确的。编译器在进行优化时,如果它能确定局部变量的生命周期和赋值情况,就有可能将这些变量转换为SSA形式的直接值。这种转换通常发生在如下情况:

  1. 变量只被赋值一次:如果局部变量只被赋值一次并且不再改变,那么它就可以被看作是一个SSA value。

  2. 优化导致的常量传播:如果局部变量被赋值为一个常量,并且这个常量在后续代码中没有被修改,那么编译器可能会直接使用该常量值,而不是通过内存位置来访问它。

  3. 变量被提升到寄存器:在某些情况下,如果编译器认为将局部变量保持在寄存器中更为高效,它可能会选择这样做,并直接操作寄存器中的值而不是通过内存。

在这些情况下,原本需要通过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


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值