CS143 斯坦福大学编译原理-Optimization

简介

编译流程

L.A. Parsing Semantic A. Opt. Code Gen. 代码优化器是现在编译器中最复杂的,也是耗时最长的。

优化时机

  • 对于AST Pro:机器无关 Con:层次太多
  • 对于汇编 Pro:暴露优化选项 Con:机器相关 Con:不同目标必须重新实现优化器
  • 对于中间码 Pro:机器无关 Pro:暴露优化选项

Basic block基本块

是一个最大指令序列,拥有以下特性: - 没有标签(第一行指令除外) - 没有跳转(最后一行指令除外)

思路

  • 不能跳转到基本块内部(第一行指令除外)
  • 不能跳出基本块外部(最后一行指令除外)
  • 基本块是一个单入口、单出口、单线性代码段

示例说明

一般不能优化。除非能其他地方没有使用t变量。

Control-flow graph控制流图

有以下特点: - 基本块为节点 - 一条边由基本块A指向基本块B,即A的最后一条指令指向B的第一条指令

示例

所有节点都有结束

目的

代码优化主要是提升程序资源利用率,比如: - 执行时间 - 代码大小 - 网络消息 优化不能改变计算结果。 按粒度分有三类优化器: - 局部优化:应用在基本块 - 全局优化:应用在控制流(方法体中) - 函数间优化:应用在跨方法边间 大部分编译器做局部优化,许多会做全局优化,只有少数会做函数间优化。

Local Optimization

最简单的优化器。 主要关注基本块内部。

删除优化

简化优化

常量折叠

在编译时,根据上下文能计算结果的可以优化:

注意

常量优化有时是比较危险的。比如在嵌入式开发中进行交叉编译,浮点数常量会存在精度问题。

不可达优化

代码块没有被其他jump指令跳转即不可达基本块。移除不可达基本块可以减小生成代码大小,有时运行会更快。

单声明

每个寄存器只进行一次声明,减少重复声明。

相同表达式

中间过程寄存器值不变,直接结果赋值。

拷贝传播

代码块中有w:=x的申明,使用x替换w。 一般配合其他优化共同作用,常量折叠、删除优化等。

特点

  • 优化器交叉作用
  • 编译优化需要不断调用优化器,直到没有提升的可能结束

综合示例

示例1 示例2 初始化代码: 最终形式:

Peephole Optimization

简介

直接应用于汇编代码。 Peephole是一组精简(通常是连续的)的指令序列。 优化器使用等效的代码序列替换它们。

示例说明

示例1

示例2

示例3

特点

