llvm中端之mem2reg源码分析
引言
mem2reg是llvm中间IR层构建SSA的一个pass。它将store和load指令替换成基于寄存器的依赖,同时对分歧处插入PHI指令消除多def的影响。
注:参考源码路径为 https://github.com/llvm/llvm-project/tree/release/10.x
1 依赖简介
mem2reg源码依赖了一些封装库,包括分析器、util、mem2reg自身的封装。
- DominatorTree:由DominatorTreeAnalysis生成,用于获取函数的支配树,建立函数基本块之间的支配关系;
- AssumptionCache:由AssumptionAnalysis生成,用于注册ICmpInst指令和建立影响该指令结果的映射关系。对后续的指令预判有用;
- ValueTracking:依赖AssumptionCache,对Value跟踪分析并作一些结果判断;
- SimplifyQuery:依赖AssumptionCache和DominatorTree,简化查询变量,用于简化一些IR。例如折叠一些编译期可以确定结果的计算;
- ForwardIDFCalculator:类型为IDFCalculator < false >,用于构建节点的支配边界。节点P的支配边界表示:在P的所有可达节点集合中,剔除P的支配节点、再剔除能被集合中其他节点支配的节点,最后剩下的节点就是P的支配节点;
- AllocaInfo:在mem2reg源码处封装的功能。用于迭代AllocaInst的def和use信息,生成DefiningBlocks(每个store记录一次其基本块,可能存在重复)、UsingBlocks(每个load记录一次其基本块,可能存在重复)、OnlyStore(最后一个def基本块)、OnlyBlock(第一个def或use的基本块)、OnlyUsedInOneBlock(为true表示所有def和use在一个基本块中);
- RenamePassData:在mem2reg源码处封装的结构体,表示一个基本块对另一个基本块的输入Incoming。BB代表基本块、Pred代表BB的一个前继节点、Values代表Pred对BB关于AllocaInst的输入Incoming、Locations代表Pred对BB关于AllocaInst的DebugLoc输入Incoming。Values和AllocaInst的数组下标代表在AllocaInst缓存数组中的下标;
2 源码解析
mem2reg的源码过程分为如下基本:
- 通过isAllocaPromotable找出可以替换的AllocaInst;
- 在PromoteMem2Reg::run中,首先对一些简单情况作替换;
- 在PromoteMem2Reg::run中,计算要插入PHI指令的基本块、并插入PHI;
- 通过PromoteMem2Reg::RenamePass进行数据流过程,为PHI添加Incoming、对基本块内部的load替换;
- 在PromoteMem2Reg::run中,清理Allocas和简化PHI指令;
- 在PromoteMem2Reg::run中,为不可达输入设置UndefValue;
2.1 isAllocaPromotable
该函数用于筛选可替换的alloc指令。当且仅当满足如下条件一个或多个才可以被替换,或者说不满足如下条件的alloc都不会替换:
- AllocaInst的user是LoadInst,且LoadInst->isVolatile()为false;
- AllocaInst的user是StoreInst,且StoreInst->isVolatile()为false、同时第一个操作数不能是当前AllocaInst。
- AllocaInst的user是Intrinsic::lifetime_start或Intrinsic::lifetime_end指令;
- AllocaInst的user是BitCastInst,且BitCastInst是将AllocaInst转换成Int8Ptr、同时BitCastInst的user是Intrinsic::lifetime_start或Intrinsic::lifetime_end指令;
- AllocaInst的user是GetElementPtrInst,且GetElementPtrInst是将AllocaInst转换成Int8Ptr、同时GetElementPtrInst的索引参数必须都是0、同时GetElementPtrInst的user是Intrinsic::lifetime_start或Intrinsic::lifetime_end指令;
2.2 简单条件的替换
在PromoteMem2Reg::run中的第一个for循环,对每个AllocaInst的简单情况进行替换。具体如下:
- 通过removeLifetimeIntrinsicUsers函数,移除AllocaInst指令的BitCastInst、GetElementPtrInst、Intrinsic::lifetime_start或Intrinsic::lifetime_end类型的user,并对BitCastInst和GetElementPtrInst的user也进行移除。即只保留LoadInst和StoreInst的user;
- 通过AllocaInfo::AnalyzeAlloca构建alloc的基本块信息;
- 在rewriteSingleStoreAlloca中,处理alloc只有一个StoreInst的情况。其实现思想是只处理store指令能支配的LoadInst(如果不在一个基本块通过支配树判断、否则通过在基本块中的索引判断),对于不处理的LoadInst需要将其基本块在AllocaInfo::UsingBlocks中保留,否则移除;
- 若rewriteSingleStoreAlloca处理完alloc的user,则清理alloc后重新下一个循环,否则继续;
- 在promoteSingleBlockAlloca中,处理StoreInst和LoadInst都在一个基本块中的情况。其核心思想是,对于每个LoadInst,查找其在哪个StoreInst前面,然后用该StoreInst的前一个的第0个操作数替换LoadInst;特别地,如果LoadInst前面没有StoreInst,则用UndefValue替换;
- 若promoteSingleBlockAlloca处理完alloc的user,则清理alloc后重新下一个循环,否则继续;
2.3 计算PHI插入点
在PromoteMem2Reg::run中的第一个for循环后段,用于计算PHI节点插入点、并创建PHI节点。具体如下:
- 在PromoteMem2Reg::ComputeLiveInBlocks中,用于生成ForwardIDFCalculator的LiveIn参数。通过函数第一个for循环迭代,剔除AllocaInfo::UsingBlocks中出现StoreInst在所有LoadInst前面的基本块,就是LiveInBlocks的初始集合;继续通过while循环,将LiveInBlocks的每个基本块向前迭代,将不在DefBlocks中的前继节点也加入到LiveInBlocks。最后得到ForwardIDFCalculator的LiveIn参数;
- 通过ForwardIDFCalculator::calculate(该方法为模板方法)计算需要插入PHI指令的基本块。这些基本块在输出的vector中可能会出现重复,因为如果某个基本块是N个def块的支配边界点,那么就会出现N个重复;
- 通过PromoteMem2Reg::QueuePhiNode对每个需要生成PHI指令的基本块构建PHINode。因为会出现重复的基本块,所以NewPhiNodes中已经构建则返回false;特别地,此时只是构建了PHINode,并没有添加addIncoming。
注:在第一个for后半段,还会建立AllocaInst、PHINode有关的各个索引,方便后续查找。
2.4 PromoteMem2Reg::RenamePass
在进入RenamePass前(也就是紧接着PromoteMem2Reg::run的第一个for后),构建entry基本块的RenamePassData作为第一个处理基本块、并追加到RenamePassWorkList中;然后,通过循环每次弹出RenamePassWorkList最后一个元素,送入RenamePass中执行,这个循环就是将RenamePass的递归调用转为非递归。RenamePass核心逻辑如下:
- 如果当前处理的基本块BB的第一个指令是之前通过PromoteMem2Reg::QueuePhiNode插入的PHI节点,由于同一个基本块可能插入多个不同的AllocaInst的PHI节点,则对每个PHI节点进行处理:在IncomingVals和IncomingLocs中找到对应AllocaInst的store value和DebugLoc的Incoming,然后进行addIncoming操作、并更新IncomingVals参数。特别地,LLVM 10.x有个不太严谨的逻辑是在迭代后续的PHI没有继续判断是否为先前调用QueuePhiNode插入的节点,只是简单粗暴用NumOperands是否相等判断。
- 进行函数退出条件判断,如果Visited缓存中已经插入过BB,即已经被处理过BB基本块,则退出函数;
- 迭代基本块的每条指令、直到遇到基本块的终结指令。如果是StoreInst指令、且存储的地址是之前提取的AllocaInst,则将IncomingVals中对应的指针替换为StoreInst的第一个操作数,将IncomingLocs替换为StoreInst的的DebugLoc;如果是LoadInst、且加载的指针为之前提取的AllocaInst,则用IncomingVals中对应值替换LoadInst的user。过程中虽然isKnownNonZero判断为false、但是LoadInst的meta data存在LLVMContext::MD_nonnull的情况,会添加一个非零假设,为后续的值跟踪判断作前置假设。
- 再进行函数退出判断,如果BB没有后继基本块,则退出函数;
- 首先用BB替换Pred参数、用BB的第一个后继基本块替换BB;然后原BB的其他后继节点追加到Worklist中。最后goto到函数开始处,继续处理新的BB基本块。
注:整个过程是典型的数据流迭代思路,进行更新IncomingVals和IncomingLocs缓存。从前往后遇到store更新其缓存、遇到load用其替换、遇到PHI添加Incoming并更新缓存。此外,通过goto实现深度优先、通过将第2个和之后的后继节点压栈防止其他节点丢失。
2.5 Allocas的清理和PHI的指令简化
在PromoteMem2Reg::run中处理完RenamePass并退出循环后,作了一些简单的清理和PHI指令简化:
- 首先,清理Visited;
- 对于之前提取的可替换的Allocas,如果还有user,那么必然是在不可达基本块中,所以用UndefValue对其replaceAllUsesWith;并且删除所有已提取的Allocas缓存;
- 对AllocaDbgDeclares也进行清理;
- 在一个小的while循环中,对每个生成的PHI指令执行SimplifyInstruction函数,如果成功简化了PHI指令,则用简化的指令替换PHI的user;
2.6 为不可达输入设置UndefValue
PromoteMem2Reg::RenamePass对PHI指令的Incoming基本添加完毕,但也有一些特殊情况。假设有三个基本块entry、A、B,其中entry和A都可达B,而entry和B不能可达A,那么B的PHI指令只添加了entry的Incoming;由于A是不可达基本块,那么B的PHI关于A的Incoming可以设置为UndefValue,但目前还没添加。在PromoteMem2Reg::run的最后一个for循环处理这种情况:
- 迭代NewPhiNodes中的PHI指令,只处理PHI在基本块最前面的情况(后面的PHI指令在本次迭代中一并处理),同时也不处理PHI所在基本块的前驱数量等于PHI的NumIncomingValues(这意味没有不可达输入);
- 将PHI所在基本块的前驱基本块有序排列在Preds,同时剔除addIncoming过的基本块;
- 对当前PHI和后续由前面创建的PHI,依次为Preds中的基本块添加UndefValue的Incoming;