后端Pass简介——DeadMachineInstructionElim.cpp
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 级别再做一次死指令消除,才能确保最终生成的机器码没有冗余、体积更小、执行更高效。