视频加载失败

Lec9: 指令选择

把 canonical IR Tree 翻译成具体目标机器的 abstract assembly:树覆盖算法(Maximal Munch / Dynamic Programming)、指令模板与开销估计。

课程导航与课程讲次
课程讲次
文章目录

第 9 章关注什么?#

第 8 章已经做完两件事:

  1. 把 IR 树变成规范树。
  2. 重新排列规范树,使每个 CJUMP 后面紧跟假分支标签。

第 9 章接着做:

指令选择:从规范树生成伪汇编 / 抽象汇编代码。

整体位置:

中间表示
|
| 第 8 章
v
规范化后的中间表示
|
| 第 9 章:指令选择
v
抽象汇编代码
|
| 第 10/11 章:寄存器分配
v
汇编代码

后端从 IR 到机器码主要分三步:

阶段说法做什么
指令选择把 IR 映射成抽象汇编代码选择机器指令来实现 IR
寄存器分配用真实寄存器替换抽象寄存器决定哪些值放寄存器,并把抽象寄存器换成真实寄存器
指令调度本课程不讲重排指令以隐藏延迟、利用并行

第 1 部分:为什么需要指令选择?#

1.1 为什么 IR 不能直接当机器指令?#

IR 树语言中,每个树节点通常只表达一个原始操作:

  • 取内存
  • 存内存
  • 加法
  • 减法
  • 条件跳转

但真实机器指令经常可以一次完成多个原始操作。

典型例子是:

LOAD ri <- M[rj + c]

这条机器指令同时做了:

  1. 取寄存器 rj。
  2. 加上常量偏移 c。
  3. 用得到的地址访问内存。
  4. 把内存中的值放入 ri。

对应到 IR 树,大概是:

MEM
|
+
/ \
TEMP CONST
rj c

所以指令选择要解决的问题就是:

找到合适的机器指令来实现给定的 IR 树。

1.2 用模式匹配做指令选择#

两类 IR 对应不同的匹配方法:

IR 形态适合的方法
树形 IR在树上做模式匹配,用树模式
线性 IR在字符串或线性序列上做匹配,例如窥孔匹配

本章讨论的是树形 IR,所以核心工具是:

树模式 + 瓦片覆盖(tiling)


第 2 部分:树模式与瓦片覆盖#

2.1 树模式#

定义:

每条机器指令都可以表示成一片 IR 树,这片树称为 树模式。

也就是说,一条机器指令可以看成一片 IR 树。

例如 LOAD ri <- M[rj + c] 的模式可以画成:

指令:
LOAD ri <- M[rj + c]
树模式:
MEM
|
+
/ \
r CONST
c

这里的 r 表示某个寄存器值,CONST c 表示常量偏移。

2.2 瓦片覆盖#

定义:

瓦片覆盖:用不重叠的树模式覆盖整棵树。

也就是用一块块树模式覆盖整棵 IR 树:

  • 每块 tile(瓦片)对应一条合法机器指令。
  • tile 之间不能重叠。
  • 整棵 IR 树要被覆盖。
  • tile 的叶子处可以留下子树,继续递归处理。

可以把它理解成:

IR 树节点 = 原始操作
树模式 / tile = 一条机器指令能实现的一片 IR
瓦片覆盖 = 把整棵 IR 树切成若干机器指令

2.3 Jouette 体系结构#

为了说明指令选择,本章使用教材中的假想机器 Jouette。

需要强调几点:

  • BINOP(PLUS, x, y) 可以简写成 +(x, y)。
  • r0 = 0。
  • CONST 和 TEMP 节点的实际值有时不会画出来。
  • r0 永远包含 0。
  • 有些机器指令对应不止一个树模式。

例如,常量 c 可以通过 r0 得到:

ADDI ri <- r0 + c

对应的树模式可以看成:

CONST c

因为 r0 固定为 0,所以 r0 + c 就是 c。


第 3 部分:树模式例子:a[i] := x#