与[[#Local Optimization]]相似,要想获得最佳效果必须要不断重复进行优化。

Dataflow Analysis

简介

为使用常量k替换变量x,需要满足条件: - 使用变量x的每条路径均为x:=k

示例说明

X为常量,可以优化: 左边X值改变,不能优化:

特点

  • 检测条件是否满足不能太琐碎
  • 所有路径包括循环路径和条件分支
  • 检测条件依赖全局数据流分析 全局优化任务有几个方法:
  • 依靠执行程序已知的属性X
  • 依靠整个程序的体系在任意点证明X
  • 保守没问题 全局数据流分析是解决这类问题的标准技术。 全局常量传播是一种优化手段,它依赖全局数据流分析。

Constant Propagation

约定

假定需要在每个代码点观测X的值,定义如下符号: 对于每个状态s,可以在s前后迅速得到x的值信息:

规则

Rule 1

前面有声明,则输入为声明

Rule 2

前面存在不同常量,则输入为T

Rule 3

前面所有值为相同常量或未执行,则输入为常量

Rule 4

前面都为未执行,则输入为未执行

Rule 5

输入为未执行,输出为未执行

Rule 6

输入不为未执行,且c为常量,输出为常量

Rule 7

输入不为未执行,且x:=f(…),输出为声明

Rule 8

输出与输入相同,声明y且y不等于x

通用步骤

  1. 程序的每个入口s,初始值为C(s,x,in)=T
  2. 除步骤1的每个地方C(s,x,in)=C(s,x,out)=⊥
  3. 不断遍历所有状态s,将不满足规则1-8的,进行更新。直到全部s符合规则

示例说明

初始状态 最终状态

Orderings

简化分析表达,可以得到:⊥ < c < T 使用图形表达是 构造函数lub计算最小上边界,例如: lub(⊥,1)=1 lub(T,⊥)=T lub(1,2)=T 任意两点通过图形找到通用的边界 使用lub可以将Rule 1改写为: C(s,x,in)=lub{C(p,x,out)|p是s的前一状态}

特点

  • 状态值只能由⊥开始,并持续上升
  • ⊥可以变为常数,常数可以变为T
  • C(s,x,in/out)最多只能变化两次

Liveness Analysis

简介

如图,第一个X没有被使用,就是dead;第二个X就是live。

基本定义

如何判断变量x在状态s中一直存活: - 存在一个状态s’使用变量x - 有一个路径由s指向s’ - 这个路径中没有重新声明x 若在变量x声明后又遇到x:=…,就说明x存活结束(dead)

规则

Rule 1

Rule 2

Rule 3

Rule 4

通用步骤

  1. 初始化所有L(…)=false
  2. 重复选择规则1-4中的s,并按照规则更新,直到所有s满足规则1-4

示例说明

特点

  • false可以变为true,但不能反向;(即false<true)
  • 每个值只能改变一次,因此循环结束有保障
  • 使用存活分析,可以消除多余代码很容易 前面介绍的常量传播是正向分析,即信息由输入到输出推出的; 存活分析是后向分析,即信息由输出反向到输入推出的;

Register Allocation

简介

中间码使用大量的临时变量。需要使用与物理寄存器数量匹配的临时变量。 主要方式有: - 为每个寄存器声明多个临时变量 - 不能改变程序行为 简单示例

示例说明

获取每个点存活的变量:

register interference graph

构建一个无向图,这也是寄存器干涉图 - 每个临时变量一个节点 - 边连接的节点不能同时被相同寄存器替代 两个能用相同寄存器存储的节点,不存在边直接连接它们 简单示例

Graph Coloring

简介

边连接的节点有不同的颜色。若图有k种颜色,则它就是k可着色(k-colorable) 其中有多少颜色就需要等量的寄存器。

问题

如何计算图有多少颜色不是很容易的。 - 可以使用heuristics 若需要的寄存器少于颜色 - 可以使用后面讲解spilling

简单方案

步骤: - 在图中选择邻居少于k的节点t - 从图中将节点t和它的边移除 - 若结果图就是k可着色,那这就是原图

泛化步骤: 1. 构造堆栈 - 在图中选择邻居少于k的节点t - 从图中将节点t和它的边移除,并将节点t放入堆栈 - 重复上述步骤直到图为空

  1. 为节点涂色
  • 从堆栈中获取节点
  • 新节点颜色与上色的邻居不一致即可

示例说明

开始的图形: 所有节点少于4邻居: 所有节点入栈: 节点上色: 最终结果:

Spilling

若最终寄存器数量不足,则需要将数据“持久化”。 示例: 使用图中三颜色 可以选择一个节点,比如f将它存入内存。 简化后可以得到满足条件的图

具体步骤

  • 若颜色优化失败,则声明内存存放f
  • 在每处读取f前,增加指令从内存中恢复f再操作
  • 在每处写入f后,增加指令将结果写入内存 示例 spilling后的图可以化简为

注意

spilling选定的临时变量有一些要求: - 邻居要尽量多 - 声明少且使用少 - 避免在循环内使用(会增加循环时间复杂度)

缓存管理

简介

编译器擅长管理寄存器,但不擅长管理缓存。编译器可以对缓存进行优化的空间很小。

内存管理

简介

需要对临时对象内存的生命周期进行管理(声明由程序进行)。 如何判断对象是否为不再使用则是内存管理的关键。 示例:

管理方法

标记mark阶段: 清理sweep阶段: 遍历堆中所有对象标记为0的,将其加入到释放列表中

完整过程示例