退优化Deoptimization

退优化Deoptimization

退优化Deoptimization #

这是一个 C 语言编译器没有办法优化,但是 JIT 编译却能进一步优化的例子。在这个例子中,你会发现常量传播不再起作用了:

public static int test(boolean flag) {
    int b = 0;
    if (flag) {
        b = 3;
    }
    else {
        b = 2;
    }
    return b + 4;
}

和第一个例子相比较,这个例子增加了第 3 行到第 8 行的条件判断。所以编译器无法知道在第 9 行 b 的真实取值是什么。只能严格按照这个函数的逻辑去生成比较,跳转,赋值等等,那么这个例子就比可以常量折叠那个例子复杂多了。

一般情况下,虽然这个例子中的 test 函数没有优化空间了,但是 JVM 的 JIT 技术还是在这里找到了最优化的机会。假如存在一种情况,每一次 test 方法被调用的时候,传的参数 flag 都是 true 或者都是 false,也就是说,flag 的取值固定。那么 JIT 编译器就可以认为另外一个分支是不存在的,可以不编译。

JIT 编译器在开始之前,test 方法是由解释器执行的。解释器一边执行,一边会统计 flag 的取值,这种统计就叫做性能采样(Profiling)。当 JIT 编译器发现,test 方法被调用了 500 次(这个阈值可以以 JVM 参数指定),每一次 flag 的值都是 true,那它就可以合理地猜测,下一次可能还是 true,它就会把 test 方法优化成这个样子:

public static int test(boolean flag) {
    return 7;
}

但是,这种做法,相信你也都看出问题来了,如果恰好 test 方法的下一次调用就是 false 呢?所以 JVM 必须在 test 方法里留一个哨兵,当参数 flag 的值为 false 的时候,可以再退回到解释器执行。这个过程就是退优化

(Deoptimization)。这个过程相当于,JVM 的 JIT 编译器生成的机器码等效于以下代码:

public static int test(boolean flag) {
    if (!flag)
        deoptimize()
    return 7;
}

在这个代码中,deoptimize 方法是 JVM 提供的内建方法,它的作用是由 JIT 编译器退回到解释器进行执行。这个过程涉及栈帧的运行时切换,无疑是非常精巧和复杂的,但我们做为 Java 语言的使用者,并不需要完全理解 JIT 背后的每一个技术细节。但通过这个例子,我们可以掌握了如何写程序,才能让 JIT 编译器帮我们生成最高效的机器码。让 JIT 编译器运行得好,我们只需要遵守一条原则:让程序行为可预测。因为 JIT 编译优化的基本假设是过去和未来,程序的运行规律基本一致,所以它基于过去的行为测试未来。如果它预测的未来和真实情况不一致,就会发生退优化。退优化的情况会对性能带来巨大的伤害,所以 JIT 有时也可能是一把双刃剑。

Viewpoint #

From #

11 | 即时编译:高性能JVM的核心秘密