用 a[i] := x 展示同一棵 IR 树可以有多种瓦片覆盖方式。

原始树形大致如下:

MOVE
/ \
MEM MEM
| |
+ +
/ \ / \
MEM * FP CONST x
| / \
+ TEMP i CONST 4
/ \
FP CONST a

这棵树表示:

  • 左边 MEM(...) 是赋值目标 a[i] 的地址。
  • 右边 MEM(FP + x) 是变量 x 的值。
  • MEM(FP + a) 取出数组 a 的基地址。
  • TEMP i * CONST 4 计算下标偏移。
  • + 把数组基地址和偏移加起来,得到 a[i] 的地址。

3.1 小 Tile#

要点:

总是可以用很小的 tile 覆盖整棵树,让每个 tile 只覆盖一个节点。

也就是说,最保守的做法是每个 tile 只覆盖一个节点。这样一定能翻译,但指令会比较多。

对 a[i] := x,小 tile 风格的指令序列是:

ADDI r1 <- r0 + a
ADD r1 <- fp + r1
LOAD r1 <- M[r1 + 0]
ADDI r2 <- r0 + 4
MUL r2 <- ri * r2
ADD r1 <- r1 + r2
ADDI r2 <- r0 + x
ADD r2 <- fp + r2
LOAD r2 <- M[r2 + 0]
STORE M[r1 + 0] <- r2

这里可以看到:小 tile 会把地址计算、常量构造、取内存、存内存都拆得很细。

3.2 两种不同覆盖方式#

还可以比较 a[i] := x 的两种覆盖方式:

普通覆盖:6 个单位
使用 MOVEM 的覆盖:5 + m 个单位

其中:

  • 普通指令代价都是 1。
  • MOVEM 的代价是 m。

这说明:大 tile 不一定永远更好,关键要看代价。


第 4 部分:局部最优与全局最优覆盖#

一棵 IR 树可以有多种覆盖方式。所谓最佳覆盖,就是代价最低的指令序列。

在单发射、固定延迟机器上,最低代价通常就是:

指令条数最少

4.1 Optimum Tiling#

定义:

Optimum tiling(全局最优覆盖):所有 tile 的总代价达到全局最低。

也就是:

全局总代价最小

4.2 Optimal Tiling#

定义:

Optimal tiling(局部最优覆盖):不存在两个相邻 tile 能合并成一个代价更低的 tile。

也就是:

局部不能再改进

具体说,如果有两个相邻 tile:

tile A
|
tile B

并且它们能合并成一个更便宜的大 tile:

tile C

那原来的覆盖方式就不是局部最优。

4.3 两者关系#

结论:

每个全局最优覆盖都是局部最优覆盖,但反过来不一定成立。

也就是说:

  • 全局最优一定局部最优。
  • 局部最优不一定全局最优。

原因很直观:如果一个覆盖方式已经是全局代价最低,那它不可能存在“两个相邻 tile 合并后更便宜”的局部改进;否则总代价还能下降,和“全局最低”矛盾。

反过来不成立,因为没有局部改进,不代表所有全局组合都已经最好。

4.4 模式表的要求#

注意:

如果某个树模式总能拆成若干个总代价更低的小 tile,那么就应该把这个树模式从模式表中删掉。

也就是说,模式表里不该保留“永远比拆开更贵”的 tile。


第 5 部分:Maximal Munch#

5.1 基本思想#

maximal munch 的定位:

Maximal Munch:用于寻找局部最优覆盖的算法。

它假设:

越大的 tile 越好

算法思想是贪心:

  1. 从根节点开始。
  2. 找到能覆盖当前根节点的最大 tile。
  3. 用这个 tile 覆盖根节点附近的节点。
  4. tile 叶子处留下若干子树。
  5. 对这些子树重复同样过程。

“tile 的叶子接子树”的结构可以画成:

当前根节点
|
[ tile ]
/ \
叶子1 叶子2
| |
子树1 子树2

