phinode的创建和消除
1. SSA构建算法
依赖:
- dom tree
- DF
- llvm ir基础知识
对以下c代码
1 | int foo(int a){ |
clang会生成类似这种IR
1 | define i32 @foo(i32 %a){ |
我们能看到llvm采用了alloc+load+store指令来实现c中局部变量的存放。
这里参数处理i32 %a; store %a, %pa实际上是个比较复杂的话题,按下不表。
这段IR的有很多冗余的load/store, 有没有什么办法消除掉呢?
SROA(mem2reg)!
预期的IR
1 | define i32 @foo(i32 %a){ |
如果控制流比较复杂呢?
1 | int b = a; |
这里我们发现,如果想要正确删除掉这些load/store,我们需要引入phinode,而且必须拿到控制流信息。phinode是个逻辑节点,在实际的CPU上没对应的指令/寄存器表示。所以我们在生成实际的汇编前需要再删除掉phi。
接下来看如何实现SSA构建:
原理分为两步, insertPHI, Rename, 伪代码如下:
1 |
|
我们看到这种伪代码的形式和LLVMIR区别很大。因为IR格式不一样,接下来看llvm ir下如何实现mem2reg
1 | mem2reg(Function f){ |
当然,以上代码不是llvm mem2reg实际实现代码。
llvm实现做了一些优化,domtree遍历优化,特殊情况处理等。
2. SSA destruction
如何销毁SSA形式,删除phinode。
llvm的phi-elimination是在mir阶段发生。
一般来说,SSA destruction是将phinode替换为copy指令。
- 暴力做法,对于
p = phi (%a1, %bb1), (%a2, %bb2) ....,在bb1,bb2… 尾部插入p = copy %a...然后删除掉phi节点。但是该做法结果不对。
考虑到lost copy problem和swap problem:
1 |
|
我们的暴力算法会生成
1 |
|
split critical edges
图中红边就是critical edge:
1 |
|
对lost copy和swap应用该算法:
1 |
|
swap problem结果看起来不太对哦。
看起来是插入copy出了问题, 再看一眼a2=phi(a1, b2);b2=phi(b1,a2) a2, b2形成了环。
是个环。。。
我们发现swap problem中
- a2和b2的生命周期重叠了
- 并且插入copy方式也需要改进
isolating phi + parallel copy
先介绍几个概念
- CSSA(Conventional SSA) form is defined as SSA form for which each phi-web is interference-free.
- TSSA(Transformed SSA) form is non-conventional SSA, may have phi-web is not interference-free.
swap problem和lost-copy problem中 都是TSSA,而不是CSSA。
如何将TSSA转换为CSSA?并且在CSSA中插入copy是否还需要注意类似swapproblem的情况?
所以我们接下来的算法步骤是:
- Insert parallel copies for all φ-functions (TSSA => CSSA)
- eliminate phis in CSSA
- Sequentialize parallel copies, possibly with one more variable and some additional copies
- some optimization
首先转换为cssa格式
1 |
|
消除phi节点后:
1 |
|
上一小节提到的swap中copy的问题仍然存在,所以接下来要介绍,parallel copies的概念。
我们将这些插入的copy指令视为parallel copies,然后采用算法求解copy插入顺序
1 | // Revisiting Out-of-SSA Translation for Correctness, Code Quality, and Efficiency (https://inria.hal.science/inria-00349925v1/document) |
不幸的是,这个算法有点小问题。
实际上是个图遍历算法:cc09.pdf
我们用python写个demo:
1 | class Copy: |
运行脚本,啥也不输出。
但是如果我们将b==l改成b!=l, 就会输出
emit copy tmp<- a
emit copy a <- b
emit copy b <- tmp
至此,我们完成了phi-elimination的一半,合法化问题解决了。但是性能问题没解决。
对于lost copy problem,在BB块前插入copy指令是必须的。(如果采用Split Critical Edges的方式,循环的代码质量会下降)
对于swap problem,需检测环的出现。
copy插入位置问题,lost copy 和swap problem插入顺序还不一样。这帮人写论文能不能靠谱点
llvm采用的算法
llvm有split critical edge也有不依赖split critical edge的实现。
parallel copy面对如下代码还是有些问题
1 | // 格式化整型的代码实现 |
原因还是copy插入顺序。(parallel copies的遍历顺序)
解决方法,额。
根据llvm中split critical edges的方法。
如果phi的incoming block是critical edge 的src block,例如 bb0:a0 = phi a1,bb1, a2, bb2 其中bb2是critical edge,那么在bb1尾部创建tmp = copy a1, 在bb0头部a0 = copy tmp, 在bb2尾部tmp = copy a2.
如果没有critical edge, 直接copy,bb1尾部创建a0 = copy a1,bb2尾部a0 = copy a2.
很简单的实现,但是还是挺有效。。。
https://gcc.godbolt.org/z/aMWPjK3a9
为什么不split block呢?因为跳转太多会导致性能下降,特别是在loop中。
优化问题
关键字:copy de-coalescing
即如何最小化插入copy指令
- 在CSSA上面对parallel copies 优化
- 先生成很多冗余的copy指令,然后再优化这些copy指令
第一种方式有些困难,个人认为难点在于parallel copies相关生命周期如何计算。
第二种方式就很传统了, interference graph + union find, 不干涉的放在一个点集里面。
others
我们重新关注下parallel copies 。
1 |
|
注意
\\可能会被识别为\, 用\newline更好
$\begin{bmatrix} a \newline b \newline c \end{bmatrix}
= Φ \begin{bmatrix} a1 & a2 & a3 \newline b1 & b2 & b3 \newline c1 &c2 & c3 \end{bmatrix} $
那么对于bb1来说,就有parallel copies: $a \gets a1, b \gets b1, c \gets c1$。
其对应的Location Transfer Graph为$G = (V, E), V = \lbrace a,b,c, a1, b1, c1\rbrace, E = \lbrace a \gets a1, b \gets b1, c \gets c1 \rbrace$
再看下swap problem里面的
1 | a2 = phi(a1, b2) |
- 前驱1的parallel copies
1 | flowchart TD |
- 前驱2的parallel copies
1 | flowchart TD |
参考
- https://www.cs.utexas.edu/~pingali/CS380C/2010/papers/ssaCytron.pdf
- https://ics.uci.edu/~yeouln/course/ssa.pdf
- Revisiting Out-of-SSA Translation for Correctness, Code Quality, and Efficiency
- cc09.pdf
llvm实现。
1 | int cond_random(); |
lost copy
1 |
|
swap
1 |
|