TT Lab
开始
学习 学习路径 课程

编译器 — 从头到尾亲手打造一门小语言

结果相同、代价更低的程序 — CFG 与 SSA

在 TT Lab 中继续学习

一句话总结

优化就是把程序换成结果相同、但开销更低的程序。在树上,做法是提前计算常量、清除已经确定的分支;在指令列表上,则是建立基本块和控制流图(CFG),删除没有路径可达的代码。在这张图上求出支配节点之后,就能进一步算出实用编译器的中间表示 SSA 把 φ 放在哪里。

为什么需要它

人写代码是为了好读。会写 60 * 60 * 24 而不是 86400,会留下调试用的 if (false) { … },有时还会在 return 后面留几行。第 6 个模块的编译器会把这些原样翻译,所以循环里的 60 * 60 * 24,每转一圈就是两次乘法。如果编译器能替人提前算好,人就既能写出好读的代码,又能得到快的程序。

不过,优化有一条不能越过的线。结果不能改变。不能因为想把 7 / (3 - 3) 当作“是常量,所以提前计算”,就让编译器崩溃,或者把错误消除掉——这一行在运行时必须就地报出除以 0 的错误。好的优化器,先知道的是不能做什么,其次才是能做什么。

工作原理

常量折叠。从下往上遍历树,把两边都是字面量的运算算出来,换成一个字面量。64 位回绕和向 0 截断的除法,要与解释器完全一致;而执行时会出错的表达式(除以 0、负指数、类型不匹配的运算),则不折叠,保留原样。折叠出的字面量继承原运算符的位置。false && f() 的右边不会被执行,所以整个就是 false,而 true && e 就是 e。if (true) 变成它的那个分支(保持块的原样——为了让作用域保留),while (false) 则整个消失。

基本块与 CFG。在指令列表里,把第一条指令、跳转的目的地,以及跳转和 RETURN 的正后方标为“首指令(leader)”,从一个首指令到下一个首指令之前,就是一个基本块。进入块内部只能经过第一条指令,离开只能经过最后一条指令。给每个块记下它的后继块(顺序落入的一边、跳转的一边),就是 CFG。从入口出发沿着 CFG 走不到的块,可以删掉——本课程的编译器会在每个函数末尾附上 CONST 0 · RETURN,所以每个以 return 结尾的函数里,这两条指令都会作为不可达代码留下来。删掉之后,剩下的跳转的偏移量必须按新的位置重新计算。

fn pos(v) { if (v < 0) { return 0; } return v; }

 B0  0 GET_LOCAL 0         B0 → B1(흘러내림), B2(점프)
     1 CONST 0   ; 0
     2 LT
     3 JUMP_IF_FALSE 2 ; → 6
 B1  4 CONST 0   ; 0       B1 → 없음(RETURN)
     5 RETURN
 B2  6 GET_LOCAL 0         B2 → 없음
     7 RETURN
 B3  8 CONST 0   ; 0       ← 입구에서 닿지 않는다(컴파일러가 붙인 꼬리): 지운다
     9 RETURN

支配节点与 SSA。块 d 支配块 b,意思是从入口到 b 的所有路径都经过 d。所有块的支配节点集合,通过反复应用“b 的支配节点 = {b} ∪(前驱块们的支配节点的交集)”,直到不再变化为止来求出。为什么需要它——实用编译器(LLVM、GCC)的中间表示是 SSA,也就是每个变量只被赋值一次的形式。把 x = 1; if (c) x = 2; print x; 变成 SSA,会得到 x1 = 1、x2 = 2,以及在两条路径汇合的地方的 x3 = φ(x1, x2)。φ 的意思是“根据是从哪条路来的来选择”。

      B0: x1 = 1; if c
       /          \
  B1: x2 = 2       |
       \          /
      B2: x3 = φ(x1, x2); print x3        ← B2 는 B1 의 지배 경계

φ 放在哪里,由支配边界(dominance frontier)告诉我们。块 b 的支配边界,是从 b 所支配的区域向外走一步,第一次支配中断的那些块——也就是从 b 来的值与从别的路来的值第一次相遇的地方。在变量被赋值的那些块的支配边界上放置 φ,并且把新放置的 φ 也当作赋值,一直扩展到不再增加为止(迭代支配边界)。

在现场相遇的样子

下一项实验要做什么

在 opt.py 中做出表达式折叠、程序折叠(清除已确定的分支)、基本块、CFG、删除不可达块(重新计算跳转)、支配节点与直接支配节点、支配边界与 φ 的位置,以及折叠 → 编译 → 删除的流水线。评分器会把树、chunk 和图与参考对照,并用 VM 确认优化后的程序在得出同样结果的同时,执行了更少的指令。