其中 [ tile ] 是当前已经选中的机器指令模式;子树1、子树2 是还没覆盖的部分。

5.2 什么叫最大 tile?#

要点:

最大 tile:节点数最多的 tile。

例如:

单节点 tile:
TEMP
双节点 tile:
MEM
|
r
三节点 tile:
+
/ \
r CONST

maximal munch 会优先选择能匹配的最大 tile。

如果两个 tile 大小相同,而且都能匹配根节点:

可以任意选择其中一个。

也就是任选一个。

5.3 用 a[i] := x 看自顶向下#

对这棵树:

MOVE
/ \
MEM MEM
| |
+ +
/ \ / \
MEM * FP CONST x
| / \
+ TEMP i CONST 4
/ \
FP CONST a

maximal munch 的“选择方向”是自顶向下:

  1. 先看根节点 MOVE。
  2. 找到能匹配 MOVE 的最大存储 / 移动类 tile。
  3. 这个 tile 的叶子位置可能留下目标地址子树和右值子树。
  4. 再分别处理这些子树。

注意:这里说的是“选择 tile 的方向”。实际输出指令时,子树结果通常要先算出来。

要点:

指令会按反向顺序生成。

一个简单例子:

IR 树:
*
/ \
+ MEM
/ \ |
a 3 p

表示:

(a + 3) * M[p]

maximal munch 选择 tile 时先看到根部 *,但真正输出指令要先产生 * 的两个操作数:

ADDI t1 <- a + 3 ; 先处理左边叶子子树
LOAD t2 <- M[p] ; 再处理右边叶子子树
MUL t3 <- t1 * t2 ; 最后输出根部 tile 对应的指令

所以它是:

选择 tile:先根节点
输出指令:先子节点,最后父节点

5.4 Maximal Munch 的实现思想#

只需要掌握这几点:

  • 有两个递归函数:
    • munchStm 处理语句。
    • munchExp 处理表达式。
  • 每个分支匹配一个 tile。
  • 分支按 tile 优先级排列,也就是大 tile 放前面。

所以 maximal munch 的“贪心”在实现上体现为:

先尝试大模式
匹配不到再尝试小模式

第 6 部分:用动态规划寻找全局最优覆盖#

6.1 为什么 maximal munch 不够?#

要点:

Maximal munch 总能找到局部最优覆盖,但不一定能找到全局最优覆盖。

原因:

  • maximal munch 是自顶向下的。
  • 它每次在当前根节点处做局部选择。
  • 它不知道这个选择会让下面的子树变便宜还是变贵。

如果要找 全局最优覆盖,需要动态规划。

6.2 DP 的核心定义#

定义:

节点 x 的代价记为 f(x),表示覆盖以 x 为根的子树所需的最小代价。

也就是说:

f(x) = 覆盖以 x 为根的整棵子树的最小代价

递推式:

f(x)=min⁡覆盖 x 的 tile t(ct+∑i∈leaves(t)f(i))f(x) = \min_{\text{覆盖 } x \text{ 的 tile } t} \left( c_t + \sum_{i \in leaves(t)} f(i) \right)

其中:

  • t:一个能覆盖当前节点 x 的 tile。
  • c_t:tile t 自身的代价。
  • leaves(t):tile 叶子处接上的子树根节点。
  • f(i):这些叶子子树的最优覆盖代价。

6.3 DP 的工作顺序#

给定根节点 n:

  1. 先递归求出 n 的子节点、孙节点等所有子树的代价。
  2. 然后在 n 处尝试每一种能匹配的树模式。
  3. 对每个 tile 计算:
tile 自身代价 + 所有叶子子树的 f 值
  1. 选择总代价最小的 tile。

所以 DP 是:

自底向上

6.4 DP 例子:MEM(+(CONST 1, CONST 2))#

例子是这棵树:

MEM
|
+
/ \
CONST 1 CONST 2

记号:

(a, b)

其中:

  • a 是最小代价。
  • b 是对应的模式编号。

