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. Rust 框架分阶段指导
  2. PA4:中间代码优化

常量传播

Previous复写传播Next死代码消除

Last updated 5 years ago

Was this helpful?

原理

常量传播的目的在于发掘代码中可能存在的常量,尽量用对常量的引用替代对虚拟寄存器的引用(虚拟寄存器和变量是同一个概念,以下都使用变量),并尽量计算出可以计算的常量表达式。

为了实现常量传播,类比于前两种数据流,不难想象常量传播的数据流是"可用常量分析",找出在一条语句处有哪些对变量赋的常量值可以到达。每一条涉及到赋值的语句处,杀死之前与左端项相关的赋值,如果右端项为变量则生成对左端项的变量赋值;反之如果对左端项赋予了一个常量值或者常量表达式,则生成对左端项的常量赋值。

形式化地,常量传播的值集是R→VR \rightarrow VR→V的函数的集合。其中RRR是变量,VVV是集合变量的所有可能取值∪{NAC,UNDEF}变量的所有可能取值 \cup \{NAC, UNDEF\}变量的所有可能取值∪{NAC,UNDEF},它用来表示变量在一个程序点处的取值。其中NACNACNAC表示已知它的值不是一个常量;UNDEFUNDEFUNDEF表示不知道它的取值,出现UNDEFUNDEFUNDEF是因为这个程序点处这个变量尚未赋值。

如果你觉得函数太抽象了,可以认为值集就是一个元素类型为VVV的数组,用变量的id为下标访问,实际上也是这么实现的。

这个数据流和其他数据流有一个很大的区别:它的值集的大小是无限的。不过这并不影响数据流分析算法的收敛性,感兴趣的同学可以自己查阅相关的资料。

单条语句的传递函数fSf_SfS​定义为:设m′=fS(m)m' = f_S(m)m′=fS​(m)(注意mmm和m′m'm′都属于值集,所以它们都是函数),对于语句SSS:

  • 如果SSS没有给任何变量赋值,则m′=mm' = mm′=m

  • 否则,设SSS对变量xxx赋值了(假设一条语句最多只能给一个变量赋值),则m′(y)=m(y),∀y∈R,y≠xm'(y) = m(y), \forall y \in R, y \ne xm′(y)=m(y),∀y∈R,y=x,此外:

    • 如果赋值的右端项是常数ccc,则m′(x)=cm'(x) = cm′(x)=c

    • 如果赋值的右端项是表达式y⊕zy \oplus zy⊕z(⊕\oplus⊕是任意一个运算符,也不仅局限于两个运算数的情况,单目运算和复写语句都可以归于此类,规则是类似的)则:

      • 如果m(y)m(y)m(y)和m(z)m(z)m(z)都是常数,则m′(x)=m(y)⊕m(z)m'(x) = m(y) \oplus m(z)m′(x)=m(y)⊕m(z)

      • 如果m(y)m(y)m(y)和m(z)m(z)m(z)中有一个是NACNACNAC,则m′(x)=NACm'(x) = NACm′(x)=NAC

      • 否则,m′(x)=UNDEFm'(x) = UNDEFm′(x)=UNDEF

    • 否则,m′(x)=NACm'(x) = NACm′(x)=NAC,这里可能包括赋值的右端项是函数的返回值,是访存的结果之类的,在简单的优化器中都直接认为这些结果不是常数

当两个基本块交汇的时候,对于每个变量,需要考虑以下几种情况:

  • 如果两个基本块末尾处它都是常量,且两个常量值相等,则交汇结果为这个常量

    • 一旦出现了这样的情形,可能意味着后续程序中有使用未赋初值就使用一个变量的行为,优化器可以依据"未赋初值的变量可以有任何取值",认为这个取值就是另一个基本块的常量值,从而就回到了前一种情形

这个交汇操作用格图表示如下:

框架中的实现

实际的编译器中,对于这样的情形往往会汇报一个警告。你可能会发现,有些情况下编译的优化等级开高了之后会产生一些新的警告。这也许就是编译器在高等级的优化下,进行各种数据流分析的时候发现了程序的漏洞。

完成了数据流分析之后,对于每一条语句,可以得到这条语句处所有变量的取值,把这条语句用到的所有值是常数的变量都换成对应的常数。此外,如果基本块结尾处的条件跳转的条件也是常数,就可以把这个条件跳转替换成无条件跳转,这可能导致一些原先认为可达的基本块变成不可达的,所以可以调用simplify函数来删除不可达的基本块。

  if (b) a = VTBL<C>
  else a = 100
  // use a

根据上面描述的交汇规则,下面使用a的地方会认为a是常量100。出现这个结果的直接原因是这样的tac本来就不能算作合法,VTBL<C>和100本来就不应该以同样的方式被使用,既然是不合法的程序,优化后语义发生变化也是很正常的;更根本的原因是tac中没有类型信息,导致这样的不合法的tac看起来还挺合法的。

你可能觉得VTBL<C>是地址,地址和整数混用没有什么问题,然而这只是在机器码层面没有什么问题,任何设计良好的ir中,地址和整数一定是不能混用的,它们之间的相互转换一定是有对应的指令的,哪怕这条指令最终并不生成任何机器码,在分析过程中它也是必要的。

如果一个基本块末尾处它是常量,另一个基本块结尾处它是UNDEFUNDEFUNDEF,则交汇结果为这个常量

如果两个基本块末尾处它都是UNDEFUNDEFUNDEF,则交汇结果为UNDEFUNDEFUNDEF

否则,交汇结果为NACNACNAC

数据流分析过程中,执行运算y⊕zy \oplus zy⊕z时会有一个corner case,即除/模0的情况。为了简单起见,这样的运算结果我们直接认为是0,这其实就是编译器对于未定义行为的一种处理策略。关于未定义行为,死代码消除一章也有对应的描述。

框架中,初值为对除了传入参数之外的所有变量都赋值UNDEFUNDEFUNDEF,这表示目前还不知道任何关于它的信息;对于表示传入参数的变量赋值NACNACNAC,因为参数一定不是常数(我注意到很多相关资料中都没有提到这一点,但是这一点对于编写实际的优化程序来说是不能忽略的)。

简单起见,实验框架只实现了对整数的常量传播,而像字符串,虚表这些地址则没有实现,统一用UNDEFUNDEFUNDEF来表示。这也就意味着,假设出现这样的代码:

int_lattice