The Decaf Book
  • Introduction
  • 语言规范
  • Java 框架分阶段指导
    • 本章导引
    • PA1-A:语法分析器的自动构造
      • 阶段任务
      • 词法分析
      • 抽象语法树
      • 文法分析
    • PA1-B:基于 LL(1) 的语法分析器半自动构造
      • 阶段任务
      • LL(1) 文法分析
      • 错误恢复
    • PA2:语义分析
      • 阶段任务
      • 类型、符号和作用域
      • 访问者模式
      • 符号表构建
      • 类型检查
    • PA3:中间代码生成
      • 阶段任务
      • TAC 程序
      • 面向对象机制
      • 控制流的翻译
    • PA4:中间代码优化
      • 实验内容
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
  • Scala 框架分阶段指导
    • 本章导引
    • PA1:语法分析器的自动构造
      • 阶段任务
      • 词法分析
      • 抽象语法树
      • 文法分析
      • 访问者模式
    • PA2:语义分析
      • 阶段任务
      • 类型、符号和作用域
      • 符号表构建
      • 类型检查
    • PA3:中间代码生成
      • 阶段任务
      • TAC 程序
      • 面向对象机制
      • 控制流的翻译
    • PA3-JVM:JVM 字节码生成
      • 阶段任务
      • JVM 字节码简介
      • 翻译过程
    • PA4:中间代码优化
      • 实验内容
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
  • Rust 框架分阶段指导
    • 本章导引
    • PA1-A:语法分析器的自动构造
      • 实验内容
      • lalr1使用指导
        • 编写lexer
        • impl块的可选属性
        • 产生式和语法动作
        • 解决冲突
        • 一个完整的例子
      • 抽象语法树
      • 框架中部分实现的解释
      • 文件结构
    • PA1-B:基于 LL(1) 的语法分析器半自动构造
      • 实验内容
      • lalr1使用指导
      • 错误恢复
      • 文件结构
    • PA2:语义分析
      • 实验内容
      • 语义分析
      • 符号表
      • visitor模式
    • PA3:中间代码生成
      • 实验内容
      • 中间代码
      • 中间代码中的类型信息
      • 运行时存储布局
      • 面向对象机制
      • tacvm简述
    • PA4:中间代码优化
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
    • PA5:寄存器分配
      • 实验内容
      • 图着色基本原理
      • 着色算法
      • 预着色节点
      • 干涉图节点合并
      • 调用约定
Powered by GitBook
On this page

Was this helpful?

  1. Scala 框架分阶段指导
  2. PA4:中间代码优化

死代码消除

Previous常量传播NextRust 框架分阶段指导

Last updated 5 years ago

Was this helpful?

死代码消除即无用代码消除,和不可达代码消除是两个概念。前者指的是消除执行之后没有任何作用的代码,后者指的是消除永远无法被执行到的代码。现在框架中有不可达代码消除的优化,是在构造流图和常量传播的时候进行的。

我们要求基于活跃变量分析进行死代码消除,不要求基于Faint Variables Analysis。理论上来说Faint Variables Analysis的效果应该是一定不会更差的,但是实现也一定会更复杂,不建议大家做它(做了也不会有额外加分)。

本来这一节应该是这几种优化中最简单的一个的(从结果代码行数可以直接看出),不过因为这是大家的实验任务,所以这里还是讲解的详细一些。首先来推导活跃变量分析的几个集合是怎么来的,这个推导对于到达-定值分析也基本上适用,可以帮助大家理解那几个集合的意义。

活跃变量分析为每个程序点计算出所有变量的集合的子集,用于表示该点处活跃的变量,所以数据流分析的值集为所有变量的集合的幂集。"活跃"的含义是在程序执行过这一点之后,这个变量当前的值会被使用到,所以数据流分析是后向的。对于单个语句SSS,传递函数要根据SSS之后活跃的变量计算SSS之前活跃的变量,计算方法为:所有SSS用到的变量在SSS之前都是活跃的,所有SSS之后活跃的变量,如果没有在SSS中被定值,证明未来的那次使用用的还是SSS之前的值,所以也是活跃的。

综合得,传递函数定义为:fS(x)=(x−defS)∪useSf_S(x) = (x - def_S) \cup use_SfS​(x)=(x−defS​)∪useS​。其中defSdef_SdefS​是SSS中定值的所有变量的集合,useSuse_SuseS​是SSS中使用的所有变量的集合。

基本块BBB的传递函数定义为:

fB(x)=fS1(...fSn−1(fSn(x)))=(((((x−defSn)∪useSn)−defSn−1)∪useSn−1...)−defS1)∪useS1=数学归纳法(x−⋃i=1ndefSi)∪⋃i=1n(useSi−⋃j=1i−1defSj)f_B(x) = f_{S_1}(...f_{S_{n - 1}}(f_{S_n}(x))) \\ = (((((x - def_{S_n}) \cup use_{S_n}) - def_{S_{n - 1}}) \cup use_{S_{n - 1}} ...) - def_{S_1}) \cup use_{S_1}\\ \overset{\mathrm{数学归纳法}}{=} (x - \bigcup_{i = 1}^n def_{S_i}) \cup \bigcup_{i = 1}^n (use_{S_i} - \bigcup_{j = 1}^{i - 1} def_{S_j})fB​(x)=fS1​​(...fSn−1​​(fSn​​(x)))=(((((x−defSn​​)∪useSn​​)−defSn−1​​)∪useSn−1​​...)−defS1​​)∪useS1​​=数学归纳法(x−i=1⋃n​defSi​​)∪i=1⋃n​(useSi​​−j=1⋃i−1​defSj​​)

最后一个等号使用数学归纳法来证明,读者自证不难。

定义defB=⋃i=1ndefSidef_B = \bigcup_{i = 1}^n def_{S_i}defB​=⋃i=1n​defSi​​,useB=⋃i=1n(useSi−⋃j=1i−1defSj)use_B = \bigcup_{i = 1}^n (use_{S_i} - \bigcup_{j = 1}^{i - 1} def_{S_j})useB​=⋃i=1n​(useSi​​−⋃j=1i−1​defSj​​),这样上面的式子就是大家熟悉的形式了。那么这个形式和课堂上定义的LiveUseLiveUseLiveUse和DefDefDef是一致的吗?

  1. 对于方程的求解,Flow已经提供了求解的工具。它是基于前向数据流的。根据之前的讲解大家应该知道怎么利用它来求解后向数据流。如果你觉得现有的的代码很难理解,也完全可以自己手动实现数据流方程的求解,我们并不强制使用Flow

    • 所谓的副作用,其实就是除了"改变赋值目标变量"之外,其他所有的作用。显然,tac中没有既没有副作用,同时也没有赋值目标的语句

其实还有一些语句的"副作用"不是很明确,比如load指令a = *(b + imm)和一部分计算指令,如除0,有符号整数溢出等(依平台而定),可能会导致程序崩溃,但是优化的时候可以不把这当成是副作用:按照c/c++常用的说法这叫未定义行为,这可以减轻编译器作者的负担,编译器可以假定程序永远没有未定义行为,并以此为依据来优化。

目前decaf并没有这方面的严格的语言标准,为了让大家的实现尽量简单,我们姑且认为不合法的访存和运算都是未定义行为,所以这样的指令是可以优化掉的。

先看useBuse_BuseB​和LiveUseLiveUseLiveUse,LiveUseLiveUseLiveUse为BBB中定值之前被引用的变量的集合,而useBuse_BuseB​定义中每一个求并项都是一条语句的useuseuse集合减去在这条语句前面的所有defdefdef集,也就是说如果一个变量在某条语句中被使用了,而且没有在这条语句之前的任何一条语句被定值,那么它属于useBuse_BuseB​,此外都不属于。显然,这与LiveUseLiveUseLiveUse的定义是符合的。

再看defBdef_BdefB​和DefDefDef,其实很容易可以看出这两个集合并不相同,defBdef_BdefB​包含了被定值的所有变量,而DefDefDef要求定值之前没有引用过,所以Def⊆defBDef \subseteq def_BDef⊆defB​。然而这个区别不会影响任何计算结果:如果变量xxx满足x∈defB,x∉Defx \in def_B, x \notin Defx∈defB​,x∈/Def,则意味着它定值之前被引用过,则x∈useB,x∈LiveUsex \in use_B, x \in LiveUsex∈useB​,x∈LiveUse,则它一定在这一步的结果集合中。

所以,DefDefDef这样的定义是冗余的,只会加大计算DefDefDef时的计算量,使用defBdef_BdefB​的定义就会简单一些,而且不会影响最终结果。

在实验框架中为了实现死代码消除,大致流程是首先计算每个defBdef_BdefB​和useBuse_BuseB​,以此解出每个inBin_BinB​和outBout_BoutB​,然后利用它们来计算每个outSout_SoutS​,边计算outSout_SoutS​边执行死代码删除。具体每一步怎样做,可以参考其它几个数据流分析的实现。这里讲一下几个常见的需要注意的点:

计算defBdef_BdefB​和useBuse_BuseB​的时候不用像定义式一样算,而是(反向)遍历一遍基本块中的每个语句并同时维护这两个集合。计算outSout_SoutS​也是类似的。如果你阅读了其他几个数据流分析的实现代码,会发现它们也都是类似这样做的

进行死代码删除的时候,如果一条语句没有副作用,而且它的赋值目标(如果有的话)不在outSout_SoutS​中,那么这条语句就可以删去

你在实现的时候可以认为除了a = call b之外的所有有赋值目标的语句都是没有副作用的,对于a = call b,如果a∉outSa \notin out_Sa∈/outS​,要求将它优化为call b