13.1 指令选择
指令选择(instruction selection)把目标无关 IR 转换为某个指令集架构(Instruction Set Architecture,ISA)支持的操作。输入可能只有 add、load、compare 与 branch;输出则必须选择合法的 x86-64、AArch64 或 RISC-V 指令,并满足操作数类别、立即数范围和寻址模式约束。
这不是一对一翻译。一项 IR 操作可能需要多条机器指令;反过来,一条机器指令也可能实现多个相连 IR 节点。例如 AArch64 的 madd 可以计算 a * b + c,x86 的 lea 可以把 base、scaled index 和 displacement 合进一条指令,无需单独乘法与两次加法。
用模式覆盖 IR 树
选择器会把机器指令描述成匹配 IR 的模式。简单整数加法规则可以写成:
IR: add(reg, reg)
target: ADD dst, lhs, rhs
cost: latency 1, size 4 bytes更大模式可以匹配 add(mul(a,b), c),并在目标和数值语义允许时发射 fused multiply-add。选择过程于是成为覆盖问题:用不非法重叠的瓦片覆盖所有必需 IR 节点,同时最小化成本。
对树结构,动态规划可以自底向上求最便宜覆盖:为每个节点与期望结果类别记录最佳规则及其孩子需求。成本模型可以综合指令数、编码体积、延迟、吞吐和寄存器压力。size 优先构建与 speed 优先构建可能选择不同覆盖。
DAG、共享值与求值顺序
真实 IR 往往是有向无环图(DAG),而不只是树:一个值可以有多个 users。为了树匹配而盲目复制共享子树,可能重复计算或重复 trap。选择器必须保留共享、副作用和 machine IR 中已有的顺序约束。
指令选择还要尊重寄存器类别。整数结果需要 general-purpose register,浮点值使用 FP/vector class,某些指令甚至要求固定寄存器。只有后续分配器能够满足约束,或选择器主动插入必要 class copy 时,规则才合法。
寻址模式也是指令瓦片
内存操作数常是 ISA 之间差异最大的部分:
address = base + index * scale + displacement
value = load(address)x86-64 可在 load 中直接编码许多组合,scale 限于 1、2、4、8。AArch64 有多种 base-plus-immediate 和 shifted-register 形式,但立即数范围取决于访问宽度与指令形态。基础 RISC-V load 使用 base 加有符号 12 位位移,因此 indexed address 通常要先做算术。
选择前后的合法化
IR 可能包含目标无法直接执行的类型或操作,例如 64 位目标上的 128 位加法、不支持的向量宽度,或无法放进单条指令的立即数。合法化(legalization)会把它们拆成支持的片段或 runtime helper call。有些编译器先合法化通用 IR,另一些则让选择规则展开非法操作。
选择后也可能保留目标专用 pseudo-instruction。“加载任意 64 位常量”这样的 pseudo,稍后会依据具体值展开为一条或多条真实指令。暂时保留 pseudo,能让寄存器分配和调度先看到更清晰的语义操作。
可靠测试应记录输入 IR、选中机器指令、target features 和 cost,覆盖立即数边界、所有寄存器类、共享 DAG 节点、trap 操作与被关闭的 CPU feature。正确性优先:成本再低的非法覆盖也不是候选。