本次笔记内容:

8-1 流图

8-2 常用代码优化方法一

8-3 常用代码优化方案二

8-4 基本快的优化

本节课幻灯片,见于我的 GitHub 仓库:​​第16讲 代码优化_1.pdf​


文章目录






流图

基本块(Basic Block)

​基本块​​​是满足下列条件的​​最大​​​的​​连续​​三地址指令序列:


  • 控制流只能从基本块的​​第一个指令​​进入该块。也就是说,没有跳转到基本块中间或末尾指令的转移指令
  • 除了基本块的​​最后一个指令​​,控制流在离开基本块之前不会跳转或者停机

基本块划分算法

输入:

  • 三地址指令序列

输出:

  • 输入序列对应的​​基本块列表​​,其中每个指令恰好被分配给一个基本块

方法:

  • 首先,确定指令序列中哪些指令是​​首指令​​(leaders),即某个基本块的第一个指令

  1. 指令序列的​​第一个三地址指令​​是一个首指令
  2. 任意一个条件或无条件​​转移指令的目标指令​​是一个首指令
  3. 紧跟在一个条件或无条件​​转移指令之后的指令​​是一个首指令

  • 然后,每个首指令对应的基本块包括了从它自己开始,直到​​下一个首指令​​​(不含)或者​​指令序列结尾​​之间的所有指令

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图

如上是快速排序法的部分源代码。

根据跳转指令找首指令(如跳转指令后的一条指令)。

流图(Flow Graphs)


  • 流图的​​结点​​​是一些​​基本块​
  • 从基本块B到基本块C之间有一条​​边​​​当且仅当基本块C的第一个指令​​可能​​紧跟在B的最后一条指令之后执行

此时称B是C的​​前驱​​​(predecessor) ,C是B的​​后继​​(successor)。

有两种方式可以确认这样的边:


  • 有一个​​从B的结尾跳转到C的开头​​的条件或无条件跳转语句
  • 按照原来的三地址语句序列中的顺序,C紧跟在之B后,且B的结尾不存在无条件跳转语句

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_02

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_03

感觉像是描述各个运算部分的关系。

常用的代码优化方法(1)

优化的分类


  • 机器无关优化:针对中间代码
  • 机器相关优化:针对目标代码
  • 局部代码优化:单个基本块范围内的优化
  • 全局代码优化:面向多个基本块的优化

常用的优化方法


  • 删除公共子表达式
  • 删除无用代码
  • 常量合并
  • 代码移动
  • 强度削弱
  • 删除归纳变量

①删除公共子表达式

公共子表达式:

  • 如果表达式​​x op y​​​先前已被计算过,并且从先前的计算到现在,​​x op y​​​中变量的值没有改变,那么​​x op y​​​的这次出现就称为​​公共子表达式(common subexpression)​

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_编译器_04

将 B3 重构如黄色区域。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_05

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_06

由 B3 “逆流而上”,发现 4 ∗ i 4*i 4∗i 没有被修改过,则其是一个公共子表达式。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_07

进行了再次的画家如上。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_编译器_08

发现 a [ t 2 ] a[t_2] a[t2​] 与 a [ t 4 ] a[t_4] a[t4​] 也是公共子表达式。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_09

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_10

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_11

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_12

a [ t 1 ] a[t_1] a[t1​]能否作为公共子表达式?

把 a [ t 1 ] a[t_1] a[t1]作为公共子表达式是不稳妥的,因为控制离开 B 1 B_1 B1进入 B 6 B_6 B6之前可能进入 B 5 B_5 B5,而 B 5 B_5 B5有对 a a a的赋值。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_13

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_14

关键问题:如何自动识别公共子表达式?

会在后面的课程详细介绍。

常用的代码优化方法(2)

②删除无用代码

复制传播:常用的​​公共子表达式消除算法​​​和其它一些优化算法会引入一些​​复制语句​​​(形如​​x=y​​的赋值语句)

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_15

复制传播:在复制语句x= y之后尽可能地用y代替x。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_16

​无用代码​​(死代码Dead-Code):其计算结果​​永远不会被使用​​的语句。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_17

程序员不大可能有意引入无用代码,无用代码通常是因为前面执行过的某些转换而造成的。

如何自动识别无用代码?

也将在后文详细介绍。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_18

如上,通过​​删除公共子表达式​​与​​删除无用代码​​,将 B5 与 B6 简化了不少。

③常量合并(Constant Folding)

如果在​​编译时刻​​​推导出​​一个表达式的值是常量​​​,就可以使用该常量来替代这个表达式。该技术被称为​​常量合并​​。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_19

