LLVM笔记(4) - RDF
1. 什么是RDF
RDF(register data flow)是高通在开发Hexagon时实现的一套寄存器分配后dataflow graph. 由于LLVM框架本身推荐在SSA form下实现各类变换, 因此缺少在寄存器分配后的数据流分析工具. 然而在某些情况下(比如copy propagation, 消除循环迭代间依赖, 栈指令转换等必须在寄存器分配后才稳定可知)我们仍需要这样一套工具.
2. RDF的实现思路
在postRA阶段程序处于SA form, 每个node存在若干个定义, 因此RDF目的是实现一个基于SSA form的data flow(即RDF会虚构phi节点来记录数据来源).
3. 基本概念
一个RDF graph是node的集合, 每个node代表程序中的一个元素. node被分为两类: code node与ref node, 前者代表程序的结构(function, basicblock和statement等), 后者代表对寄存器的引用(包含def与use). 对于code node而言, 他有一个单链表指向所有包含的子节点(function node包含basicblock node, basicblock node包含statement node, statement node包含ref node). 对于ref node则更复杂些, 一个def node包含reaching def, reached def和sibling, 一个use node则包含reaching def和sibling. 关于reaching def的概念, 引用wiki的说明: In compiler theory, a reaching definition for a given instruction is an earlier instruction whose target variable can reach the given one without an intervening assignment. 即若A定义的变量X作用域可达到B, 则A是B的reaching def, 反之则是reached def. 至于sibling则指代下一个与当前节点共用同一reaching def的节点.
4. 打印格式
为了能清晰的表述复杂的DFG, RDF约束了以下的打印格式.
code node记作kN, 其中k为类型, N为node id, code node类型有:
f - function
b - basic block
p - phi
s - statement
d -def
u - use
def node记作dN<R>(rd, d, u):sib
use node记作uN<R>[!](rd):sib, 其中:
N - node id
R - register being defined/used
rd - reaching def
d - reached def
u - reached use
sib - sibling
此外对于特殊寄存器还有以下助记符:
+ - preserving def, 该def仅作用于寄存器的部分, 其余位不变
~ - clobbering def
" - shadow ref, 该引用包含多个reaching def(e.g. 引用一个寄存器对, 但其中的寄存器由多条指令分别定义)
! - fixed register, 固定寄存器不可修改
让我们以下代码为例看下一个RDF graph的全貌. 由于RDF代码还未合入公共框架, 我们需要在hexagon架构下编译. 以下截取部分打印.
1 [19:43:48] hansy@hansy:~$ cat ~/test.c 2 int sigma(int cnt) 3 { 4 int sum = 0, i = 0; 5 for (i = 0; i < cnt; i++) 6 sum += i; 7 return sum; 8 } 9 10 [19:43:55] hansy@hansy:~$ ~/llvm/llvm_build/bin/clang --target=hexagon test.c -O2 -w -S -mllvm -rdf-dump 2>1.ll 11 12 Function Live Ins: $r0 13 14 bb.0.entry: 15 successors: %bb.1(0x50000000), %bb.2(0x30000000); %bb.1(62.50%), %bb.2(37.50%) 16 liveins: $r0 17 renamable $p0 = C2_cmpgti renamable $r0, 0 18 renamable $r1 = A2_tfrsi 0 19 J2_jumpf killed renamable $p0, %bb.2, implicit-def dead $pc 20 J2_jump %bb.1, implicit-def dead $pc 21 22 bb.1.for.body.preheader: 23 ; predecessors: %bb.0 24 successors: %bb.2(0x80000000); %bb.2(100.00%) 25 liveins: $r0 26 renamable $r1 = A2_addi renamable $r0, -1 27 renamable $r2 = A2_addi renamable $r0, -2 28 renamable $d1 = M2_dpmpyuu_s0 killed renamable $r1, killed renamable $r2 29 renamable $d1 = S2_lsr_i_p killed renamable $d1, 1 30 renamable $r1 = S4_addaddi renamable $r2, killed renamable $r0, -1 31 32 bb.2.for.end: 33 ; predecessors: %bb.0, %bb.1 34 liveins: $r1 35 $r0 = COPY killed renamable $r1 36 PS_jmpret $r31, implicit-def dead $pc, implicit $r0 37 38 # End machine code for function sigma. 39 40 Starting copy propagation on: sigma 41 DFG dump:[ 42 f1: Function: sigma 43 b2: --- %bb.0 --- preds(0): succs(2): %bb.1, %bb.2 44 p39: phi [+d40<R0:00000001>(,d33,u30):] 45 s3: C2_cmpgti [d4<P0>(,,u10):, u5<R0>(+d40):] 46 s6: A2_tfrsi [d7<R1>(,d15,u47):] 47 s8: J2_jumpf %bb.2 [/+d9<PC>!(,d12,):, u10<P0>(d4):] 48 s11: J2_jump %bb.1 [d12<PC>!(/+d9,\d36,):] 49 50 b13: --- %bb.1 --- preds(1): %bb.0 succs(1): %bb.2 51 s14: A2_addi [d15<R1>(d7,d28,u22):, u16<R0>(+d40):u5] 52 s17: A2_addi [d18<R2>(,d21,u23):, u19<R0>(+d40):u16] 53 s20: M2_dpmpyuu_s0 [d21<D1>(d18,d25,u26):, u22<R1>(d15):, u23<R2>(d18):] 54 s24: S2_lsr_i_p [d25<D1>(d21,,u44):, u26<D1>(d21):] 55 s27: S4_addaddi [d28<R1>(d15,,u48):, u29<R2>(d25):, u30<R0>(+d40):u19] 56 57 b31: --- %bb.2 --- preds(2): %bb.0, %bb.1 succs(0): 58 p41: phi [+d42<D1>(,,):, u43<D1>(,b2):, u44<D1>(d25,b13):u29] 59 p45: phi [+d46<R1>(,,u34):, u47<R1>(d7,b2):, u48<R1>(d28,b13):] 60 s32: COPY [d33<R0>(+d40,,u38):, u34<R1>(+d46):] 61 s35: PS_jmpret [\d36<PC>!(d12,,):, u37<R31>!():, u38<R0>!(d33):] 62 63 ]
我们截取两段打印, 第一部分是hexagon的MIR, 第二部分是MIR对应的DFG. 第一行是函数定义, 对应DFG中的节点是f1, 表示该节点序号为1且为function node, 同理第二行的b2表示该节点序号为2且为basic block node. 第三行为phi node, 关于phi node后文再详细描述, 第四行为statement node, 它引用了两个寄存器, 其中P0是def, 对应节点序号为4, 由于之前及之后再无定义P0, 因此reaching def与reached def均为空, 该寄存器在第六行中被使用, 故reached use为u10, 另一寄存器R0为use, 对应节点序号为5, 它是作为caller save寄存器传入的, 因此reaching def为d40(phi节点), 由于它是第一次使用R0所以没有sibling. 顺着R0查找下一使用R0的是u16, 它的sibling是u5. 再来看下特殊寄存器PC, 在s8中第一次定义该寄存器, /表示该值undef属性, +表示该值部分位保留, !表示该寄存器为固定寄存器不可替换. PC的另外两处定义为d12与d36, 其中d36为最后一个定义(且无引用), 所以添加\表示dead属性.
通过这些助记符可以帮助我们快速熟悉程序数据流图.
5. 代码结构
(用于构建graph的代码)一共分为三个源文件: RDFGraph.cpp(构建graph的核心代码), RDFLiveness.cpp(计算phi节点)与RDFRegisters.cpp(分析alias register).
5.1. 数据结构
RDF作者在实现RDF之初就把两个问题列入考量: 一是如何实现fake phi node, 二是如何减少内存开销. 从上面的例子可以看出: 由于是函数级优化可能产生大量节点(通过通用库函数分配耗时), 不同节点携带信息不同(所占用内存的大小也不同), 如何在高效分配/回收节点同时节省每个node的内存成为实现(阅读代码)的一个问题. RDF作者使用一个通用的类NodeBase(仅32byte, 通过union复用内存)及一个静态分配器NodeAllocator实现内存管理. NodeAllocator使用BumpPtrAllocatorImpl获取内存, 后者是一个llvm共用的slba分配器. NodeAllocator按块申请内存, 每个块大小占64K, 一块耗尽后再申请新的块. 对内每个块都看作一个NodeBase的数组, 通过游标计算出偏移与序号, 申请NodeBase时直接返回偏移地址(见NodeAllocator::New()).
5.2. RDFGraph
RDFGraph定义所有构建图所需的数据结构. 首先是最基础的rdf::DataFlowGraph(defined in RDFGraph.h), 这个类是我们使用RDFGraph的入口. rdf::DataFlowGraph::build()会构建(通过构造函数指定的MachineFunction的)graph. 构建分三步, 首先根据读入的MIR顺序的为每个instruction和register分配node, 这就是为什么上面例子中node都是顺序排布的(利于快速查找). 其次为live-ins与landing pad创建phi node, live-ins即函数入口已经live的寄存器(e.g. argument register), landing pad指由异常处理跳转到该block时定义的register. 最后为所有block构建phi node(所以phi节点序号都是在最后的).
对于没有change of flow的情况一切都很好理解. 但当控制流影响数据流时就需要一些额外的改变. 在SSA模式下有phi节点让我们可以获取寄存器的唯一定义, 而post RA阶段就需要我们自己构建fake phi node. DataFlowGraph::recordDefsForDF()(defined in RDFGraph.cpp)负责遍历每个BB, 记录该BB定义的节点并将其与该BB的IDF建立映射. DataFlowGraph::buildPhis()(defined in RDFGraph.cpp)通过该映射表为每个BB建立phi节点: 首先移除映射表中的重复定义(e.g. 定义sub reg后再定义同一寄存器的super reg), 然后遍历本BB内所有ref node, 如果映射表中存在对该node所指寄存器的定义则为其创建一个phi node, 并添加一个对应的phi use. 所有phi node建立后还要调用DataFlowGraph::linkBlockRefs()(defined in RDFGraph.cpp)更新ref node之间的链表. 最后根据build()接口传入的选项决定是否调用DataFlowGraph::removeUnusedPhis()(defined in RDFGraph.cpp)删除无用的phi node, 一个完整的RDF graph就建立完成了. 以上文例子为例, phi node(41)虚构了一个def node(42), 该node 引用了两个use node, 分别是来自b2的def node(来源于live-ins, 无def node)与来自b13的def node(25). 所有在本BB内的引用都被替换为42(在该BB内并无引用, 只是因为KeepDeadPhis选项未被删除).
5.3. RDFLiveness
如果每次修改后重新更新graph都需要上述流程那就太繁琐了, 实际上我们不需要再重新分配node, 只需计算reaching def等属性即可. Liveness::computeLiveIns()(defined in RDFLiveness.cpp)就是实现这个功能.
5.4. RDFRegisters
用于分析alias register以及register mask(call指令常出现)的情况.