斯坦福cs231(编译原理)の 9 Optimization
本文是斯坦福cs143编译原理的笔记,内容大部分来自于课件和自己的理解,笔者能力和精力有限,如果有错误欢迎指出
中间代码
什么是中间代码?一般常指介于高级语言(源语言)和低级语言(汇编语言)的一种语言
- Provides an intermediate level of abstraction
- More details than the source
- Fewer details then the target
source lang =>intermediate lang => target lang
intermediate lang = high-level assembly
- uses register names, but has unlimited number
- Uses control structures like assemblly language
- uses opcodes but some are higher level
- Eg: push translates to serveral assembly instructions
- most opcodes correspond directly to assembly opcodes
每个指令都是以下两种形式之一(三地址码):
- x := y op z
- x := op y
🌰:表达式 x + y * z 可以转换成如下中间代码形式(每一个子表达式都有一个(寄存器)名称):
t1 := y * z
t2 := x + t1
总结
中间代码的好处:与机器无关,可以在中间代码实现优化,提高了程序在不同系统架构之间迁移的可能性
优化
Optimization is complex and largest phase
Parsing => Semantic => Opt => Gen
什么时候做优化?
- On AST ?
- Pro: Machine independent
- Cons: Too high level
- On assembly lang ?
- Pro: Exposes optimization oppotunities
- Cons: Machine dependent
- Cons: Must reimplement optimizations when retargetting
- On intermediate lang ?
- Pro: machine independent
- Pro: Exposes optimization oppotunities
Basic Block: is a maximal sequence of instructions with:
- no labels (except at the first instructions), and
- no jumps (except at the last instructions)
其实就是指一段除了起始入口和末尾跳转(退出)指令没有其他跳转(退出)指令的一段指令集
A basic block is a single-entry, single-exit, straight-line code segment
单一入口,单一出口,一行一行执行的程序段
control-flow graph:is a directed graph with:
- Basic blocks as nodes
- An edge from block A to block B if the execution can pass from the last instruction in A to the first instruction in B
eg:
- the last instruction in A is
jump LabelB
- execution can fall through from block A to block B (在block A执行失败了后跳转到了block B)
优化的好处:
- 提高执行时间
- 减小代码体积
- 减少网络传输量
- 减小内存的使用
- 减小硬盘的使用(存储汇编代码文件)
- 减少硬件使用的电量 (这也可以,...)
优化的三个粒度:
- local optimizations (局部优化)
- Apply to a basic block isolation
- global optimizations (全局优化)
- Apply to a control-flow in isolation
- Inter-procedural optimizations
- Apply across method boundaries
实际情况下:很难实现一个非常理想化的优化算法
- 为什么?
- 某些优化难以实现
- 某些优化会花费大量的编译时间
- 一些优化的回报很低
- 很多花哨的优化同时满足这三点
局部优化
优化基本block,不涉及整个代码,比较简单
algebra optimization (代数优化)
有些运算可以被另一些更快的运算代替:
x := x * 8 => x := x << 3
Constant fold(常数折叠)
Operations on constants can be computed at compile time:
- if there is a statement x := y op z
- and y and z constants
- then y op z can be computed at compile time
Eg: x := 2 + 2 => x := 4; if 2 < 0 jump L can be deleted
常数折叠也并不安全,因为会存在交叉编译的情况:
在X架构上编译到架构Y上运行,编译后的产物在X上运行和在Y上运行可能会产生不同的结果
X: a : = 1.2 + 6.9 经过常数折叠后=> a := 8.1
Y: a: = 8
单一赋值形式:每一个寄存器名称仅仅出现在一次在赋值语句的左侧
x := z + b := z + y
a := x => a := b
x := 2 * x x := 2 * b
这里有一个很重要的概念,和变量活跃度有关:Single assigment form
基于这个假设:如果基本块是以单一赋值形式出现的,即 x:=
是在该块内仅有的一次为x赋值
那么当块内出现了相同的右侧表达式,这个表达式就是重复的(Common subexpression elimination)
🌰: 这个例子里x,y和z的值在省略号里是不会改变的
x := y + z x := y + z
... => ...
w := y + z w := x
Copy propagatation(拷贝传播)
假设基本块是以单一赋值形式存在的:
if w := x appears in a block, replace subsequent uses of w with uses of x
🌰:
b := z + y b := z + y
a := b => a := b
x := 2 * a x := 2 * b
Dead code elimination
上面的a := b
对代码结果没什么恭喜,可以删除
Unresearchable code delete
删除不可达代码,减小代码体积
Eg: 不可能走的条件分支语句;导入了一个包里所有的工具,但是有没有用(有点类似tree shaking)

