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,保证后续正常使用。

处理逻辑:

  1. 收集 Statepoint
    • 遍历 MachineFunction,把所有 opcode == STATEPOINT 的指令放到一个列表里。
  2. 对每个 Statepoint
    • 构造 StatepointState,读取调用约定的 caller‐saved 掩码;
    • findRegistersToSpill():扫描该指令的元操作数,找出所有既是 caller‐saved 又在元操作数里出现的物理寄存器。
  3. 分配 Spill Slot
    • 用 FrameIndexesCache 为每个要 spill 的寄存器分配(或重用)栈帧对象,返回一个 frame index。
  4. 插入 Spill
    • 在 STATEPOINT 之前,调用 TII.storeRegToStackSlot(…),将寄存器值写到对应栈 slot;
    • 中间还做一次简单的 copy‐propagation,尽可能用源寄存器直接 spill。
  5. 重写 Statepoint
    • 构造一个新的 MachineInstr,复制原 STATEPOINT 的所有操作数;
    • 对那些被 spill 的寄存器操作数,替换成 IndirectMemRefOp, size, frame_index 形式;
    • 插入新指令,删除旧伪指令。
  6. 插入 Reload
    • 在新 STATEPOINT 之后(以及可能的 landing-pad 起始处),用 TII.loadRegFromStackSlot(…) 把值从栈 slot 恢复回寄存器。
  7. 统计 & 返回
    • 更新统计变量(spilled slots 数量等),并返回 “这个函数的 MIR 被改变了”,以便后续 Pass 重跑需要的分析(比如 CFG、FrameInfo)。