GVN 优化中的负载消除简介
GVN 优化(opt -gvn)的一个非常重要的优化是负载消除。负载消除涉及多个子系统(包括别名分析、内存依赖分析、SSA 构造、PHI 节点转换),并且具有很多方面(完全冗余消除 vs 部分冗余消除、值强制、处理 memset/memcpy 等)。在这篇文章中,我将介绍并阐述这个主题,以便我们在以后的文章中进行扩展。基本冗余负载消除
GVN(代表“全局值编号”,尽管这个细节与负载消除无关)负责一种称为**冗余**负载消除的负载消除形式。这是消除其值已经可用的负载(其他形式的负载消除包括删除**死**负载)。一个简单的冗余负载示例是
%x = load i32* %P
%y = load i32* %P ; <- Redundant
在这个例子中,我们可以删除第二个负载并用 %x “替换所有使用”,因为地址 %p 处的值已经可用,值为“i32 %x”。另一种简单的冗余消除形式来自于存储,例如
store i32 4, i32* %P
%a = load i32* %P
在这个例子中,我们可以用 4 替换 %a。这种变换可以推广到支持其他操作:GVN 优化可以将 memset 转发到负载,将来自常量全局变量的 memcpy 转发到负载等。请注意,GVN 不允许优化掉易失性负载。
这个实现非常简单:GVN(通过 memdep 类)从我们试图消除的负载开始,向后扫描到该块的顶部,直到它找到提供该值的指令(如这些示例所示),或者直到它找到可能以未知方式影响内存的指令,例如调用指令。如果我们找到了可能覆盖内存位置的指令,我们就无法消除负载。
然而,直线代码非常无聊,让我们来看看更复杂的例子。
GVN 中的 SSA 构造
GVN 还可以消除非本地负载,这可能需要插入 PHI 节点。这是一个简单的示例
BB1:
store i32 5, i32* %P
br label %Merge
BB2:
%X = load i32* %P
... use %X ...
br label %Merge
Merge:
%Y = load i32 *%P
...
... use %Y ...
在这种情况下,GVN 在该块内(从 %Y 负载开始)扫描可用的 %P 值,并遇到 %Merge 块的顶部。由于它已经到达块的顶部,它开始扫描前驱块(%BB1 和 %BB2),发现负载的值在 BB1 中是 5,在 BB2 中是 %X。然后,GVN 使用 SSAUpdater 类重命名这些值,生成
BB1:
store i32 5, i32* %P
br label %Merge
BB2:
%X = load i32* %P
... use %X ...
br label %Merge
Merge:
%Y = phi i32 [5, %BB1], [%X, %BB2]
...
... use %Y ...
用 PHI 节点替换负载可能看起来不像是一种收益,但是我们在 LLVM 优化器中使用的成本模型假设 PHI 节点将由代码生成器合并掉,因此是免费的。执行 PHI 节点插入的逻辑包含在 SSAUpdater 类中,并且由它维护,这可能是未来博客文章的主题。
冗余负载消除的优缺点
我们总是认为,在优化器中尽可能地消除负载是有利的。负载可能非常昂贵(例如,如果它们在缓存中未命中),而负载/存储流量可能会隐藏其他冗余或更易于简化的逻辑,而这些逻辑是标量优化器无法识别到的。用于测试别名分析的一种习惯用法通常如下所示
%A = load i32* %P
store i32 1, i32* %Q
%B = load i32* %P
%C = sub i32 %A, %B
ret i32 %C
如果 GVN + instcombine 能够将其转换为“ret i32 0”,那么我们就知道别名分析已经证明 P 和 Q 没有别名。虽然这在现实世界中不太可能发生,但这只是一个例子,它表明了一个标量优化(X-X == 0),除非消除冗余负载,否则无法完成。
消除冗余负载的代价是会创建更长的活动范围,而代码生成器可能无法应对。这是一个我们目前没有好的解决方案的真实问题。最终的答案是改进的重构将能够在代码中更进一步地重构负载以减少寄存器压力,但我们目前还没有很好的支持。