DeadMachineInstructionElim

这个 Pass 从名字上看比较好理解,和中端似乎很相似。代码量不大,一百多行。入口我们就不看了,处理流程为:

bool DeadMachineInstructionElimImpl::eliminateDeadMI(MachineFunction &MF) {  
  bool AnyChanges = false;  
  
  // Loop over all instructions in all blocks, from bottom to top, so that it's  
  // more likely that chains of dependent but ultimately dead instructions will  // be cleaned up.  for (MachineBasicBlock *MBB : post_order(&MF)) {  
    LivePhysRegs.addLiveOuts(*MBB);  
  
    // Now scan the instructions and delete dead ones, tracking physreg  
    // liveness as we go.    for (MachineInstr &MI : make_early_inc_range(reverse(*MBB))) {  
      // If the instruction is dead, delete it!  
      if (MI.isDead(*MRI, &LivePhysRegs)) {  
        LLVM_DEBUG(dbgs() << "DeadMachineInstructionElim: DELETING: " << MI);  
        // It is possible that some DBG_VALUE instructions refer to this  
        // instruction. They will be deleted in the live debug variable        // analysis.        MI.eraseFromParent();  
        AnyChanges = true;  
        ++NumDeletes;  
        continue;  
      }  
      LivePhysRegs.stepBackward(MI);  
    }  
  }  
  LivePhysRegs.clear();  
  return AnyChanges;  
}

这个函数遍历整个函数的每个基本块(按后序),在进入每个块时先把块的 live-out 寄存器集合加载到 LivePhysRegs,然后从块尾到块首反向扫描指令:对每条指令调用 isDead(判断它既无副作用又写出的寄存器不再被使用),如果死指令就删掉并记为“有改动”,否则用 stepBackward 更新活跃寄存器集合;最后清空寄存器活跃信息并返回是否删除过任何指令。

但是我们可能还会有几个问题:

  • 为什么要按“后序遍历”(post-order)遍历基本块?
  • 为什么在每个基本块内部要“从尾到头”反向扫描指令?
  • make_early_inc_range(reverse(*MBB)) 的用途或意义是什么?
  • 为什么中端有死代码消除了这里还要?
为什么要按“后序遍历”(post-order)遍历基本块?

参考要点:后序遍历保证先处理子块(即可能的后继)再处理当前块,这样更容易在处理某个块时,其后续块中的活跃寄存器信息已经在 LivePhysRegs 中得到考虑,从而准确判断指令是否真的死。

为什么在每个基本块内部要“从尾到头”反向扫描指令?

参考要点:因为死指令的判定依赖于“后面是否有用到”的信息。反向扫描可以先看到最近的使用,再决定前面的定义是否是死的;同时也可以及时更新活跃寄存器集,使得依赖链上的整个死链能被清理。

make_early_inc_range(reverse(*MBB)) 的用途或意义是什么?

参考要点:在遍历过程中可能会删除指令,用 make_early_inc_range 可以在迭代时安全地删除当前指令而不会干扰迭代器;配合 reverse 实现反向遍历的安全删除。

为什么中端有死代码消除了这里还要?

因为 IR 级别的 DCE 只能基于 SSA 和目标无关的抽象语义来删除没用的计算,但在指令选择、寄存器分配、指令调度等后端阶段,会引入大量与具体目标指令、物理寄存器和调用约定相关的新指令(比如伪指令、拷贝、spill/fill、延迟插入的指令序列等),这些在 IR 层并不可见或无法预测。有些指令在生成后可能变得无用(比如拷贝到寄存器后没有后续使用,或者某些优化后留下的目标相关指令不再有用),只有在机器指令层面才能准确判断并清理。此外,后端优化(如寄存器合并、指令合并/拆分、模式匹配生成的复杂指令序列)也可能引入或暴露新的死指令。因而,即便中端已经做过 DCE,后端也必须在 MachineInstr 级别再做一次死指令消除,才能确保最终生成的机器码没有冗余、体积更小、执行更高效。