编译器(或JVM)是否会优化这段代码?我该如何检查?

3
假设我有这个类:
Util
{
    public static void doSomething()
    {
         if (FLAG) foo();
         else bar();
    }

    public static void foo() { /* do something */ }
    public static void bar() { /* do something else */ }

    public static final boolean FLAG = computeFlag();
    private static boolean computeFlag() { /* do some computation during init time*/ }
}
FLAG 显然永远不会改变。 假设 Util.doSomething() 被频繁使用(并且在许多关键位置上,性能确实很重要)。 Java 编译器或 JVM 是否足够聪明,可以缓存 doSomething 的代码体,以便代码无需重新评估 FLAG 或执行分支指令。

我应该如何检查这一点?

谢谢


编译器,不行。JVM,可能可以。 - undefined
2
CPU的分支预测器会因为一个新的、令人惊叹的价值永远不变而被撕裂。 - undefined
抱歉,我弄错了,我不是指编译器,我是指JVM,没错。 - undefined
我确实做出了改变。(如果有些编译器足够聪明,可以静态评估computeFlag的主体,并且在编译时知道值是已知的,因此FLAG的值也在编译时已知... :D) - undefined
在运行时确定,而不是编译时确定。如果computeFlag理论上可能受到在运行时确定的任何因素的影响,编译器无法在编译时确定其值。如果不是这样的话,你应该直接找出并赋值。 - undefined
2个回答

5

这可能取决于您使用的JVM。对于Oracle Hotspot JVM,您可以使用生成的机器代码进行检查。

java -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly your.MainClass

只要您在库路径中拥有必要的本地库(文档详细说明了可以获取这些二进制文件的位置),就可以运行该类。

运行该类:

public class DecompileTest {

    public static void doSomething() {
        if (FLAG)
            foo();
        else
            bar();
    }

    static int fooCount;

    public static void foo() {
        fooCount++;
    }

    public static void bar() {
        fooCount--;
    }

    public static final boolean FLAG = computeFlag();

    private static boolean computeFlag() {
        System.out.println("Shall I set the flag? (y/n)");
        try {
            return System.in.read() == 'y';
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            doSomething();
        }
        System.out.println(fooCount);
    }   
}

使用技巧
java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
Java HotSpot(TM) Server VM (build 23.21-b01, mixed mode)

在我的英特尔处理器上,会生成一段冗长的反汇编代码,其中相关部分如下所示:

Decoding compiled method 0x009ca408:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} 'doSomething' '()V' in 'stackoverflow/DecompileTest'
  #           [sp+0x10]  (sp of caller)
  0x009ca500: sub    $0xc,%esp
  0x009ca506: mov    %ebp,0x8(%esp)     ;*synchronization entry
                                        ; - stackoverflow.DecompileTest::doSomething@-1 (line 8)
  0x009ca50a: mov    $0x295dc208,%ebx   ;   {oop(a 'java/lang/Class' = 'stackoverflow/DecompileTest')}
  0x009ca50f: incl   0x70(%ebx)         ;*putstatic fooCount
                                        ; - stackoverflow.DecompileTest::foo@5 (line 17)
                                        ; - stackoverflow.DecompileTest::doSomething@6 (line 9)
  0x009ca512: add    $0x8,%esp
  0x009ca515: pop    %ebp
  0x009ca516: test   %eax,0x950000      ;   {poll_return}
  0x009ca51c: ret    
  0x009ca51d: hlt    
  0x009ca51e: hlt    
  0x009ca51f: hlt    
[Exception Handler]
[Stub Code]
  0x009ca520: jmp    0x009c78c0         ;   {no_reloc}
[Deopt Handler Code]
  0x009ca525: push   $0x9ca525          ;   {section_word}
  0x009ca52a: jmp    0x009ae280         ;   {runtime_call}
  0x009ca52f: hlt    

也就是说,FLAG的测试和bar()的调用都被视为死代码并被消除,而foo方法体已经被内联。

+1(如果允许我的话,我会给你+100分!)感谢关于如何获取反汇编文档的指南。 :) - undefined

1

是的,这种代码很容易成为HotSpot死代码消除的目标。即使FLAG可能会改变,HotSpot也会通过分析确定在实际执行中它始终是相同的,并消除未执行的分支。

参考: 性能策略索引


是的,JVM可以将这样的标志内联到代码中。实际上,即使对于非final标志,它也会这样做。 http://vanillajava.blogspot.co.uk/2012/01/demonstrating-when-volatile-is-required.html - undefined
即使 FLAG 可能会改变,HotSpot 仍然会消除未执行的分支: 我并不完全同意。作为一种推测性优化,生成的机器代码必须至少测试一次条件,以便在需要时进行反优化,因此生成的机器代码仍然包含分支指令。实际上,如果我移除 final 并运行我答案中的测试代码,将会对 FLAG 进行测试,并在值为 false 时进行分支。事实上,即使 FLAG 不是 final,对 bar() 的调用仍然是生成的机器代码的一部分。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接