这个patch是我在思考如何判断两个长表达式是否有公共子表达式时发现的。链接在此。该patch的作用主要是实现对一些复杂的例子通过重结合找到其中的公共子表达式。
patch解读
最终融入主branch的代码和上述链接的有出入,以下将会结合代码具体解释。
首先在Reassociate.h中定义了PairMap,这是实现该patch的一个重要的数据结构,此处的定义方式为DenseMap<std::pair<Value *, Value *>, PairMapValue> PairMap[NumBinaryOps]; 其中Map的V对应的类型不只是unsigned,而是自定义的类型,这一点是和上文链接不同的。
PairMapValue的定义如下:
struct PairMapValue { WeakVH Value1; WeakVH Value2; unsigned Score; bool isValid() const { return Value1 && Value2; } };
其中,WeakVH是指向Value的一个Handler,可以由此找到对应的Value。此处的Score也即上文链接中的unsigned类型,用于判断某个表达式更可能为公共子表达式。
此处删除了Pass和InstrOpMap。
第二处改动发生在void ReassociatePass::ReassociateExpression(BinaryOperator *I) { 。此处去除了Pass=0,也即第一次使用普通的重结合算法的过程,直接使用判断条件:if (Ops.size() > 2 && Ops.size() <= GlobalReassociateLimit) {,其中 Ops.size() <= GlobalReassociateLimit相当于限制了表达式链的长度,对于过长的表达式及时止损。
unsigned Max = 1; unsigned BestRank = 0; std::pair<unsigned, unsigned> BestPair; unsigned Idx = I->getOpcode() - Instruction::BinaryOpsBegin; unsigned LimitIdx = 0;
Max记录最大的表达式的索引,BestRank记录最大的rank,BestPair记录最佳一组的两个Value对应的序号,Idx是当前指令类型对应的序号。LimitIdx主要限制遍历的下限,如果打开了CSE LocalOpt,那么该值可能发生变化。
接下来是一个二重循环:
两重循环从最后一个指令开始依次比较当前指令和前面的每条指令,分别标记为i和j,每一个组合的开始Score都为0,如果PairMap中已经包含上述的组合,那么将Score加上上述操作的Score。Rank看起来是值编号算法计算的值的排序,应当是越小越好。如果Score比Max大,则将Max更新为当前的Score,并通过BestPair保存当前的组合,将BestRank更新为MaxRank。(如果Score > Max但MaxRank > BestRank,这样的情况是否存在)
for (unsigned i = Ops.size() - 1; i > LimitIdx; --i) { // We must use int type to go below zero when LimitIdx is 0. for (int j = i - 1; j >= (int)LimitIdx; --j) { unsigned Score = 0; Value *Op0 = Ops[i].Op; Value *Op1 = Ops[j].Op; if (std::less<Value *>()(Op1, Op0)) std::swap(Op0, Op1); auto it = PairMap[Idx].find({Op0, Op1}); if (it != PairMap[Idx].end()) { // Functions like BreakUpSubtract() can erase the Values we're using // as keys and create new Values after we built the PairMap. There's a // small chance that the new nodes can have the same address as // something already in the table. We shouldn't accumulate the stored // score in that case as it refers to the wrong Value. if (it->second.isValid()) Score += it->second.Score; } unsigned MaxRank = std::max(Ops[i].Rank, Ops[j].Rank); // By construction, the operands are sorted in reverse order of their // topological order. // So we tend to form (sub) expressions with values that are close to // each other. // // Now to expose more CSE opportunities we want to expose the pair of // operands that occur the most (as statically computed in // BuildPairMap.) as the first sub-expression. // // If two pairs occur as many times, we pick the one with the // lowest rank, meaning the one with both operands appearing first in // the topological order. if (Score > Max || (Score == Max && MaxRank < BestRank)) { BestPair = {j, i}; Max = Score; BestRank = MaxRank; } } }
上述二重循环完成后,将根据Max的值判断是否有一个组合值得放到当前组合的末尾(可能为公共子表达式的表达式)。
if (Max > 1) { auto Op0 = Ops[BestPair.first]; auto Op1 = Ops[BestPair.second]; Ops.erase(&Ops[BestPair.second]); Ops.erase(&Ops[BestPair.first]); Ops.push_back(Op0); Ops.push_back(Op1); }
此处使用的操作似乎可以简化(尝试一下)
另外的改动发生在PreservedAnalyses ReassociatePass::run(Function &F, FunctionAnalysisManager &) { 也即该Pass运行的入口函数,作者将其封装为void
ReassociatePass::BuildPairMap(ReversePostOrderTraversal<Function *> &RPOT) {。该函数构建了PairMap,首先按照PROT顺序,也即ReversePostOrder(反转后向序)遍历各基本块,对于其中的每条指令,如果是满足结合律或者是二元表达式,则暂不处理。如果该表达式不是树的根节点,也暂不处理。
建立一个WorkList,初始只包含指令的两个操作数,建立一个Ops,然后使用迭代算法不断探索更多可以重结合的可能,循环的结束条件是WorkList为空或Ops达到最大限制(理由同上,避免表达式过长造成程序占用资源过多)。找到当前Value对应的Instruction,如果该Instruction不存在,或是两个指令的操作符不同(也即不能重结合,此处似乎可以判断两个表达式之间不同但可以重结合的可能),或是对应的Instruction有多个Use。直接放入Ops不做任何操作(也即该值相关的操作已经完成),否则将对应指令的操作数放入WorkList。
当该循环结束时,可能有两种情况,一种是由于WorkList为空,也即操作结束了。一种是由于Ops.size() > GlobalReassociateLimit,也即超出了算法所能分析的长度,如果是第二种,则不够减PairMap。
下面执行的操作和上面分析的算法类似,也是一个二重循环,Visited记录了处理过的Value对,在处理前先要记录,并且在记录成功的情况下才会执行处理(通过
if (!Visited.insert({Op0, Op1}).second) continue;
来判断),在PairMap中插入一条记录,对应的Score为1.
如果此次插入失败,同样有处理逻辑。
void ReassociatePass::BuildPairMap(ReversePostOrderTraversal<Function *> &RPOT) { // Make a "pairmap" of how often each operand pair occurs. for (BasicBlock *BI : RPOT) { for (Instruction &I : *BI) { if (!I.isAssociative() || !I.isBinaryOp()) continue; // Ignore nodes that aren't at the root of trees. if (I.hasOneUse() && I.user_back()->getOpcode() == I.getOpcode()) continue; // Collect all operands in a single reassociable expression. // Since Reassociate has already been run once, we can assume things // are already canonical according to Reassociation's regime. SmallVector<Value *, 8> Worklist = { I.getOperand(0), I.getOperand(1) }; SmallVector<Value *, 8> Ops; while (!Worklist.empty() && Ops.size() <= GlobalReassociateLimit) { Value *Op = Worklist.pop_back_val(); Instruction *OpI = dyn_cast<Instruction>(Op); if (!OpI || OpI->getOpcode() != I.getOpcode() || !OpI->hasOneUse()) { Ops.push_back(Op); continue; } // Be paranoid about self-referencing expressions in unreachable code. if (OpI->getOperand(0) != OpI) Worklist.push_back(OpI->getOperand(0)); if (OpI->getOperand(1) != OpI) Worklist.push_back(OpI->getOperand(1)); } // Skip extremely long expressions. if (Ops.size() > GlobalReassociateLimit) continue; // Add all pairwise combinations of operands to the pair map. unsigned BinaryIdx = I.getOpcode() - Instruction::BinaryOpsBegin; SmallSet<std::pair<Value *, Value*>, 32> Visited; for (unsigned i = 0; i < Ops.size() - 1; ++i) { for (unsigned j = i + 1; j < Ops.size(); ++j) { // Canonicalize operand orderings. Value *Op0 = Ops[i]; Value *Op1 = Ops[j]; if (std::less<Value *>()(Op1, Op0)) std::swap(Op0, Op1); if (!Visited.insert({Op0, Op1}).second) continue; auto res = PairMap[BinaryIdx].insert({{Op0, Op1}, {Op0, Op1, 1}}); if (!res.second) { // If either key value has been erased then we've got the same // address by coincidence. That can't happen here because nothing is // erasing values but it can happen by the time we're querying the // map. assert(res.first->second.isValid() && "WeakVH invalidated"); ++res.first->second.Score; } } } } } }