本页目录

指令选择

把 IR 操作映射到目标机器指令——不是一一对应,而是树覆盖问题:用指令模式去匹配 IR 表达式树,找代价最小的覆盖方案。从贪心的 maximal munch 到动态规划,再到自动机化的 BURS。

概述

中间表示 是机器无关的——add i32 %a, %b。而目标机器有自己的指令集:x86-64 有 addl %eax, %ebx,ARM64 有 add w0, w1, w2,它们不只看操作码,还要考虑寻址模式、寄存器约束、指令组合。⁠指令选择(instruction selection) 就是把 IR 操作映射到目标机器指令序列。它不是一一对应的映射——一条 IR 指令可能对应多条机器指令(复杂操作被分解),也可能多条 IR 指令对应一条机器指令(复合寻址模式或融合指令)。

问题建模:树覆盖(tree tiling)

IR 被组织成表达式树⁠(每棵树代表一个计算,如 a[i] = x + 2 可以展开成一棵树)。目标机器的每条指令也描述为树形模式:

指令模式树去匹配 IR 树的形状(a[i] = x + 2) IR 树(表达式树) 目标机指令模式树 store [+] x load 2 a MOV [base + offset], src [+] src base offset 指令模式树的形状与 IR 树的一段子树吻合(store↔MOV,[+]↔[+])——吻合的子树即可被这条指令整体覆盖(tile)。

指令选择 = 用指令模式树去覆盖(tile)IR 树,使得每片叶子被恰好一块覆盖,且总代价最小。

这就是 tree tiling 问题——每条指令 i 有模式树 p_i、执行代价 c_i,找一个覆盖方案最小化总代价。

三种算法,代价与精度依次上升

1. 宏展开(macro expansion):最简

IR 操作 add → 直接输出 addl %src, %dst。不利用任何目标机特性,产出的代码明显差(如 load + add 分两条指令,而 CISC 有 add [addr], %reg)。但实现简单,用于 JIT 的 baseline 编译或代码大小不敏感的场景。

2. maximal munch:贪心,局部最优

从树根开始,选能覆盖当前子树的最大模式⁠(最多节点),对剩余部分递归:

munch(node):
    for each pattern from largest to smallest:
        if pattern matches at node:
            emit instruction for that pattern
            for each leaf in pattern that is a subtree root:
                munch(that leaf)
            return
  • 贪心不保证全局最优,但实践中接近最优(因为机器指令模式通常不会"大得能吃进另一条指令的全部节点")。
  • 复杂度 O(n),一遍扫描,适合 JIT 编译(如 V8 的 baseline compiler)。

3. 动态规划:全局最优

自底向上,对每个节点计算"覆盖以该节点为根的子树的最小代价":

for each node n in postorder:            ← 自底向上
    for each pattern p that matches at n:
        cost = p.cost + sum(minCost(leaf) for leaf in p.leaves)
        minCost[n] = min(minCost[n], cost)

然后自顶向下回溯,选择每个节点用的模式
  • 保证找到全局最优覆盖(在给定的指令模式集和代价模型下)。
  • 复杂度 O(n × |patterns|),对大多数 IR 树和指令集,|patterns| 在百量级,可接受。
  • BURS(Bottom-Up Rewrite System) 把 dp 做进自动机——预计算"树匹配到状态→替换"的转换表,运行时 O(n)。但 BURS 构造复杂,LLVM 的 SelectionDAG 在多数目标机上使用 dp(非严格 BURS)。

指令选择的额外复杂性

多输出指令

divmod x, y 同一条指令产出一个商和一个余数——两个 IR 值,一条机器指令。树覆盖模型需要扩展为DAG 覆盖⁠(有向无环图,同一节点可被多棵树共享),此时贪心和 dp 都变复杂。

不定长指令集

x86-64 同一操作有多种编码:addl $1, %eax(3 字节) vs addl $1, (%esp)(4 字节)。指令选择不仅考虑"哪条指令",还考虑"用哪个变体"——寻址模式选择成为指令选择的一部分。

合法化(legalization)

有些 IR 指令组合,目标机不直接支持。在指令选择之前或期间,需要做合法化⁠:

IR:  store 64-bit value to address in reg
ARM64: 支持 64-bit store → 合法
16-bit MCU: 不支持 → 分解为两次 32-bit store (legalize)

LLVM 的 SelectionDAG 把合法化和指令选择交替做——LegalizeDAG pass 把不支持的操作分解为等价序列,SelectionDAG 再选指令。

Instruction Scheduling 与 Register Allocation 的干涉

指令选择之后,还有指令调度⁠(重排顺序降低流水线停顿)和寄存器分配⁠(IR 的虚拟寄存器→有限的物理寄存器)。这三者互相干涉:

指令选择: 选指令 → 定义了对寄存器的使用
指令调度: 重排顺序 → 改变了活跃区间
寄存器分配: 可能 spilling → 插入 load/store → 改变了指令序列

后两者在 寄存器分配 里展开。指令选择和寄存器分配之间有个相位排序问题:先分配寄存器再选指令(可能选了用不到该寄存器的指令),还是先选指令再分配(可能选完发现寄存器不够)。LLVM 的做法是:SelectionDAG 阶段隐式假设"虚拟寄存器无限"选指令,后由寄存器分配器处理 spilling。

参考

  • Cooper/Torczon: "Engineering a Compiler", Chapter 11 (Instruction Selection)
  • Cattell (1978): Formalization and Automatic Derivation of Code Generators (tree pattern matching 的早期工作)
  • LLVM: lib/CodeGen/SelectionDAG/ — 工业级 dp-based 指令选择; lib/CodeGen/GlobalISel/ — 下一代 global 指令选择

Keywords: instruction selection, tree pattern matching, tiling, maximal munch, dynamic programming, BURS, macro expansion, legalization, lowering, SelectionDAG, GlobalISel, addressing mode, multi-output instruction, instruction scheduling interference