世界は優しい
首页博客关于

Site

世界は優しい

世界很温柔,我们都在努力变得更好。

Navigation

  • 首页
  • 博客
  • 关于

Connect

  • GitHub
  • 作者

© 2026 ZZC. 本站内容以 CC BY-NC-SA 4.0 协议发布。

Built with Next.js · Tailwind CSS

Folders

课程介绍与评分Lec1: IntroductionLec2: Lexical AnalysisLec3: ParsingLec4: Abstract SyntaxLec5: Semantic AnalysisLec6: Activation RecordsLec7: Translate to Intermediate CodeLec8: Basic Blocks and TracesLec9: 指令选择Lec10: Liveness Analysis
Lec0: 课程介绍与成绩Lec1: IntroductionLec2: Operating-System StructuresLec3: ProcessesLec4: Threads(多线程编程)Lec5: CPU SchedulingLec6: 进程同步Lec7: DeadlocksLec8: Main MemoryLec9: Virtual MemoryLec10: File-System InterfaceLec11: File System ImplementationLec12: Mass-Storage System
Lec1: Basic Concepts in Reinforcement LearningLec2: Bellman EquationLec3: Bellman Optimality EquationLec4: Value Iteration & Policy IterationLec5: Monte Carlo Learning
首页
ManiGaussian 论文笔记ManiGaussian++ 论文笔记
AMP: 对抗动作先验替代复杂奖励函数DeepMimic: 从动作捕捉数据学习物理仿真角色技能DreamWaQ: 纯本体感知的四足鲁棒行走Imitating Animals: 从动物模仿到真实四足敏捷运动MoE-Loco: 多任务腿足运动的专家混合架构Multi-AMP: 多重对抗动作先验学习高级技能PIE: Proprioception with Imagination for ParkourRMA: Rapid Motor Adaptation for Legged Robots
论文阅读
Lec1: 五十音Lec2: 日语声调Lec3: 浊音和长音
Callout 语法速查
Hello World - 我的第一篇博客
Typora 语法兼容性测试
首页博客Coure-NotebookCompiler_PrincipleLec9: 指令选择

编译原理

Lec9: 指令选择

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

2026 年 04 月 17 日/33 min read/ZZCZZC
#编译原理#课程笔记#计算机科学

第 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)f(x)=覆盖 x 的 tile tmin​​ct​+i∈leaves(t)∑​f(i)​

其中:

  • 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' 都是常数时,两者都是线性时间。

ZZC

Written by

ZZC
每天研究怎么摸鱼的神人

Comments

评论功能即将上线

On this page

  • 第 9 章关注什么?
  • 1.1 为什么 IR 不能直接当机器指令?
  • 1.2 用模式匹配做指令选择
  • 2.1 树模式
  • 2.2 瓦片覆盖
  • 2.3 Jouette 体系结构
  • 3.1 小 Tile
  • 3.2 两种不同覆盖方式
  • 4.1 Optimum Tiling
  • 4.2 Optimal Tiling
  • 4.3 两者关系
  • 4.4 模式表的要求
  • 5.1 基本思想
  • 5.2 什么叫最大 tile?
  • 5.3 用 `a[i] := x` 看自顶向下
  • 5.4 Maximal Munch 的实现思想
  • 6.1 为什么 maximal munch 不够?
  • 6.2 DP 的核心定义
  • 6.3 DP 的工作顺序
  • 6.4 DP 例子:`MEM(+(CONST 1, CONST 2))`
  • 步骤 1:CONST 节点
  • 步骤 2:`+` 节点
  • 步骤 3:`MEM` 节点
  • 6.5 DP 的指令输出阶段
  • 7.1 快速匹配
  • 7.2 复杂度中的变量
  • 7.3 为什么 maximal munch 是 `(K' + T') N / K`?
  • 7.4 为什么 DP 是 `(K' + T') N`?
  • 8.1 为什么需要树文法?
  • 8.2 简化版 Jouette
  • 8.3 树文法的非终结符
  • 8.4 树文法的歧义
  • 9.1 RISC 与 CISC
  • 9.2 寄存器较少
  • 9.3 寄存器类别
  • 9.4 二地址指令
  • 9.5 算术运算可以访问内存
  • 9.6 多种寻址模式
  • 9.7 变长指令
  • 9.8 带副作用的指令
  • 9.9 RISC 和 CISC 对算法选择的影响
  • 10.1 为什么寄存器分配放在指令选择后?
  • 10.2 抽象汇编指令
  • 10.3 OPER 需要记录什么?
  • 10.4 LABEL 和 MOVE
  • 10.5 机器无关性
  • 10.6 抽象汇编中的二地址指令
  • 10.7 过程调用
  • 10.8 如果没有帧指针