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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值