后端Pass简介——FixupStatepointCallerSaved
FixupStatepointCallerSaved
该 Pass 有 600 多行
在遇到带有 GC statepoint 的调用指令时,把所有在“caller-saved”寄存器里、运行时需要访问的值强制溢出到栈上,并在调用后重新加载,保证垃圾回收或异常恢复时能正确找到这些值。( “在那些标记为‘GC/去优化安全点’的调用前后,把所有运行时还需要的寄存器值先存到栈上、然后再读回来,确保垃圾回收和去优化时能找到这些值。”)
看一下这句话里面含有的概念:
LLVM 在生成带有 deopt(deoptimization)或 GC statepoint 的调用序列时,必须记录在调用返回瞬间仍然活跃、且对运行时有意义的寄存器值。由于调用可能会破坏 caller-saved 寄存器,这个 Pass “fix up”——自动为这些寄存器分配 spill slot、在调用前 spill、并重写 statepoint 元操作数为对栈上 slot 的引用,最后在合适位置 reload。
- Deopt(去优化)是什么? Deopt(deoptimization)是动态语言或高级优化中常用的机制:当编译器对某段代码做了激进优化,但运行时某个假设不再成立时,就要“去优化”——把正在执行的优化后机器代码切换回更通用(例如解释器或较低级别代码)的版本,并准确重建原始程序状态,以保证正确性和调试能力。
- GC Statepoint(垃圾回收安全点)是什么? GC Statepoint 是编译器在生成机器代码时插入的一种特殊“伪指令”或调用标记,专门用来告诉运行时==“在这里是垃圾回收或去优化的安全点”==,并携带哪些寄存器或栈位置中存放着 GC 根(或后续需要重建的值)的信息,供垃圾回收器或去优化逻辑准确地扫描和恢复。
注意,只是告诉一个潜在的安全点,真正 GC 只有在堆内存不足时才启动!
该 Pass 是可以调参的:
STATISTIC(NumSpilledRegisters, "Number of spilled register");
STATISTIC(NumSpillSlotsAllocated, "Number of spill slots allocated");
STATISTIC(NumSpillSlotsExtended, "Number of spill slots extended");
static cl::opt<bool> FixupSCSExtendSlotSize(
"fixup-scs-extend-slot-size", cl::Hidden, cl::init(false),
cl::desc("Allow spill in spill slot of greater size than register size"),
cl::Hidden);
static cl::opt<bool> PassGCPtrInCSR(
"fixup-allow-gcptr-in-csr", cl::Hidden, cl::init(false),
cl::desc("Allow passing GC Pointer arguments in callee saved registers"));
static cl::opt<bool> EnableCopyProp(
"fixup-scs-enable-copy-propagation", cl::Hidden, cl::init(true),
cl::desc("Enable simple copy propagation during register reloading"));
// This is purely debugging option.
// It may be handy for investigating statepoint spilling issues.
static cl::opt<unsigned> MaxStatepointsWithRegs(
"fixup-max-csr-statepoints", cl::Hidden,
cl::desc("Max number of statepoints allowed to pass GC Ptrs in registers"));
- -fixup-scs-extend-slot-size:允许当需要 spill 的寄存器比已有栈槽大时,扩展该栈槽以复用而不是新建一个更小的槽。
- -fixup-allow-gcptr-in-csr:允许将 GC 指针元操作数保留在 callee-saved 寄存器里,而不是强制 spill 到栈上。
- -fixup-scs-enable-copy-propagation:在插入 spill 之后进行简单的拷贝传播,以避免对原本可以直接 spill 的寄存器执行多余的 copy/spill 操作。
- -fixup-max-csr-statepoints:只对最前面 N 个 statepoint 允许 GC 指针保留在寄存器,超过该数量后就不再放宽 spill 限制。
假设在某个函数的 MIR 中,出现了这样一条伪指令(STATEPOINT)——它携带了一个保存在 caller‐saved 寄存器 %r1 里的 GC 指针 ptr,并标记这是一个 GC/Deopt 安全点:
… ; 前面已有指令
STATEPOINT gc_state, …, %r1, … ; %r1 中存了需要在 GC 时扫描的根
… ; 后面还有指令
如果不做处理,函数调用时就可能覆写 %r1,GC 或去优化时就找不到这个根。Finalize 后,MIR 大致被改成:
…
; 1) spill 前把 %r1 存到栈 slot #FI42
STORE_REG_STACK %r1, FI42
; 2) 重写 STATEPOINT,用栈 slot 间接操作代替 %r1
STATEPOINT gc_state, …, INDIRECT_MEMREF, size=8, frame_index=FI42, …
; 3) reload %r1
LOAD_STACK_REG FI42 → %r1
…
- spill:调用前把寄存器里的值写到栈上。
- rewrite:原来伪指令里对 %r1 的引用,改成对栈 slot 的间接内存引用。
- reload:调用后再把值从栈读取回 %r1,保证后续正常使用。
处理逻辑:
- 收集 Statepoint
- 遍历 MachineFunction,把所有 opcode == STATEPOINT 的指令放到一个列表里。
- 对每个 Statepoint
- 构造 StatepointState,读取调用约定的 caller‐saved 掩码;
- findRegistersToSpill():扫描该指令的元操作数,找出所有既是 caller‐saved 又在元操作数里出现的物理寄存器。
- 分配 Spill Slot
- 用 FrameIndexesCache 为每个要 spill 的寄存器分配(或重用)栈帧对象,返回一个 frame index。
- 插入 Spill
- 在 STATEPOINT 之前,调用 TII.storeRegToStackSlot(…),将寄存器值写到对应栈 slot;
- 中间还做一次简单的 copy‐propagation,尽可能用源寄存器直接 spill。
- 重写 Statepoint
- 构造一个新的 MachineInstr,复制原 STATEPOINT 的所有操作数;
- 对那些被 spill 的寄存器操作数,替换成 IndirectMemRefOp, size, frame_index 形式;
- 插入新指令,删除旧伪指令。
- 插入 Reload
- 在新 STATEPOINT 之后(以及可能的 landing-pad 起始处),用 TII.loadRegFromStackSlot(…) 把值从栈 slot 恢复回寄存器。
- 统计 & 返回
- 更新统计变量(spilled slots 数量等),并返回 “这个函数的 MIR 被改变了”,以便后续 Pass 重跑需要的分析(比如 CFG、FrameInfo)。
评论