概述
BURG 是编译器后端指令选择的经典工具。
它的核心思想是:给定一棵 IR 表达式树和一组带代价的机器指令模式,用动态规划找到覆盖整棵树的最小代价方案。
BURG 要解决什么问题?
问题场景
假设我们有以下 IR 表达式树(表示 (a + 5) * b):

其中:
- a 是内存中的变量
- b 是内存中的变量
- 5 是常量
机器提供以下指令(每条指令 = 一个模式 + 代价):
| 规则 | 模式 | 指令 | 代价 |
|---|---|---|---|
| R1 | add(reg, reg) | ADD rd, rs1, rs2 | 1 |
| R2 | add(reg, imm) | ADDI rd, rs1, imm | 1 |
| R3 | mul(reg, reg) | MUL rd, rs1, rs2 | 3 |
| R4 | load(addr) | LOAD rd, addr | 2 |
| R5 | const(imm) | LOADI rd, imm | 1 |
目标:用这些指令覆盖整棵树,总代价最小。
BURG 的两步算法
Label 阶段(自底向上)
从叶子到根,给每个节点标注"用哪条规则、代价是多少"。
节点 a(叶子,内存变量):
text
规则 R4:load(addr)
代价 = 2
标注:a 的最优代价 = 2(用 LOAD)
节点 5(叶子,常量):
text
规则 R5:const(imm)
代价 = 1
标注:5 的最优代价 = 1(用 LOADI)
节点 +(内部节点,子节点是 a 和 5):
text
候选规则 1:R1(add(reg, reg))
需要 a 在寄存器中,5 在寄存器中
代价 = cost(a) + cost(5) + 1 = 2 + 1 + 1 = 4
候选规则 2:R2(add(reg, imm))
需要 a 在寄存器中,5 作为立即数
代价 = cost(a) + 1 = 2 + 1 = 3
取最小:代价 = 3(用 ADDI)
标注:+ 的最优代价 = 3
节点 b(叶子,内存变量):
text
规则 R4:load(addr)
代价 = 2
标注:b 的最优代价 = 2
节点 * (根节点,子节点是 + 和 b):
text
候选规则:R3(mul(reg, reg))
需要 + 的结果在寄存器中,b 在寄存器中
代价 = cost(+) + cost(b) + 3 = 3 + 2 + 3 = 8
标注:* 的最优代价 = 8
Label 阶段结果汇总:
| 节点 | 最优规则 | 代价 |
|---|---|---|
| a | LOAD | 2 |
| 5 | LOADI | 1 |
| + | ADDI | 3 |
| b | LOAD | 2 |
| * | MUL | 8 |
Select 阶段(自顶向下)
从根到叶,每步选择到达目标最便宜的路径。
根节点*:
text
选择规则:R3(MUL)
需要:+ 的结果在寄存器中,b 在寄存器中
节点 +(被 * 需要,必须在寄存器中):
text
选择规则:R2(ADDI)
需要:a 在寄存器中,5 作为立即数
节点 a(被 + 需要,必须在寄存器中):
text
选择规则:R4(LOAD)
生成:LOAD r1, a
节点 5(被 + 作为立即数使用):
text
不单独生成指令,直接作为 ADDI 的立即数
节点 b(被 * 需要,必须在寄存器中):
text
选择规则:R4(LOAD)
生成:LOAD r2, b
节点 +(生成指令):
text
生成:ADDI r1, r1, 5
节点 * (生成指令):
text
生成:MUL r3, r1, r2
最终生成的汇编
assembly
LOAD r1, a ; 代价 2
ADDI r1, r1, 5 ; 代价 1
LOAD r2, b ; 代价 2
MUL r3, r1, r2 ; 代价 3
; 总代价 = 8
验证:如果不用 ADDI 而用 ADD,需要额外加载 5 到寄存器:
assembly
LOAD r1, a ; 2
LOADI r4, 5 ; 1
ADD r1, r1, r4 ; 1
LOAD r2, b ; 2
MUL r3, r1, r2 ; 3
; 总代价 = 9
BURG 选择了代价 8 的方案,比 9 更优。
BURG 与 Maximal Munch 的区别
| 算法 | 策略 | 最优性 |
|---|---|---|
| Maximal Munch | 从根到叶,每次选覆盖最多节点的瓦片 | 贪心,不一定最优 |
| BURG | 从叶到根计算代价,再从根到叶选择 | 动态规划,保证最优 |
Maximal Munch 可能错过的例子
假设树是 (a + 5) * b,但规则是:
| 规则 | 模式 | 代价 |
|---|---|---|
| R1 | mul(add(reg, imm), reg) |
4(一条指令完成加法和乘法) |
| R2 | add(reg, imm) |
1 |
| R3 | mul(reg, reg) |
3 |
Maximal Munch :从根 * 开始,最大瓦片是 R1(覆盖 * + + + 5),代价 4。
然后处理 a(LOAD,代价 2)和 b(LOAD,代价 2)。
总代价 = 4 + 2 + 2 = 8。
BURG:会计算所有可能组合,发现 R1 的总代价 8,而 R2 + R3 的总代价是 1 + 3 + 2 + 2 = 8。两者相同。
但如果 R1 的代价是 5,Maximal Munch 会选 R1(代价 5+2+2=9),而 BURG 会选 R2+R3(代价 8),更优。
结论:Maximal Munch 贪心,可能不是最优;BURG 用动态规划,保证最优。
BURG 的输入与输出
输入:模式描述
BURG 的输入是一个 .burg 文件,描述每条指令的模式和代价:
burg
%term ADD=1 MUL=2 ADDI=3 LOAD=4 LOADI=5
%start reg
reg: ADD(reg, reg) 1
reg: ADDI(reg, imm) 1
reg: MUL(reg, reg) 3
reg: LOAD(addr) 2
reg: LOADI(imm) 1
%term:定义终结符(操作符)%start reg:目标非终结符reg: ADD(reg, reg) 1:规则"reg → ADD(reg, reg)",代价 1
输出:匹配器
BURG 生成一个 C/C++ 函数,输入是 IR 树,输出是最优覆盖方案:
c
int burg_label(Node *node); // Label 阶段
void burg_select(Node *node, int goal); // Select 阶段
BURG 的完整工作流程
text
输入:
IR 树 + 模式描述(.burg 文件)
BURG 生成器:
解析 .burg 文件
生成状态机(自动机)
生成 label() 和 select() 函数
编译器使用:
① 对 IR 树调用 label() → 每个节点标注最优规则和代价
② 对 IR 树调用 select() → 自顶向下选择规则,生成指令
③ 输出汇编代码
总结
| 概念 | 含义 |
|---|---|
| BURG | Bottom-Up Rewrite Generator,自底向上重写生成器 |
| 输入 | 模式描述(.burg 文件),每条模式有代价 |
| 输出 | 匹配器(label + select 函数) |
| Label | 自底向上遍历,给每个节点标注最优规则和代价 |
| Select | 自顶向下遍历,每步选择到达目标最便宜的路径 |
| 核心算法 | 动态规划,保证找到最小代价覆盖 |
| 与 Maximal Munch 的区别 | Maximal Munch 贪心,BURG 动态规划保证最优 |
一句话:BURG 是编译器指令选择的经典工具。
它把"用机器指令覆盖 IR 树"转化为"动态规划求最小代价覆盖",通过 Label(自底向上计算代价)和 Select(自顶向下选择规则)两步,保证生成的指令序列总代价最小。