11.4 编译器优化技术 11.4.1 优化技术概览
11.4 编译器优化技术
经过前面对即时编译、提前编译的讲解,读者应该已经建立起一个认知:编译器的目标虽然是做由程序代码翻译为本地机器码的工作,但其实难点并不在于能不能成功翻译出机器码,输出代码优化质量的高低才是决定编译器优秀与否的关键。在本章之前的内容里出现过许多优化措施的专业名词, 有一些是编译原理中的基础知识,譬如方法内联,只要是计算机专业毕业的读者至少都有初步的概念;但也有一些专业性比较强的名词,譬如逃逸分析,可能不少读者只听名字很难想象出来这个优化会做什么事情。本节将介绍几种HotSpot虚拟机的即时编译器在生成代码时采用的代码优化技术,以小见大,见微知著,让读者对编译器代码优化有整体理解。
11.4.1 优化技术概览
OpenJDK的官方Wiki上,HotSpot虚拟机设计团队列出了一个相对比较全面的、即时编译器中采用的优化技术列表^1,如表11-1所示,其中有不少经典编译器的优化手段,也有许多针对Java语言,或者说针对运行在Java虚拟机上的所有语言进行的优化。本节先对这些技术进行概览,在后面几节中, 将挑选若干最重要或最典型的优化,与读者一起看看优化前后的代码发生了怎样的变化。
上述的优化技术看起来很多,而且名字看起来大多显得有点“高深莫测”,实际上要实现这些优化确实有不小的难度,但大部分优化技术理解起来都并不困难,为了消除读者对这些优化技术的陌生感,笔者举一个最简单的例子:通过大家熟悉的Java代码变化来展示其中几种优化技术是如何发挥作用的。不过首先需要明确一点,即时编译器对这些代码优化变换是建立在代码的中间表示或者是机器码之上的,绝不是直接在Java源码上去做的,这里只是笔者为了方便讲解,使用了Java语言的语法来表示这些优化技术所发挥的作用。
第一步,从原始代码开始,如代码清单11-6所示[^2]。
1 | static class B { |
代码清单11-6所示的内容已经非常简化了,但是仍有不少优化的空间。首先,第一个要进行的优化是方法内联,它的主要目的有两个:一是去除方法调用的成本(如查找方法版本、建立栈帧等); 二是为其他优化建立良好的基础。方法内联膨胀之后可以便于在更大范围上进行后续的优化手段,可以获取更好的优化效果。因此各种编译器一般都会把内联优化放在优化序列最靠前的位置。内联后的代码如代码清单11-7所示。
1 | public void foo() { |
第二步进行冗余访问消除(Redundant Loads Elimination),假设代码中间注释掉的“…do stuff…”所代表的操作不会改变b.value的值,那么就可以把“z=b.value”替换为“z=y”,因为上一句“y=b.value”已经保证了变量y与b.value是一致的,这样就可以不再去访问对象b的局部变量了。如果把b.value看作一个表达式,那么也可以把这项优化看作一种公共子表达式消除(Common Subexpression Elimination),优化后的代码如代码清单11-8所示。
1 | public void foo() { |
第三步进行复写传播(Copy Propagation),因为这段程序的逻辑之中没有必要使用一个额外的变量z,它与变量y是完全相等的,因此我们可以使用y来代替z。复写传播之后的程序如代码清单11-9所示。
1 | public void foo() { |
第四步进行无用代码消除(Dead Code Elimination),无用代码可能是永远不会被执行的代码,也可能是完全没有意义的代码。因此它又被很形象地称为“Dead Code”,在代码清单11-9中,“y=y”是没有意义的,把它消除后的程序如代码清单11-10所示。
1 | public void foo() { |
经过四次优化之后,代码清单11-10所示代码与代码清单11-6所示代码所达到的效果是一致的,但是前者比后者省略了许多语句,体现在字节码和机器码指令上的差距会更大,执行效率的差距也会更高。编译器的这些优化技术实现起来也许确实复杂,但是要理解它们的行为,对于一个初学者来说都是没有什么困难的,完全不需要有任何的恐惧心理。
接下来,笔者挑选了四项有代表性的优化技术,与大家一起观察它们是如何运作的。它们分别是:
- 最重要的优化技术之一:方法内联。
- 最前沿的优化技术之一:逃逸分析。
- 语言无关的经典优化技术之一:公共子表达式消除。
- 语言相关的经典优化技术之一:数组边界检查消除。
[^2]: 本示例原型来自Oracle官方对编译器技术的介绍材料: http://download.oracle.com/docs/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/underst_jit.html。