SimplifyCFGPass 是 LLVM编译器框架中的一个 控制流图优化(Control Flow Graph, CFG) 相关的 优化 Pass。它的主要作用是 简化控制流图(CFG),减少不必要的基本块(Basic Blocks)和跳转,从而提高指令级并行性、减少分支预测失败、提高编译器生成的代码质量。
其入口为 SimplifyCFGPass. cpp,其主要逻辑位于 SimplifyCFG. cpp。
根据 SimplifyCFG. cpp 的说明,可以知道这个文件主要用途是:

  • 移除没有前驱的基本块。
  • 如果基本块只有一个前驱,且前驱只有一个后继,则将该基本块合并到前驱中。
  • 对于只有一个前驱的基本块,消除 PHI 节点。
  • 消除仅包含无条件跳转的基本块。
  • 将对 nounwind 函数的 invoke 指令转换为 call 指令。
  • 将类似 “if (x) if (y)” 的结构转换为 “if (x&y)”。

代表性函数

MergeBlockIntoPredecessor

这个函数定义于 BasicBlockUtils. cpp,但是在该文件起辅助作用,因为满足时可以使用该函数合并基本块。我们可以看到次级入口函数 simplifyCFG 中有这个函数。当前的允许合并到前驱块的条件是:

  • 前驱只有一个只有一个前驱的基本块才会考虑合并,避免了像循环中的基本块(有多个前驱)被误合并的情况。
  • 前驱只有一个后继基本块的前驱只有一个后继时,才进行合并。这样做是为了避免循环或者分支结构的破坏,确保控制流图的结构不被错误修改。
  • 没有 PHI 节点:==如果基本块中存在 PHI 节点,则不允许合并。==这是因为 PHI 节点涉及到不同控制流路径的合并,合并后可能会导致不明确的值或冲突。
  • 删除无用的基本块:如果基本块没有前驱(或者只有自己作为前驱),就会被删除,避免冗余的控制流。

显然这个要求是比较粗略的,但是也可能为了防止逻辑过于复杂(更激进的策略)带来的复杂度开销

bool SimplifyCFGOpt::simplifyOnce(BasicBlock *BB) {
  bool Changed = false;

  assert(BB && BB->getParent() && "Block not embedded in function!");
  assert(BB->getTerminator() && "Degenerate basic block encountered!");

  // Remove basic blocks that have no predecessors (except the entry block)...
  // or that just have themself as a predecessor.  These are unreachable.
  if ((pred_empty(BB) && BB != &BB->getParent()->getEntryBlock()) ||
      BB->getSinglePredecessor() == BB) {
    LLVM_DEBUG(dbgs() << "Removing BB: \n" << *BB);
    DeleteDeadBlock(BB, DTU);
    return true;
  }

  // Check to see if we can constant propagate this terminator instruction
  // away...
  Changed |= ConstantFoldTerminator(BB, /*DeleteDeadConditions=*/true,
                                    /*TLI=*/nullptr, DTU);

  // Check for and eliminate duplicate PHI nodes in this block.
  Changed |= EliminateDuplicatePHINodes(BB);

  // Check for and remove branches that will always cause undefined behavior.
  if (removeUndefIntroducingPredecessor(BB, DTU, Options.AC))
    return requestResimplify();

  // Merge basic blocks into their predecessor if there is only one distinct
  // pred, and if there is only one distinct successor of the predecessor, and
  // if there are no PHI nodes.
  if (MergeBlockIntoPredecessor(BB, DTU))
    return true;

  if (SinkCommon && Options.SinkCommonInsts)
    if (sinkCommonCodeFromPredecessors(BB, DTU) ||
        mergeCompatibleInvokes(BB, DTU)) {
      // sinkCommonCodeFromPredecessors() does not automatically CSE PHI's,
      // so we may now how duplicate PHI's.
      // Let's rerun EliminateDuplicatePHINodes() first,
      // before foldTwoEntryPHINode() potentially converts them into select's,
      // after which we'd need a whole EarlyCSE pass run to cleanup them.
      return true;
    }

  IRBuilder<> Builder(BB);

  if (Options.SpeculateBlocks &&
      !BB->getParent()->hasFnAttribute(Attribute::OptForFuzzing)) {
    // If there is a trivial two-entry PHI node in this basic block, and we can
    // eliminate it, do so now.
    if (auto *PN = dyn_cast<PHINode>(BB->begin()))
      if (PN->getNumIncomingValues() == 2)
        if (foldTwoEntryPHINode(PN, TTI, DTU, Options.AC, DL,
                                Options.SpeculateUnpredictables))
          return true;
  }

  Instruction *Terminator = BB->getTerminator();
  Builder.SetInsertPoint(Terminator);
  switch (Terminator->getOpcode()) {
  case Instruction::Br:
    Changed |= simplifyBranch(cast<BranchInst>(Terminator), Builder);
    break;
  case Instruction::Resume:
    Changed |= simplifyResume(cast<ResumeInst>(Terminator), Builder);
    break;
  case Instruction::CleanupRet:
    Changed |= simplifyCleanupReturn(cast<CleanupReturnInst>(Terminator));
    break;
  case Instruction::Switch:
    Changed |= simplifySwitch(cast<SwitchInst>(Terminator), Builder);
    break;
  case Instruction::Unreachable:
    Changed |= simplifyUnreachable(cast<UnreachableInst>(Terminator));
    break;
  case Instruction::IndirectBr:
    Changed |= simplifyIndirectBr(cast<IndirectBrInst>(Terminator));
    break;
  }

  return Changed;
}

另一处 MergeBlockIntoPredecessor 位于函数 foldCondBranchOnValueKnownInPredecessorImpl 之中,其主要是在处理 条件分支时 通过常量已知值简化控制流,然后可以合并基本块。

hoistCommonCodeFromSuccessorssinkCommonCodeFromPredecessors

两个函数顾名思义是从后继块和前驱块中提取公共指令,并将它们提升到更合适的位置,以减少冗余(可能的重复部分)。

常用超参数

  • PHINodeFoldingThresholdPHI 节点折叠启用门槛。它的默认值是 2。
  • tTwoEntryPHINodeFoldingThreshold: 折叠具有两个入口的 PHI 节点(即只有两个输入值的 PHI 节点)时 可以接受的最大总指令成本,默认值是 4。
    上述两个都是启发式调参因子,所以没有直接意义。