步骤 1:CONST 节点#

两个 CONST 节点都一样:

CONST 1 CONST 2
(1,8) (1,8)

对应表格:

模式代价叶子代价总计
(8) CONST101

所以:

f(CONST 1) = 1
f(CONST 2) = 1

步骤 2:+ 节点#

现在看:

+
/ \
CONST 1 CONST 2
(1,8) (1,8)

有三种模式可以匹配 +:

模式代价叶子代价总计
(2) +(e1, e2)11 + 13
(6) +(CONST, e1)112
(7) +(e1, CONST)112

所以 + 节点的最小代价是 2:

+ (2,6)
/ \
CONST 1 CONST 2
(1,8) (1,8)

如果 (6) 和 (7) 都是 2,选哪个都可以。这里沿用 (6)。

步骤 3:MEM 节点#

现在看整棵树:

MEM
|
+ (2,6)
/ \
CONST 1 CONST 2
(1,8) (1,8)

有三种模式可以匹配 MEM:

模式代价叶子代价总计
(13) MEM(e1)123
(10) MEM(+(e1, CONST))112
(11) MEM(+(CONST, e1))112

所以根节点:

MEM (2,10)
|
+ (2,6)
/ \
CONST 1 CONST 2
(1,8) (1,8)

整棵树的全局最优代价是 2。

6.5 DP 的指令输出阶段#

一旦根节点的代价找到了,就开始输出指令:

输出指令(节点 n):
对节点 n 选中的 tile 的每个叶子 li:
输出指令(li)
输出节点 n 匹配到的指令

对上面的例子,输出是:

ADDI r1 <- r0 + 1
LOAD r1 <- M[r1 + 2]

对应理解:

  • 根节点选择的是 MEM(+(e1, CONST)) 这种读取模式。
  • 这个 tile 把 MEM、+、CONST 2 一起覆盖了。
  • 它留下的叶子是 CONST 1。
  • 所以先发 CONST 1 的指令,再发根部读取指令。

第 7 部分:快速匹配与复杂度#

7.1 快速匹配#

maximal munch 和 DP 都需要检查“哪些 tile 能匹配当前节点”。

tile 能匹配的条件:

tile 中每个非叶节点的标签,都要和 IR 树中对应节点的算子一样。

例如:

tile:
MEM
|
+
/ \
r CONST
IR 树:
MEM
|
+
/ \
TEMP CONST

这个 tile 可以匹配,因为非叶节点 MEM 和 + 都对上了。

为了快一点,可以按根节点标签分类:

如果当前节点是 MEM,只检查以 MEM 为根的模式。
如果当前节点是 BINOP,只检查以 BINOP 为根的模式。
如果当前节点是 CONST,只检查以 CONST 为根的模式。

目标:

尽量避免反复检查同一个 IR 树节点。

7.2 复杂度中的变量#

定义:

符号含义
K平均匹配到的 tile 含有 K 个带标签的非叶节点
N输入 IR 树的节点数
K'判断一个子树能匹配哪些 tile 时,最多需要检查的节点数
T'平均每个树节点能匹配的不同模式数

结论:

Maximal munch: 正比于 (K' + T') N / K
动态规划: 正比于 (K' + T') N

7.3 为什么 maximal munch 是 (K' + T') N / K?#

可以拆成两部分看。

第一,每次 maximal munch 在某个根节点上选 tile,需要做两类工作:

检查树节点以判断是否匹配 -> 最多 K'
在可匹配模式中做选择 -> 平均 T'

所以一次选择的成本近似是:

K' + T'

第二,maximal munch 每选中一个 tile,平均会覆盖 K 个带标签的非叶节点。

整棵树有 N 个节点,所以大约需要选择:

N / K

个 tile。

合起来就是:

每次选择成本 × 选择次数
= (K' + T') × (N / K)
= (K' + T') N / K

如果 K、K'、T' 都看成常数,那么:

(K' + T') N / K = 常数 × N

所以是线性时间。

