SlotIndex机制--以AMDGPU为例
适用范围 :LLVM AMDGPU(GCN/SI+)后端的 Machine IR、LiveIntervals、寄存器分配前优化与调度分析
通用实现 :
llvm/include/llvm/CodeGen/SlotIndexes.h、llvm/lib/CodeGen/SlotIndexes.cpp、llvm/include/llvm/CodeGen/LiveInterval.h
1. SlotIndex 解决什么问题
LLVM 后端进入 Machine IR(MIR) 后,很多分析不能只用"第几条 MachineInstr"描述程序位置。寄存器活跃性需要回答更细的问题:
text
某个寄存器值在一条指令之前、指令内部 early-clobber 时刻、普通 use/def 时刻、指令之后是否仍然 live?
SlotIndex 就是 MIR 层的程序位置坐标。它给每条非 debug/pseudo 的 MachineInstr 分配一个可比较的 index,并在同一条指令内部继续划分多个 slot。LiveInterval / LiveRange 使用这些位置作为区间端点,从而表达寄存器值的存活范围。
对 AMDGPU 来说,SlotIndex 尤其重要:
- VGPR、SGPR、AGPR live range 长度直接影响寄存器压力、spill 和 occupancy。
EXEC、VCC、SCC 等特殊寄存器经常参与控制流和 predication 优化,必须精确判断 reaching def。- WQM/WWM、waterfall loop、divergent 控制流会让活跃性变得比普通 CPU 后端更敏感。
- MFMA(CDNA GPU 中的矩阵融合乘加)等指令可能带 early-clobber 约束,同一条 MI 内部的 slot 先后关系会影响寄存器冲突判断。
text
SlotIndex 是 LLVM 后端寄存器活跃性分析的时间轴;AMDGPU 的 pre-RA 优化、寄存器压力分析和调度逻辑大量依赖它。
2. SlotIndex 内部结构
SlotIndex 是对 IndexListEntry 的轻量包装。IndexListEntry 保存:
- 对应的
MachineInstr *。 - 该指令的基础整数 index。
SlotIndex 自身还携带 2 bit 的 slot 编号。通用定义位于 llvm/include/llvm/CodeGen/SlotIndexes.h:
cpp
/// SlotIndex - An opaque wrapper around machine indexes.
class SlotIndex {
friend class SlotIndexes;
enum Slot {
/// Basic block boundary. Used for live ranges entering and leaving a
/// block without being live in the layout neighbor. Also used as the
/// def slot of PHI-defs.
Slot_Block,
/// Early-clobber register use/def slot.
Slot_EarlyClobber,
/// Normal register use/def slot.
Slot_Register,
/// Dead def kill point.
Slot_Dead,
Slot_Count
};
}
四种 slot 的语义如下:
| Slot | 打印后缀 | 典型含义 |
|---|---|---|
Slot_Block |
B |
基本块边界;也用于 PHI-def |
Slot_EarlyClobber |
e |
early-clobber use/def |
Slot_Register |
r |
普通寄存器 use/def |
Slot_Dead |
d |
dead def 的 kill 点 |
SlotIndex::print 会把 slot 打印为 "Berd" 中的一个字符。因此如果某条 MI 的 base index 是 32,同一条指令上的几个位置可以表示为:
text
32B 指令边界 / block slot
32e early-clobber slot
32r 普通 register slot
32d dead slot
这也是为什么 LiveInterval dump 中经常能看到类似 [32r, 80r)、96B、144d 的位置。
3. SlotIndexes 如何给 MachineInstr 编号
SlotIndexes 是 MachineFunction 级 analysis/pass。它遍历 MachineFunction 中的所有 MBB 和 MI,并维护几类映射:
| 数据结构 | 作用 |
|---|---|
mi2iMap |
MachineInstr * 到 base SlotIndex 的映射 |
MBBRanges |
每个 MBB 的 [start, end) index 范围 |
idx2MBBMap |
从 index 反查所在 MBB |
indexList |
所有 index entry 的有序链表 |
初始化时,每条有效 MI 之间默认留出 SlotIndex::InstrDist 的间距:
cpp
enum {
InstrDist = 4 * Slot_Count
};
Slot_Count 是 4,所以默认间距是 16。这样后续插入新指令时,很多情况下可以在相邻 index 的空隙里局部插入,避免频繁全函数重编号。
遍历编号时会跳过 debug/pseudo 指令:
cpp
for (MachineInstr &MI : MBB) {
if (MI.isDebugOrPseudoInstr())
continue;
indexList.push_back(*createEntry(&MI, index += SlotIndex::InstrDist));
mi2iMap.insert(std::make_pair(
&MI, SlotIndex(&indexList.back(), SlotIndex::Slot_Block)));
}
常用接口包括:
| 接口 | 作用 |
|---|---|
getInstructionIndex(MI) |
获得某条 MI 的 base index |
getRegSlot() |
转到普通 register slot |
getRegSlot(true) |
转到 early-clobber slot |
getBaseIndex() |
转到 block/base slot |
getDeadSlot() |
转到 dead slot |
getMBBStartIdx(MBB) |
获得 MBB 起始位置 |
getMBBEndIdx(MBB) |
获得 MBB 结束位置 |
4. 与LiveInterval 有什么关系
SlotIndex 本身不保存寄存器活跃信息,它只是程序位置。真正的活跃性由 LiveInterval / LiveRange 表示:
text
LiveInterval = 若干个基于 SlotIndex 的半开区间 [start, end)
例如:
text
%42:vgpr_32 = [32r, 80r), [112B, 160d)
含义是:
%42在32r开始 live。- 到
80r前结束第一段 live range。 - 在
112B重新变为 live,持续到160d。
常见查询模式:
cpp
SlotIndex SI = LIS->getInstructionIndex(MI).getRegSlot();
LiveInterval &LI = LIS->getInterval(Reg);
if (LI.liveAt(SI)) {
// Reg is live at this program point.
}
对于 AMDGPU,LiveInterval 还经常带 subrange。VGPR/AGPR tuple、subreg 或 lane mask 相关优化会用 SlotIndex 加 LaneBitmask 判断某个 lane 在某个程序点是否 live。
5. AMDGPU示例1:判断两条指令之间是否有重新定义def
SIOptimizeExecMaskingPreRA.cpp 中有一个典型 pre-RA 优化,会把如下序列:
text
%sel = V_CNDMASK_B32_e64 0, 1, %cc
%cmp = V_CMP_NE_U32 1, %sel
$vcc = S_AND_B64 $exec, %cmp
S_CBRANCH_VCC[N]Z
折叠成:
text
$vcc = S_ANDN2_B64 $exec, %cc
S_CBRANCH_VCC[N]Z
这个变换必须保证 %cc 在 V_CNDMASK 到 S_AND_B64 之间没有被重新定义。否则,折叠后的 S_ANDN2_B64 会使用错误的值。
AMDGPU 后端用 SlotIndex 精确查询这两个程序点上的 live value:
cpp
static bool isDefBetween(const LiveRange &LR, SlotIndex AndIdx,
SlotIndex SelIdx) {
LiveQueryResult AndLRQ = LR.Query(AndIdx);
return (!AndLRQ.isKill() && AndLRQ.valueIn() != LR.Query(SelIdx).valueOut());
}
SlotIndex AndIdx = LIS->getInstructionIndex(And).getRegSlot();
SlotIndex SelIdx = LIS->getInstructionIndex(Sel).getRegSlot();
这里的关键点:
getInstructionIndex(And)取得S_AND_B64的 base index。.getRegSlot()转到普通寄存器 use/def 时刻。LR.Query(AndIdx)查询And位置看到的 live value。LR.Query(SelIdx)查询Sel位置输出的 live value。- 两者不是同一个 value 时,说明中间存在影响语义的 def,不能折叠。
这类代码体现了 SlotIndex 的核心价值:优化不是简单比较指令前后顺序,而是在 LiveRange 时间轴上比较具体程序点的值流动。
6. AMDGPU示例2:GCN 寄存器压力分析
GCNRegPressure.cpp 会在寄存器压力分析中查找某个虚拟寄存器在两个程序点之间是否有 use:
cpp
SlotIndex InstSlot = LIS->getInstructionIndex(*MI).getRegSlot();
bool InRange = Upward ? (InstSlot > PriorUseIdx && InstSlot <= NextUseIdx)
: (InstSlot >= PriorUseIdx && InstSlot < NextUseIdx);
这里 SlotIndex 让"两个 use 之间"变成一个可比较的半开范围。对于 AMDGPU,这不仅影响普通 live range,还影响 lane mask:
cpp
if ((S.LaneMask & LaneMaskFilter).any() && S.liveAt(SI))
LiveMask |= S.LaneMask;
也就是说,在某个 SlotIndex 上,一个 VGPR 的部分 lane 可能 live,另一部分 lane 可能不 live。GCN 调度、寄存器压力估算、rematerialization 等逻辑都需要这种精度。
7. AMDGPU示例3:early-clobber def 的 slot 修正
AMDGPU部分指令,特别是某些 MFMA 相关指令,可能有 early-clobber def。普通 def 位于 register slot,early-clobber def 位于 early-clobber slot。
如果后端把一条 MI 转成 early-clobber 形式,LiveInterval 中对应 def 的位置也要从 r slot 改到 e slot。SIInstrInfo.cpp 中有类似处理:
cpp
SlotIndex OldIndex = LIS->getInstructionIndex(*MIB).getRegSlot(false);
SlotIndex NewIndex = LIS->getInstructionIndex(*MIB).getRegSlot(true);
auto &LI = LIS->getInterval(Def.getReg());
auto *S = LR.find(OldIndex);
if (S != LR.end() && S->start == OldIndex) {
S->start = NewIndex;
S->valno->def = NewIndex;
}
这个例子说明:同一条 MachineInstr 内的 e 和 r 不是装饰信息,而是会影响寄存器干涉关系的真实程序点。如果 early-clobber def 仍被记录在普通 register slot,寄存器分配可能错误地认为某些 use/def 不冲突。
8. 插入、删除和替换 MachineInstr 时的注意点
AMDGPU 后端的很多 pass 会在 LiveIntervals 已经可用时改写 MI,例如:
- 替换一条指令为另一条指令。
- 删除 compare/select/and 等中间指令。
- 插入 WQM/WWM、waterfall 或 spill/reload 相关指令。
- 调整 early-clobber、implicit def/use 或 tied operand。
如果当前 pass 依赖或保留 LiveIntervals,修改 MI 后必须同步维护 SlotIndexes 和 LiveIntervals。常见手段包括:
| 操作 | 常用接口 |
|---|---|
| 替换 MI | LiveIntervals::ReplaceMachineInstrInMaps |
| 删除 MI | removeInterval、removeAllRegUnitsForPhysReg、从 maps 中移除 |
| 新增/移动 MI | 修复 SlotIndexes,并重新计算相关 vreg interval |
| 不确定如何局部维护 | 对受影响寄存器调用 createAndComputeVirtRegInterval |
经验规则:
text
只要 pass 在 LiveIntervals 可用之后移动、删除、替换 MachineInstr,就必须重新检查 SlotIndex 和 LiveInterval 是否仍一致。
如果没有维护好,常见后果包括:
Instruction not found in maps断言。- LiveRange segment 起止点指向旧 MI。
VNInfo::def与 segment start 不一致。- 寄存器分配错误判断干涉关系。
- AMDGPU pre-RA 优化误删或错误折叠
EXEC/VCC相关序列。
9. 调试建议
调试 AMDGPU SlotIndex / LiveInterval 问题时,通常先看 MIR 和 LiveInterval dump:
bash
llc -mtriple=amdgcn-amd-amdhsa -mcpu=<gfx> input.ll -stop-after=<pass> -o out.mir
llc -mtriple=amdgcn-amd-amdhsa -mcpu=<gfx> input.ll -print-after-all 2> trace.log
源码中也可以临时打印:
cpp
SlotIndex Idx = LIS->getInstructionIndex(MI);
dbgs() << "MI index: " << Idx
<< " reg slot: " << Idx.getRegSlot()
<< " dead slot: " << Idx.getDeadSlot() << '\n';
LIS->getInterval(Reg).dump();
排查时重点确认:
- 当前 MI 是否已经存在于
SlotIndexes::mi2iMap。 - 查询的是 base slot、register slot,还是 early-clobber slot。
- LiveRange 的 segment start/end 是否和
VNInfo::def一致。 - 替换或删除 MI 后,相关 vreg/phys reg interval 是否同步更新。
- 对 physical register,是否还需要检查 regunit live range。
10. 文章总结
可以把 SlotIndex 理解为 MIR 层的细粒度时间轴:
text
MachineInstr 顺序:
MI0 MI1 MI2
SlotIndex:
16B 16e 16r 16d
32B 32e 32r 32d
48B 48e 48r 48d
在 AMDGPU 后端中,它主要用于:
- 表达 VGPR/SGPR/AGPR 的 live range 端点。
- 查询
EXEC、VCC、SCC 和普通 vreg 的 reaching def。 - 做 pre-RA peephole / exec masking 优化的合法性检查。
- 估算 GCN register pressure 和 lane-level liveness。
- 正确处理 early-clobber 指令内部的 use/def 顺序。
- 在 MI 替换、删除、插入后维护
LiveIntervals的一致性。
SlotIndex 不直接决定寄存器分配结果,但它是 LiveIntervals 的坐标系。坐标系一旦错,后续的 AMDGPU 活跃性、干涉判断、寄存器压力和优化合法性都会跟着出错。