PHI指令在LLVM中有什么作用,如何使用?

124

LLVM有一个名为phi的指令,其解释相当奇怪:

'phi'指令用于实现表示函数的SSA图中的φ节点。

通常用于实现分支。如果我理解正确,它需要使依赖分析成为可能,并且在某些情况下可以帮助避免不必要的加载。然而,它仍然很难理解它到底做了什么。

Kaleidoscope example 对于if情况解释得相当清楚。但是如何实现逻辑操作例如&&||就不那么清晰了。如果我在online llvm编译器中输入以下内容:

void main1(bool r, bool y) {
    bool l = y || r;
}

最后几行完全让我困惑:

; <label>:10                                      ; preds = %7, %0
%11 = phi i1 [ true, %0 ], [ %9, %7 ]
%12 = zext i1 %11 to i8

看起来phi节点产生了可以使用的结果。我曾经认为phi节点只是定义了从哪些路径获取值。

有人能解释一下什么是Phi节点,以及如何使用它实现||吗?


4
phi节点是编译器将IR转换为“静态单赋值”形式的问题的解决方案。为了更好地理解这个解决方案,我建议先更好地理解这个问题。因此,我会向您推荐一个链接:“为什么需要phi节点”。 - Vraj Pandya
3个回答

106

Phi节点是一种指令,用于根据当前块的前驱选择一个值(查看这里以查看完整的继承层次结构 - 它也被用作值之一,它继承自其中的类)。

由于LLVM代码采用SSA(静态单赋值)风格的结构,因此Phi节点是必需的。例如,以下C++函数:

void m(bool r, bool y){
    bool l = y || r ;
}

被翻译成下面的 IR(通过 clang -c -emit-llvm file.c -o out.bc 创建,并通过 llvm-dis 查看)

define void @_Z1mbb(i1 zeroext %r, i1 zeroext %y) nounwind {
entry:
  %r.addr = alloca i8, align 1
  %y.addr = alloca i8, align 1
  %l = alloca i8, align 1
  %frombool = zext i1 %r to i8
  store i8 %frombool, i8* %r.addr, align 1
  %frombool1 = zext i1 %y to i8
  store i8 %frombool1, i8* %y.addr, align 1
  %0 = load i8* %y.addr, align 1
  %tobool = trunc i8 %0 to i1
  br i1 %tobool, label %lor.end, label %lor.rhs

lor.rhs:                                          ; preds = %entry
  %1 = load i8* %r.addr, align 1
  %tobool2 = trunc i8 %1 to i1
  br label %lor.end

lor.end:                                          ; preds = %lor.rhs, %entry
  %2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]
  %frombool3 = zext i1 %2 to i8
  store i8 %frombool3, i8* %l, align 1
  ret void
}

那么这里会发生什么呢? 与 C++ 代码不同,变量 bool l 在 LLVM IR 中必须被定义一次,它只能是 0 或 1。所以我们检查 %tobool 是否为 true,然后跳转到 lor.endlor.rhs

lor.end 中,我们最终得到了 || 运算符的值。如果我们从入口块进入,则其值为 true。否则,它等于 %tobool2 的值——这正是我们从下面的 IR 行中得到的:

%2 = phi i1 [ true, %entry ], [ %tobool2, %lor.rhs ]

14
TL;DR φ节点是一个三元表达式。有人可能会争辩说它不包含条件,但实际上,在转换为最终代码时,无法确定哪个参数是活动的,所以φ也必须具有条件。 - Hi-Angel
3
你也可以使用clang -S -emit-llvm -o -(而不是bc和llvm-dis)。 - Et7f3XIV
使用 clang --target=aarch64-linux-gnu -S -emit-llvm -fno-discard-value-names -O3 .\sourceFile.cpp 命令查看变量名。 - CiaranWelsh

44

您不需要使用phi。只需创建一堆临时变量。LLVM优化会处理掉这些临时变量,并自动使用phi节点进行优化。

例如,如果您想做到这一点:

x = 4;
if (something) x = x + 2;
print(x);
您可以使用Phi节点实现这个功能(伪代码):
  1. 将4赋值给x1
  2. 如果(!something),跳转到4
  3. 通过将2添加到x1中计算x2
  4. 从x1和x2分别赋值给x3 phi
  5. 使用x3调用print
但是,您也可以不使用Phi节点(伪代码):
  1. 在堆栈上分配名为x的本地变量
  2. 将4的值加载到temp x1中
  3. 将x1存储到x中
  4. 如果(!something),跳转到8
  5. 将x加载到temp x2中
  6. 将4与x2相加得到temp x3
  7. 将x3存储到x中
  8. 将x加载到temp x4中
  9. 使用x4调用print
通过运行LLVM的优化传递,第二段代码将被优化为第一段代码。

5
根据我所读的,似乎需要注意一些限制。mem2reg 是所讨论的优化过程,在Kaleidoscope例子中指出了它的一些限制。不过,这似乎是处理问题的首选方式,并且被Clang使用。 - Matthew Sanders

21
现有的答案很好。但是,我想让它更简单、更短一些。
block3:
    %result = phi i32 [%a, %block1], [%b, %block2]

这意味着如果前一个块是block1,选择值a。如果前一个块是block2,选择值b
为什么我们要这样写呢?这是为了防止在两个不同的块中分配result,比如if块和else块。因为我们不想违反SSA原则。SSA帮助编译器应用各种优化,并且它是中间代码的事实标准。有关更多信息,请参考this reference

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接