代数优化后:

拷贝传播后:

常数折叠:

公共子表达式删除:
最终:

Peephole Optimization
全局优化
为了在基本块之间用常数k代替变量x,编译器必须知道每条使用变量x路径的最后一个关于x的赋值语句x := k
~~
对于全局优化,全局常量传播应该在~~处执行
所以全局优化的目标就是找到所有的~~
定义如下形式化符号:
value | |
---|---|
$ $ | This statement never executes(还没执行,一般用于初始化) |
$ C $ | X = C (C is constant) |
$$ | X is not a constant(已执行,但是不确定具体值) |
常数拷贝:常数拷贝是很有用的因为它可以使得一些变量直接转换成常量,从而减少寄存器的使用,但是在控制图里,判断一个变量是否可以被替换成常量是困难的
一种思路是一个变量的信息和它的上下文有关(前后语句) \[ C(s, x, in) = value \quad of \quad x \quad before \quad s \quad 在语句s之前变量x的信息 \]
\[ C(s, x, out) = value \quad of \quad x \quad after \quad s \quad 在语句s之后变量x的信息 \]
由于课本上的规则过于复杂,本人总结了以下规则rules:
- 针对某个节点的入度,有以下几种情况:
- 如果其入度节点其中有一个是T,则该节点信息为T
- 如果其入度节点存在常量,但是均不相等,则该节点信息为T
- 如果其入度节点存在节点固定为某个常量,且其入度节点不为T,则该节点也为该常量
- 如果其入度节点均为unknown,则该节点为unknown
- 针对某个节点的出度,
入度为unknown,则出度也为unknown
在s语句赋值为某个常量,则出度为该常量
在s语句赋值为某个非常量表达式,则出度为T
s语句如果没有对入度节点做任何更改,则出度=入度
伪代码描述常量传播:
For every entry s to the program,,set C(s, x, in) = \(\top\) set C(s, x, in) = C(s, x, out) = \[\perp\] everywhere else repeat util all points satisfy 1-8: Pick s not satisfying 1-8 and update using the appropriate rule
为什么要引入$ $?
由于循环,循环的每个点都需要值存在
直觉上,分配一些初始值去打破循环
$ $表示直到目前为止,控制流程还没有到达当前点;
\(\top\)事一种抽象值,因为不知道运行的时候具体的值,
C之间是不可比较的
\(\top\)是最大的,$ $是最小的
符号形式化:
对是$ ,, C$进行排序: \[ \perp < C < \top \] 可以形象化如下:

定义 lub
运算: 在这个层级规则下的最小上界
lub(x, y, z, ..)
:大于等于x,y,z,...的最小值
部分可以写作:
1 |
|
🌰:
Lub(到, 1) = 1
Lub(T, 1) = 1
Lub(1, 2) = T
之前简单的说一直重复直到没什么东西发生变化才停止是不准确,不规范的;
正式描述应该使用lub,lub为什么是正确的?
- values start as 到 and only increase
- $ $ can change to a constant, and a constant to \(\top\)
- Thus,
C(s, x, _)
can change at most twice
常量拷贝算法是的时间复杂度和程序大小成正比
Number of steps =
Number of C(...) values computed * 2 = ✖️2是因为每个语句的最多算两次
Number of program statements * 4 ✖️2是因为每个语句的in and out
变量生存分析:
从程序退出节点开始分析,向上回溯节点,
- 如果当前节点包含出度的活跃变量的赋值语句,则当前节点的入度,就不会在有这个活跃变量(因为重新赋值了,之前的相当于是dead code,可以被视为dead variable);
- 如果当前节点的rfs(右表达式)包含某个变量,则这些变量是活跃变量;

1 |
|
总结:
变量分析是自底向上,从程序的退出节点回溯的,因为程序退出的时候,可能希望某些变量依旧保存