7.4 为什么 DP 是 (K' + T') N?#

DP 要给每个树节点都计算一个代价。

对每个节点,仍然要检查:

K' + T'

量级的匹配工作。

一共有 N 个节点,所以总成本是:

(K' + T') × N

如果 K'、T' 是常数,DP 也是线性时间。

和 maximal munch 相比,DP 这里没有除以 K,因为 DP 不是“每选一个 tile 跳过一片节点”,而是要给每个节点都算一次最优代价。


第 8 部分:树文法#

8.1 为什么需要树文法?#

对于复杂指令集、多个寄存器类别和多种寻址模式:

  • 树模式更多。
  • 树模式更复杂。
  • 手写 tiling 算法会很繁琐,也更容易出错。

因此可以使用指令选择器生成器:

  1. 在单独的规格说明中定义树模式。
  2. 用通用的树模式匹配算法计算 tiling。

解决方式:

用树文法描述树模式,把指令选择转化成解析问题,并使用推广后的动态规划算法求解。

8.2 简化版 Jouette#

简化版 Jouette 把寄存器分成两类:

寄存器类别用途
a 寄存器地址计算
d 寄存器数据计算

要点:

每个 tile 的根和叶子都必须标记为 a 或 d。

也就是说,一个 tile 不只要说明树形结构,还要说明:

这个表达式算出来以后放在地址寄存器还是数据寄存器?

8.3 树文法的非终结符#

对应的非终结符:

非终结符含义
s语句
a计算结果放入 a 寄存器的表达式
d计算结果放入 d 寄存器的表达式

LOAD、MOVEA、MOVED 的规则可以写成:

d -> MEM(+(a, CONST))
d -> MEM(+(CONST, a))
d -> MEM(CONST)
d -> MEM(a)
d -> a
a -> d

8.4 树文法的歧义#

要点:

这样的文法高度歧义。

同一个表达式可能有许多条不同的指令序列实现它。

因此第 3 章的普通解析技术不太有用。更合适的是 DP 的推广:

对每个节点,记录它作为每个非终结符时的最小代价匹配。

例如:

节点 x:
作为 a 寄存器表达式的最小代价
作为 d 寄存器表达式的最小代价

用表格表示:

节点作为 a 的最小代价作为 d 的最小代价
xcost_a(x)cost_d(x)

这样就能处理寄存器类别。


第 9 部分:CISC 机器#

9.1 RISC 与 CISC#

RISC 和 CISC 的对照表:

RISC 机器CISC 机器
32 个寄存器寄存器较少,例如 16、8 或 6 个
整数 / 指针寄存器只有一类寄存器分成不同类别
算术运算只在寄存器之间进行算术运算可以访问寄存器或内存
三地址指令:r1 <- r2 op r3二地址指令:r1 <- r1 op r2
取内存 / 存内存只支持 M[reg + const]有多种寻址模式
每条指令恰好 32 位指令长度可变
每条指令只有一个结果或效果指令可能有副作用,例如自动递增寻址

9.2 寄存器较少#

问题:

CISC 寄存器较少。

解决办法:

自由生成 TEMP 节点,并假设寄存器分配器会处理好。

也就是指令选择阶段先自由地产生临时值,寄存器数量问题留给寄存器分配。

9.3 寄存器类别#

例子:Pentium 乘法指令。

  • 左操作数必须放在 eax
  • 结果的高位放进 edx
  • Tiger 程序不需要这些高位

解决办法:

显式移动操作数和结果。

例子:

目标: t1 <- t2 * t3
mov eax, t2 eax <- t2
mul t3 eax <- eax * t3; edx <- 无用值
mov t1, eax t1 <- eax

重点:如果某条指令会额外写坏某些寄存器,要让后续寄存器分配器知道。

9.4 二地址指令#

问题:

目标寄存器必须和第一个源寄存器相同。

例如想实现:

t1 <- t2 + t3

在二地址机器上可以生成:

