基本块

在数据流分析和优化之前需要先将tac划分成基本块:大家应该是默认地接受了这个设定,其实这不是必要的。之后提到的所有的数据流分析和优化的算法都可以直接在tac语句级别进行,对于每条语句都可以计算出相应的集合,并且在语句间进行传播。将tac划分为基本块可以看做是一种加速手段:每个基本块从进入到退出,对于数据流信息的处理是固定的,而因为这些分析和优化方法都是迭代的算法,划分为基本块就可以避免每个迭代步重复计算了。

很多参考文献中为了简化算法的描述,会假定每个基本块都由单条语句组成。从这也可以看出划分基本块是不必要的,不过在实际的编译器中为了编译速度考虑,一般还是要进行划分基本块的操作。

原理课中对于基本块的定义如下:

  • 程序中一个顺序执行的语句序列

  • 只有一个入口语句和一个出口语句

  • 除入口语句外其他语句均不可以带标号

  • 除出口语句外其他语句均不能是转移或停止语句

也就是说,除非执行过程中发生意料之外的错误,一个基本块总能够从开头执行到结尾,且不可能从除开头外的任何一个中间点开始执行。

基本块的划分方法是先标记"首语句",也就是可以成为基本块的第一条语句的语句。它可以是:

  • 程序的第一条语句

  • 转移语句的目标语句

  • 紧跟在条件转移语句后面的语句

容易证明基本块就是被首语句划分的一系列语句块。因此一个直白的划分基本块的方法是先标记所有首语句,然后遍历一遍语句,将两条首语句之间的语句划分给一个基本块。不过实现的时候没有必要分成这样两趟的过程,只需要一趟遍历就可以划分出基本块。具体可以参考框架已经提供好的代码。

事实上为了获取基本块的划分,不一定要像现在的实验框架和课堂中讲的一样对线性的ir进行划分,也可以直接从ast构造出cfg,只是会让这个翻译阶段(pa3)麻烦一些而已。也许明年的框架中就没有划分基本块这一步了。

基本块之间的连接关系由基本块的最后一条语句决定:

  • 返回/停止:没有出边

  • 无条件转移:一条出边指向转移的标号所在的基本块

  • 条件转移:一条出边指向转移的标号所在的基本块,一条出边指向下一个基本块

实现的时候,以下几个点需要注意:

  1. 这个"最后一条语句"没有被包含在基本块的tac链表中,它的其它信息(例如返回值,条件)保存到了表示出边的enum中

  2. 在遇到tac中的条件转移语句时,不是直接把它对应到条件转移,而是判断条件是否是一个常量,如果是则把它转化成无条件转移

  3. tac语句中没有"停止指令",但是有对功能为停止的intrinsic函数的调用,对它按照停止语句处理

容易看出,因为有第二条的"优化"的存在(其实主要目的不是优化,而是让本阶段的后续实现更简单),如果在本阶段通过图的可达性来检查一个函数是否不返回就结束,可以对pa2中提到的循环条件为常量true且没有作用于它的break的循环做出正确的判断。事实上,在之前的某个版本中,我基本实现了java规范要求的这类错误的检测,但是结果只能在划分基本块这个阶段才能给出,而且需要依赖pa3中进行一定程度的常量折叠,但是其它的框架都希望能在pa2这个阶段就检查出这类错误,所以我也只能简化成现在这个样子了。

按照现在的实现,可以保证pa4中处理的所有函数都满足不会不返回就结束(不仅限于返回值非空的函数,而是所有函数),tacopt::bb::simplify函数依赖于这个条件,如果违反的话可能会出现下标越界。这样的设计是完全合适的,按照rust的错误处理的理念,这样的不可恢复,而且是因为输入没有满足约定而导致的错误,就应该panic

Last updated