编译器优化–4--消除无用和不可达代码
概述
有时候,程序包含的一些计算不具有外部可见的效应。如果编译器能够确定给定操作不会影响程序的结果,那么它完全可以消除该操作。大多数程序员都不会有意编写这种代码。但是,这种代码在大多数程序中作为编译器中优化的直接结果出现,通常是因宏展开或编译器前端的"朴素"转换所致。
有两种不同的效应可以使操作成为消除的合格对象。操作可能是无用的
,意即其结果没有外部可见的效应。另外,操作可能是不可达的
,意即它不可能执行。如果一个操作属于上述两类之一,那么它就可被移除。术语"死代码"通常可用于指代无用代码或不可达代码,但我们使用"死代码"来指无用代码。
Note:
删除无用或不可达代码可以缩减IR代码,可使程序更小、编译更快、(通常)执行也更快。同事它还可以增强编译器改进代码的能力。例如,不可达代码的效果可能呈现在静态分析结果中,从而阻碍应用某些变换。在这种情况下,消除不可达程序块可以改变分析结果,从而能够进一步应用变换。
某些形式的冗余消除也会删除无用代码。例如,局部值编号算法会应用代数恒等式来简化代码。这样的例子包括
x
+
0
→
x
x + 0 \rightarrow x
x+0→x,
y
∗
1
→
y
y * 1 \rightarrow y
y∗1→y和
m
a
x
(
z
,
z
)
→
z
max(z,z) \rightarrow z
max(z,z)→z。对这些例子的每个简化都会消除一个无用操作,即根据定义,在删除后不会对程序的外部可见行为造成影响的操作。
因为消除无用和不可达代码
的算法会修改程序的控制流图(CFG),我们将审慎地区分术语分支(指令br)
和跳转(jump)
。
消除无用代码
用于消除无用代码的经典算法,其运作方式类似于标记——清除垃圾收集器
,只是输入是IR代码而已。类似于标记——清除收集器
这类算法会在代码上处理两趟。第一趟清除所有的标记字段,并将"关键"操作均标记为"有用的"。如果一个操作会设置过程的返回值,是输入输出语句,或者会影响从当前过程外部可访问的某个内存的位置中的值,则我们称该操作为"关键的"。关键操作的例子包括过程的其实代码序列和收尾代码序列,以及调用位置处的调用前代码序列和返回后代码序列。接下来,算法将跟踪"有用"操作的操作数,回溯到其定义位置,将想用的操作标记为"有用"。在简单的Worklist迭代模式下,这个过程会一直持续下去,直至无法将更多操作标记为有用为止。第二趟将遍历代码并删除任何没有标记为有用的操作。
Note:
- 一个操作可能有几种方法设置一个返回值,包括对引用调用参数或全局变量赋值,通过具有歧义的指针赋值,或通过return语句传递一个返回值。
消除无用控制流
优化可能会改变程序的IR形式,使之包含无用的控制流。如果编译器包括可能产生无用控制流(作为一种副效应)的优化,那么其中也应该包括一趟对应的处理,即通过消除无用控制流来简化CFG。
消除无用控制流的算法(Clean)非常简单,该算法直接运行在过程的CFG上。它使用4个变换,它们按以下顺序应用
。
-
合并冗余分支指令(Merge redundant branch instruction)
,如果Clean发现一个程序块以分支指令结束,但该分支指令的两个目标是同一个程序块,则将其替换为到目标程序块的跳转指令。之所以会出现这种情况,是因为其他简化所致。例如
B
i
B_i
Bi可能有两个后继节点,而每个后继节点结束时都跳转到
B
j
B_j
Bj。
-
删除空程序块(Delete empty basicblocks)
,如果Clean发现一个程序块只包含一条跳转指令,则可以将改程序块合并到其后继节点。当有其他趟从程序
B
i
B_i
Bi中删除了所有操作时,就会出现这种情况。如下图中两个CFG途中的左侧的那个。由于
B
i
B_i
Bi只有一个后继节点
B
j
B_j
Bj,变换将进入
B
i
B_i
Bi的边重定目标到
B
j
B_j
Bj,并将
B
i
B_i
Bi从
B
j
B_j
Bj的前驱节点集合中删除。这简化了CFG。也应该能够加速代码的执行。在原来的图中,穿越
B
i
B_i
Bi的代码路径需要两个控制流操作才能到达
B
j
B_j
Bj。在变换后的图中,相应的代码路径只需一个操作即可到达
B
j
B_j
Bj。
-
合并程序块(Merge basicblock)
,如果Clean发现一个程序块
B
i
B_i
Bi结束于到
B
j
B_j
Bj的跳转指令,且
B
j
B_j
Bj只有一个前驱节点,那么就可以合并这两个程序块,如下图所示。这种情况可能有几种原因。可能有另一个变换消除了进入
B
j
B_j
Bj的其他边,当然
B
i
B_i
Bi和
B
j
B_j
Bj也可能是合并冗余分支指令
的结果。但无论是哪种情况,这两个程序块都可以合并为一个程序块。这样就消除了
B
i
B_i
Bi末尾的跳转指令。
-
提升分支指令(Promotion branch instruction)
,如果Clean发现程序块
B
j
B_j
Bj是空程序块,且
B
j
B_j
Bj以分支指令结束,那么Clean可以将
B
i
B_i
Bi程序块末尾的跳转指令替换为
B
j
B_j
Bj中分支指令的副本。实际上,这相当于将分支指令提升到
B
i
B_i
Bi中,如下图所示。当有其他趟处理删除了
B
j
B_j
Bj中的其他操作,使得
B
j
B_j
Bj中的跳转指令直接跳转到一个分支指令时,就会出现这种情况。变换后的代码只有一个分支指令就达到了同样的效果。这向CFG中添加了一条边。请注意,
B
i
B_i
Bi不可能是空的,否则负责消除空程序块的趟已经删除了它。(提升之后,
B
j
B_j
Bj任然至少有一个前驱节点。)
为了实现这些变换优化器需要进行一些工作。上述提到的4种变换所作的某些修改颇为简单。在使用IR和CFG表示的程序中合并冗余分支指令
,Clean将程序块末尾的分支指令改写为跳转指令,并调整程序块的后继结点和前驱结点列表。其他修改则较为困难。合并两个基本块涉及为合并后的程序块分配空间,将各个操作复制到新程序块中,调整新程序块(以及在CFG中的相邻结点)的前驱结点和后继结点列表,并丢弃原来的两个程序块。
Clean以一种系统化的方式来应用这4种变换。它按后根次序遍历流图,因此
B
i
B_i
Bi的后继结点会在
B
i
B_i
Bi结点之前被简化,除非其后继结点在后根次序编号下位于某条后向边上。在这种情况下,Clean将先访问前驱结点,而后访问后继结点。在有环图中这是不可避免的。在前驱结点之前简化后继结点可以减少实现必须移动某些边的次数。
消除不可达代码
有时候CFG中包含了不可达的代码。编译器应该找到不可达程序块并删除它们。程序块可能因为以下两个原因成为不可达代码:
- 可能没有穿越CFG的代码路径到达该程序块
- 到达该程序块的代码路径是不可能执行的
如受控于一个条件判断的代码,如果条件表达式的值始终是false,那么该代码是不可能被执行的。
前一种情形易于处理。编译器可以在CFG上执行一种简单的标记——清除
类的可达性分析。首先,他在每个程序块上初始化一个标记,值为"不可达"。接下来,它从CFG的入口结点开始,将每个可以到达的CFG结点标记为"可达"。如果所有分支和跳转指令都是无歧义的,那么所有未标记为可达的程序块都可以删除。如果存在有歧义的分支或跳转指令,则编译器必须保留这种分支或跳转指令可能到达的那些程序块。这种分析简单且代价不高。可以在因其他目的遍历CFG期间完成此种分析,也可以在CFG构造期间完成。
Note:
如果源语言允许对代码指针或标号进行算术运算,则编译器必须保留所有程序块。否则,保留下来的程序块集合可以仅限于哪些标号被引用到的程序块。
第二种情况处理起来要困难些。它要求编译器推断控制分支指令的表达式的值。
参考:
《编译原理》
《Engineering a Compiler》