mov t1, t2 t1 <- t2
add t1, t3 t1 <- t1 + t3

之后希望寄存器分配器能把 t1 和 t2 分配到同一个寄存器,这样 mov 可以被删除。

9.5 算术运算可以访问内存#

CISC 算术指令可以直接访问内存。下面是两段等价代码:

mov eax, [ebp - 8]
add eax, ecx
mov [ebp - 8], eax

和:

add [ebp - 8], ecx

右边更简洁,但这两段代码速度差不多。

左边的明显缺点是:

它会破坏 eax。

解决办法:

运算前先把所有操作数取到寄存器中,运算后再把结果存回内存。

9.6 多种寻址模式#

一个完成六件事的寻址模式通常也要六步执行。

复杂寻址模式的两个优势:

  • 破坏更少寄存器
  • 指令编码更短

结论:

  • 通过树匹配式指令选择,可以选择 CISC 的复杂寻址模式。
  • 但使用简单的 RISC 风格指令,程序也可能一样快。

9.7 变长指令#

要点:

这对编译器来说并不是真正的问题。

一旦指令选好了,具体编码交给汇编器做。

9.8 带副作用的指令#

例子:自增式内存读取。

r2 <- M[r1]
r1 <- r1 + 4

问题:

一条指令产生两个结果

这很难用树模式表示。

三个解决办法:

  1. 忽略自动递增指令,不使用它们。
  2. 用临时特判的方式匹配特殊代码习惯。
  3. 使用 DAG 模式,而不是树模式。

9.9 RISC 和 CISC 对算法选择的影响#

总结:

  • 求 optimal tiling 的算法比求 optimum tiling 的算法更简单。
  • 对 CISC 来说,optimum 和 optimal 的差异比较明显。
  • 对 RISC 来说,optimum 和 optimal 通常几乎没有差异。
  • 因此,对 RISC 来说,较简单的 tiling 算法就足够了。

原因:

CISC:某些指令每条能完成多个操作。
RISC:tile 小,而且代价比较统一。

第 10 部分:Tiger 的指令选择#

这一部分的重点不是具体写代码,而是说明指令选择和寄存器分配如何衔接。

10.1 为什么寄存器分配放在指令选择后?#

问题:

在一棵由指令模式覆盖的树中,每个 tile 的根都会对应一个保存在寄存器中的中间结果。那么该使用哪个寄存器?

回答:

寄存器分配的任务,就是给这些节点分配真实寄存器编号。

如果在指令选择之前做寄存器分配,会有问题:

还不知道哪些树节点需要寄存器保存结果

所以:

寄存器分配放在指令选择之后。

10.2 抽象汇编指令#

AS_instr 是:

尚未分配真实寄存器的汇编指令。

它有三类:

类别含义
OPER普通操作指令
LABEL跳转可以到达的程序位置
MOVE类似 OPER,但只能做数据搬运

10.3 OPER 需要记录什么?#

OPER 保存:

字段含义
assem汇编指令模板
src源操作数寄存器列表,可以为空
dst结果寄存器列表,可以为空
jumps可能跳转到的目标标签

如果一个操作总是顺序执行到下一条指令:

jumps = NULL

10.4 LABEL 和 MOVE#

LABEL:

  • 是跳转可以到达的点。
  • assem 说明标签在汇编中长什么样。
  • label 是实际使用的标签符号。

MOVE:

  • 类似 OPER。
  • 但必须只做数据搬运。

MOVE 单独列出来,是因为后续寄存器分配器有机会处理 move 指令。

10.5 机器无关性#

重点:

AS_instr 类型独立于具体选择的目标机器汇编语言。

抽象汇编中还没有真实寄存器。

例如抽象形式:

LOAD `d0 <- M[`s0 + 8]

这里:

  • `s0 表示第一个源 temp。
  • `d0 表示第一个目标 temp。

寄存器分配之后,可能打印成:

LOAD r1 <- M[r27 + 8]

