运行期 【笔记】深入理解 Java 虚拟机:晚期优化( 四 )


公共子表达式消除
公共子表达式消除是一个普遍应用于各种编译器的经典优化技术,它的原理是:如果一个表达式 E 已经计算过了,并且从先前计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次计算就称为公共子表达 。对于这种表达式,就没有必要再对其进行计算了,使用之前计算过的值即可 。下面举例说明其优化过程,对于下面的代码:
int d = (c * b) * 12 + a + (a + b * c)

运行期  【笔记】深入理解 Java 虚拟机:晚期优化

文章插图
当这段代码进入即时编译器后,编译器检测到 c * b 和 b * c 是一样的表达式,并且在计算期间 b、c 的值不变,因此这段表达式可以被视为:
int d = E * 12 + a + (a + E)
这时,编译器还能进行另一种简化:代数化简,把表达式简化为:
int d = E * 12 + 2 * a
表达式经过优化后,计算效率就提高了 。
数组边界检查消除
数组边界检查消除是即时编译器中语言相关的经典优化技术 。如果有一个数组 foo[],Java 语言在访问数组元素 foo[i] 时,系统会自动进行上下界的范围检查,即 i 的取值范围是 0 ~ foo.-1,否则将抛出运行时异常 java.lang. 。这对开发者来说是好事情,即时程序员没有专门编写防御代码,也可以避免大部分的溢出攻击 。但是,对于虚拟机的执行子系统来说,每次数组元素的读写操作都带有一次隐含的条件判定操作,对于拥有大量数组访问的系统,无疑是一种性能上的负担 。
编译器会对代码进行分析,如果确定某次数组访问一定不会越界,就可以去掉数组的上下界检查 。比如在循环访问数组时,编译器只要通过数据流分析确定循环变量的取值范围一定在 [0, foo.) 之间,就可以在整个循环中把数组上下界检查消除 。
与数组边界检查消除类似的优化,还有隐式异常处理,Java 中空指针检查和除数为零检查都采用了这种思路 。举个例子,Java 中访问一个对象 foo 的 value 属性的代码如下:
if (foo != null) {return foo.value;} else {throw new NullPointerException();}
在使用隐式异常优化后,伪代码如下:
try {return foo.value;} catch (segment_fault) {uncommon_trap();}
虚拟机会注册一个falut 信号的异常处理器(伪代码中的 ()),这样当 foo 不为空时,对 value 的访问不会额外消耗一次空值检查的开销 。代价是,当 foo 为空时,必须转入到异常处理器中恢复并抛出异常,这个过程必须从用户态转到内核态处理,结束后再回到用户态,速度远比一次判空检查慢 。当 foo 极少为空的时候,隐式异常优化是值得的,虚拟机会根据运行期收集到的信息自动选择最优方案 。
方法内联
方法内联是编译器最重要的优化手段之一,除了消除方法调用成本之外,它更重要的意义是为其他优化手段建立良好的基础 。比如下面的代码,事实上里都是无用代码,如果不做内联,是无法发现任何 dead code 的,因为分开来看,foo 和两个方法里的操作都可能是有意义的 。
public static void foo(Object obj) {if (obj != null) {System.out.println("hello");}}public static void testInline() {Object obj = null;foo(obj);}
方法内联看起来简单,但实际中很多方法都无法直接进行内联 。原因是除了使用指令调用的私有方法、实例构造器、父类方法以及使用指令调用的静态方法,还有部分 final 方法能够在编译时唯一确定执行的方法版本,其他都可能存在多于一个版本的方法接收者,需要在运行时才能确定,这一类方法称为非虚方法 。