LLVM内存和SSA的介绍

766 阅读5分钟

在这篇文章中,我们将了解关于SSA和LLVM内存的所有信息,以及这两者之间的关系。

目录:

  1. 简介
  2. 可变变量
  3. LLVM内存
  4. 总结
  5. 参考文献

先决条件

Kaleidoscope中的用户定义操作符。

介绍

在以前的文章中,我们学习了如何构建和表示AST,生成LLVM IR,优化代码和JIT编译。

SSA是IR(中间表示法)的一种属性,它要求每个变量只被分配一次,并且每个变量在使用前都要被声明。

SSA很有用,因为它简化并改善了编译器优化的结果。通过SSA改进的编译器优化算法包括:常量传播、死代码消除、全局变量编号、值范围传播等。

Kaleidoscope这样的功能语言可以很容易地直接以SSA形式生成LLVM IR。LLVM也要求其输入是SSA形式的。
一般来说,由于LLVM提供了高度调整的测试支持,所以不需要编译器前端来构建SSA。

可变变量

在构建SSA的过程中,可变变量会引起复杂的问题,例如,让我们看看下面的代码:

int G, H;
int test(_Bool Condition) {
  int X;
  if (Condition)
    X = G;
  else
    X = H;
  return X;
}

上面,我们有一个变量X,它的值取决于程序中的执行路径。X有两种可能,我们插入一个PHI节点,将这两个值合并。
我们得到的是LLVM IR:

@G = weak global i32 0   ; type of @G is i32*
@H = weak global i32 0   ; type of @H is i32*

define i32 @test(i1 %Condition) {
entry:
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  br label %cond_next

cond_next:
  %X.2 = phi i32 [ %X.1, %cond_false ], [ %X.0, %cond_true ]
  ret i32 %X.2
}

上面,来自GH全局变量的负载是显式的,并且存在于if语句的then/else分支中。
为了合并进入的值,cond_next块中的X.2
phi节点根据控制流的方向选择正确的值来使用。

LLVM内存

LLVM要求所有的寄存器值都是SSA形式的,另一方面,它不要求内存对象是SSA形式的。在LLVM中,我们使用按需计算的分析通道来处理内存的数据流分析,而不是将其纳入LLVM的IR中。
关于LLVM分析通道的更多信息,这个链接
很有帮助。

为了利用这个技巧,我们首先讨论一下堆栈变量在LLVM中是如何表示的。

LLVM中的内存访问是明确的加载/存储指令。它们被设计为不需要操作符的地址。堆栈变量是在LLVM的allocal指令中声明的。

代码显示了我们如何在LLVM IR中声明和操作一个堆栈变量:

define i32 @example() {
entry:
  %X = alloca i32           ; type of %X is i32*.
  ...
  %tmp = load i32, i32* %X  ; load the stack value %X from the stack.
  %tmp2 = add i32 %tmp, 1   ; increment it
  store i32 %tmp2, i32* %X  ; store it back
  ...

alloca指令分配的堆栈内存是通用的,我们可以将堆栈槽地址传递给函数或存储在其他变量中。
我们可以使用alloca技术编写上述例子,避免使用PHI 节点:

@G = weak global i32 0   ; type of @G is i32*
@H = weak global i32 0   ; type of @H is i32*

define i32 @test(i1 %Condition) {
entry:
  %X = alloca i32           ; type of %X is i32*.
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  store i32 %X.0, i32* %X   ; Update X
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  store i32 %X.1, i32* %X   ; Update X
  br label %cond_next

cond_next:
  %X.2 = load i32, i32* %X  ; Read X
  ret i32 %X.2
}

我们应该注意以下关于处理任意可变变量而不创建PHI节点的问题:

  • 每一个可改变的变量都成为堆栈分配
  • 每个变量的读取都会成为堆栈的负载
  • 取一个变量地址直接使用堆栈地址

我们解决了中间问题,但引入了另一个问题,一个性能问题。这个问题是由简单和常见的表达式的大量stakc流量引起的。

LLVM优化器有一个名为mem2reg的优化器通道,可以处理这种情况。
当我们执行上述例子时,我们会有以下结果:

$ llvm-as < example.ll | opt -mem2reg | llvm-dis
@G = weak global i32 0
@H = weak global i32 0

define i32 @test(i1 %Condition) {
entry:
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  br label %cond_next

cond_next:
  %X.01 = phi i32 [ %X.1, %cond_false ], [ %X.0, %cond_true ]
  ret i32 %X.01
}

我们使用mem2reg通证来实现构建SSA的standad算法,并有一些优化措施来加速退化的情况。
mem2reg是处理可变变量的解决方案。它只对特定情况下的变量起作用:

  • 首先,mem2reg是alloca驱动的--它找到alloca,如果它能处理它们,它就提升它们。这不适用于全局变量或堆分配

  • mem2reg只在函数入口块中寻找 alloca 指令,
    在入口块中保证 alloca 被执行一次,这使得分析更简单

  • mem2reg只促进那些使用直接加载和存储的 allocas

  • 最后,mem2reg只在allocas的数组大小为1时才工作,但它不支持结构体或数组到寄存器

上述属性

总结

SSA是IR(Intermediate representation)的一个属性,它要求每个变量只被分配一次,并且每个变量在使用前都要被声明。
Kaleidoscope 等功能语言使得直接以SSA形式生成LLVM IR变得很容易。 可变的变量在构建SSA的过程中会造成复杂性。 LLVM要求所有的寄存器值都是SSA形式的,LLVM中的内存访问是通过加载/存储 指令明确的。

参考文献

Kaleidoscope中的变量突变