也就是说,指令选择先生成带 temp 的抽象汇编;真实寄存器名之后再填。

另一个例子:

抽象汇编:
ADDI `d0 <- `s0 + 3
LOAD `d0 <- M[`s0 + 0]
MUL `d0 <- `s0 * `s1

寄存器分配后可能变成:

ADDI r1 <- r12 + 3
LOAD r2 <- M[r13 + 0]
MUL r1 <- r1 * r2

10.6 抽象汇编中的二地址指令#

例子:

add t1, t2

效果:

t1 <- t1 + t2

在抽象汇编中可以描述成:

汇编模板目标源
add `d0, `s1t1t1, t2

注意:

`s0` 隐式出现,但没有显式写在汇编模板字符串中。

意思是:t1 既是目标,也是第一个源。汇编文本里不显式写 `s0,但 src 列表里必须包含它,让后续分析知道它被读了。

10.7 过程调用#

区分:

过程调用:EXP(CALL(f, args))
函数调用:MOVE(TEMP t, CALL(f, args))

munchArgs 的任务:

生成代码,把所有参数移动到调用约定要求的位置,即寄存器或内存中。

CALL 会破坏一些寄存器:

  • 调用者保存寄存器
  • 返回地址寄存器
  • 返回值寄存器

这些 calldefs 应该列为 CALL 的目标寄存器。

一般原则:

任何带有“写入额外寄存器”副作用的指令,都需要这样处理。

10.8 如果没有帧指针#

传统帧指针做法:

  • 每次过程调用时,把栈指针寄存器复制到帧指针寄存器
  • 栈指针增加新栈帧的大小

然后给出虚拟帧指针:

优点:

  • 节省时间:不需要复制指令
  • 节省空间:多出一个寄存器可供其他用途使用

处理方式:

把 FP + k 替换成 SP + k + fs

其中 fs 是栈帧大小。


总结#

本章核心脉络可以压成一句话:

指令选择用树模式覆盖规范 IR 树,生成还没有分配真实寄存器的抽象汇编。

关键点:

主题要记住什么
树模式一条机器指令对应的一片 IR 树
瓦片覆盖用不重叠的树模式覆盖整棵 IR 树
Jouette用来说明瓦片覆盖的假想机器;r0 = 0
Optimum全局代价最小
Optimal相邻 tile 不能合并成更便宜的 tile
Maximal munch自顶向下贪心,找 optimal
动态规划自底向上,找 optimum
树文法用文法描述复杂树模式
CISC寄存器少、寄存器分类、二地址指令、复杂寻址、副作用
Tiger指令选择后再做寄存器分配

最容易混淆的两个词:

词含义对应算法
optimal局部最优maximal munch
optimum全局最优动态规划

复杂度也要记住:

Maximal munch: (K' + T') N / K
动态规划: (K' + T') N

当 K、K'、T' 都是常数时,两者都是线性时间。