④代码移动(Code Motion)

这个转换处理的是那些​​不管循环执行多少次都得到相同结果的表达式​​​(即​​循环不变计算​​,loop-invariant computation) ,在进入循环之前就对它们求值。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_20

如何自动识别循环不变计算?

循环不变计算的相对性

对于多重嵌套的循环,循环不变计算是相对于某个循环而言的。可能对于更加外层的循环,它就不是循环不变计算。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_21

⑤强度削弱(Strength Reduction)

用较快的操作代替较慢的操作,如用加代替乘。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_编译器_22

循环中的强度削弱

对于一个变量x ,如果存在一个正的或负的​​常数​​​c使得每次x被赋值时它的值总增加c ,那么x就称为​​归纳变量​​(Induction Variable)。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_23

归纳变量可以通过在每次循环迭代中进行一次简单的增量运算(​​加法​​或​​减法​​)来计算。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_24

⑥删除归纳变量

在沿着循环运行时,如果有​​一组归纳变量​​的值的变化保持​​步调一致​​,常常可以将这组变量删除为只剩一个。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_25

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_26

如上, i i i 与 j j j 都无用了。

基本块的优化

很多重要的​​局部优化技术​​​首先把一个​​基本块​​​转换成为一个​​无环有向图​​(directed acyclic graph,DAG)。

基本块的 DAG 表示

基本块中的每个​​语句​​​s都对应一个​​内部结点​​N:


  • 结点N的​​标号​​​是s中的​​运算符​​​;同时还有一个​​定值变量表​​被关联到N ,表示s是在此基本块内最晚对表中变量进行定值的语句
  • N的​​子结点​​​是基本块中在s之前、最后一个对s所使用的​​运算分量​​​进行定值的​​语句对应的结点​​​。如果s的某个运算分量在基本块内没有在s之前被定值,则这个运算分量对应的子结点就是代表该运算分量初始值的叶结点(为区别起见,​​叶节点​​的定值变量表中的变量加上下脚标0)
  • 在为语句x=y+z构造结点N的时候,如果x已经在某结点M的定值变量表中,则从M的定值变量表中删除变量x

例,有基本块:

a = b + c
b = a - d
c = b + c
d = a - d

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_27

对于形如 x=y+z 的三地址指令,如果已经有一个结点表示 y+z,就不往 DAG 中增加新的结点,而是给已经存在的结点附加​​定值变量​​x。

基于基本块的 DAG 删除无用代码

从一个DAG上删除所有​​没有附加活跃变量​​(活跃变量是指其值可能会在以后被使用的变量)的​​根结点​​(即没有父结点的结点) 。重复应用这样的处理过程就可以从DAG中消除所有对应于无用代码的结点。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_流图_28

数组元素赋值指令的表示

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_基本块_29

如上,因为有可能出现 i = j i=j i=j ,因此不能轻易把 a [ i ] a[i] a[i] 算作公共子表达式。


  • 对于形如a[j] = y的三地址指令,创建一个运算符为“​​[]=​​”的结点,这个结点有3个子结点,分别表示a、j和y
  • 该结点没有定值变量表
  • 该结点的创建将​​杀死​​所有已经建立的、其值依赖于a的结点
  • 一个被杀死的结点​​不能再获得任何定值变量​​,也就是说,它不可能成为一个公共子表达式

根据基本块的DAG可以获得一些非常有用的信息


  • 确定哪些变量的值在该基本块中赋值前被​​引用​​​过:在DAG中​​创建了叶结点​​的那些变量
  • 确定哪些语句计算的值可以在基本块外被引用:在DAG构造过程中为语句s(该语句为变量x定值)创建的节点N,在DAG构造结束时x仍然是N的定值变量

从 DAG 到基本块的重组

对每个具有若干定值变量的节点,构造一个​​三地址语句​​来计算其中某个变量的值。

倾向于把计算得到的结果赋给一个在基本块​​出口处活跃​​​的变量(如果没有​​全局活跃变量的信息​​作为依据,就要假设所有变量都在基本块出口处活跃,但是不包含编译器为处理表达式而生成的临时变量)。

如果结点有​​多个附加的活跃变量​​​,就必须引入​​复制语句​​,以便给每一个变量都赋予正确的值。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_DAG_30

构建 DAG 如右边。常量直接标记出来。

【编译原理笔记16】代码优化:流图,常用代码优化方法, 基本块的优化_结点_31

最终,根据 DAG 得到优化后的基本块如下:

D = A + C
E = A * C
F = E + D
L = 15 + F