1
第 9 章关注什么?
2
第 1 部分:为什么需要指令选择?
3
1.1 为什么 IR 不能直接当机器指令?
4
1.2 用模式匹配做指令选择
5
第 2 部分:树模式与瓦片覆盖
6
2.1 树模式
7
2.2 瓦片覆盖
8
2.3 Jouette 体系结构
9
第 3 部分:树模式例子:a[i] := x
10
3.1 小 Tile
11
3.2 两种不同覆盖方式
12
第 4 部分:局部最优与全局最优覆盖
13
4.1 Optimum Tiling
14
4.2 Optimal Tiling
15
4.3 两者关系
16
4.4 模式表的要求
17
第 5 部分:Maximal Munch
18
5.1 基本思想
19
5.2 什么叫最大 tile?
20
5.3 用 a[i] := x 看自顶向下
21
5.4 Maximal Munch 的实现思想
22
第 6 部分:用动态规划寻找全局最优覆盖
23
6.1 为什么 maximal munch 不够?
24
6.2 DP 的核心定义
25
6.3 DP 的工作顺序
26
6.4 DP 例子:MEM(+(CONST 1, CONST 2))
步骤 1:CONST 节点
步骤 2:+ 节点
步骤 3:MEM 节点
27
6.5 DP 的指令输出阶段
28
第 7 部分:快速匹配与复杂度
29
7.1 快速匹配
30
7.2 复杂度中的变量
31
7.3 为什么 maximal munch 是 (K' + T') N / K?
32
7.4 为什么 DP 是 (K' + T') N?
33
第 8 部分:树文法
34
8.1 为什么需要树文法?
35
8.2 简化版 Jouette
36
8.3 树文法的非终结符
37
8.4 树文法的歧义
38
第 9 部分:CISC 机器
39
9.1 RISC 与 CISC
40
9.2 寄存器较少
41
9.3 寄存器类别
42
9.4 二地址指令
43
9.5 算术运算可以访问内存
44
9.6 多种寻址模式
45
9.7 变长指令
46
9.8 带副作用的指令
47
9.9 RISC 和 CISC 对算法选择的影响
48
第 10 部分:Tiger 的指令选择
49
10.1 为什么寄存器分配放在指令选择后?
50
10.2 抽象汇编指令
51
10.3 OPER 需要记录什么?
52
10.4 LABEL 和 MOVE
53
10.5 机器无关性
54
10.6 抽象汇编中的二地址指令
55
10.7 过程调用
56
10.8 如果没有帧指针
57
总结
文章目录
1
第 9 章关注什么?
2
第 1 部分:为什么需要指令选择?
3
1.1 为什么 IR 不能直接当机器指令?
4
1.2 用模式匹配做指令选择
5
第 2 部分:树模式与瓦片覆盖
6
2.1 树模式
7
2.2 瓦片覆盖
8
2.3 Jouette 体系结构
9
第 3 部分:树模式例子:a[i] := x
10
3.1 小 Tile
11
3.2 两种不同覆盖方式
12
第 4 部分:局部最优与全局最优覆盖
13
4.1 Optimum Tiling
14
4.2 Optimal Tiling
15
4.3 两者关系
16
4.4 模式表的要求
17
第 5 部分:Maximal Munch
18
5.1 基本思想
19
5.2 什么叫最大 tile?
20
5.3 用 a[i] := x 看自顶向下
21
5.4 Maximal Munch 的实现思想
22
第 6 部分:用动态规划寻找全局最优覆盖
23
6.1 为什么 maximal munch 不够?
24
6.2 DP 的核心定义
25
6.3 DP 的工作顺序
26
6.4 DP 例子:MEM(+(CONST 1, CONST 2))
步骤 1:CONST 节点
步骤 2:+ 节点
步骤 3:MEM 节点
27
6.5 DP 的指令输出阶段
28
第 7 部分:快速匹配与复杂度
29
7.1 快速匹配
30
7.2 复杂度中的变量
31
7.3 为什么 maximal munch 是 (K' + T') N / K?
32
7.4 为什么 DP 是 (K' + T') N?
33
第 8 部分:树文法
34
8.1 为什么需要树文法?
35
8.2 简化版 Jouette
36
8.3 树文法的非终结符
37
8.4 树文法的歧义
38
第 9 部分:CISC 机器
39
9.1 RISC 与 CISC
40
9.2 寄存器较少
41
9.3 寄存器类别
42
9.4 二地址指令
43
9.5 算术运算可以访问内存
44
9.6 多种寻址模式
45
9.7 变长指令
46
9.8 带副作用的指令
47
9.9 RISC 和 CISC 对算法选择的影响
48
第 10 部分:Tiger 的指令选择
49
10.1 为什么寄存器分配放在指令选择后?
50
10.2 抽象汇编指令
51
10.3 OPER 需要记录什么?
52
10.4 LABEL 和 MOVE
53
10.5 机器无关性
54
10.6 抽象汇编中的二地址指令
55
10.7 过程调用
56
10.8 如果没有帧指针
57
总结