JIT无法优化涉及Integer.MAX_VALUE的循环

49

在回答另一个问题时,我注意到了JIT优化的一个奇怪边界情况。

以下程序不是“微基准测试”,也旨在可靠地测量执行时间(正如其他问题的答案中指出的那样)。它仅用作MCVE来重现这个问题:

class MissedLoopOptimization
{
    public static void main(String args[])
    {
        for (int j=0; j<3; j++)
        {
            for (int i=0; i<5; i++)
            {
                long before = System.nanoTime();
                runWithMaxValue();
                long after = System.nanoTime();
                System.out.println("With MAX_VALUE   : "+(after-before)/1e6);
            }
            for (int i=0; i<5; i++)
            {
                long before = System.nanoTime();
                runWithMaxValueMinusOne();
                long after = System.nanoTime();
                System.out.println("With MAX_VALUE-1 : "+(after-before)/1e6);
            }
        }
    }

    private static void runWithMaxValue()
    {
        final int n = Integer.MAX_VALUE;
        int i = 0;
        while (i++ < n) {}
    }

    private static void runWithMaxValueMinusOne()
    {
        final int n = Integer.MAX_VALUE-1;
        int i = 0;
        while (i++ < n) {}
    }
}

它基本上运行相同的循环,while (i++ < n){},其中限制n一次设置为Integer.MAX_VALUE,一次设置为Integer.MAX_VALUE-1

在使用JDK 1.7.0_21和Win7/64执行时。

java -server MissedLoopOptimization

计时结果如下:

...
With MAX_VALUE   : 1285.227081
With MAX_VALUE   : 1274.36311
With MAX_VALUE   : 1282.992203
With MAX_VALUE   : 1292.88246
With MAX_VALUE   : 1280.788994
With MAX_VALUE-1 : 6.96E-4
With MAX_VALUE-1 : 3.48E-4
With MAX_VALUE-1 : 0.0
With MAX_VALUE-1 : 0.0
With MAX_VALUE-1 : 3.48E-4

显然,对于MAX_VALUE-1的情况,JIT会像人们期望的那样:它检测到循环是无用的,并完全消除它。但是,在循环运行到MAX_VALUE时,它并不会删除该循环。

通过查看JIT汇编输出可以证实这一观察结果。

java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly MissedLoopOptimization

记录包含运行到 MAX_VALUE 的方法的以下汇编代码:

Decoding compiled method 0x000000000254fa10:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} &apos;runWithMaxValue&apos; &apos;()V&apos; in &apos;MissedLoopOptimization&apos;
  #           [sp+0x20]  (sp of caller)
  0x000000000254fb40: sub    $0x18,%rsp
  0x000000000254fb47: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - MissedLoopOptimization::runWithMaxValue@-1 (line 29)
  0x000000000254fb4c: mov    $0x1,%r11d
  0x000000000254fb52: jmp    0x000000000254fb63
  0x000000000254fb54: nopl   0x0(%rax,%rax,1)
  0x000000000254fb5c: data32 data32 xchg %ax,%ax
  0x000000000254fb60: inc    %r11d              ; OopMap{off=35}
                                                ;*goto
                                                ; - MissedLoopOptimization::runWithMaxValue@11 (line 30)
  0x000000000254fb63: test   %eax,-0x241fb69(%rip)        # 0x0000000000130000
                                                ;*goto
                                                ; - MissedLoopOptimization::runWithMaxValue@11 (line 30)
                                                ;   {poll}
  0x000000000254fb69: cmp    $0x7fffffff,%r11d
  0x000000000254fb70: jl     0x000000000254fb60  ;*if_icmpge
                                                ; - MissedLoopOptimization::runWithMaxValue@8 (line 30)
  0x000000000254fb72: add    $0x10,%rsp
  0x000000000254fb76: pop    %rbp
  0x000000000254fb77: test   %eax,-0x241fb7d(%rip)        # 0x0000000000130000
                                                ;   {poll_return}
  0x000000000254fb7d: retq   
  0x000000000254fb7e: hlt    
  0x000000000254fb7f: hlt    
[Exception Handler]
[Stub Code]
  0x000000000254fb80: jmpq   0x000000000254e820  ;   {no_reloc}
[Deopt Handler Code]
  0x000000000254fb85: callq  0x000000000254fb8a
  0x000000000254fb8a: subq   $0x5,(%rsp)
  0x000000000254fb8f: jmpq   0x0000000002528d00  ;   {runtime_call}
  0x000000000254fb94: hlt    
  0x000000000254fb95: hlt    
  0x000000000254fb96: hlt    
  0x000000000254fb97: hlt    

我们可以清晰地看到这个循环,它与0x7fffffff进行比较,并跳回inc。相比之下,当它运行到MAX_VALUE-1的情况下,汇编代码如下:

Decoding compiled method 0x000000000254f650:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} &apos;runWithMaxValueMinusOne&apos; &apos;()V&apos; in &apos;MissedLoopOptimization&apos;
  #           [sp+0x20]  (sp of caller)
  0x000000000254f780: sub    $0x18,%rsp
  0x000000000254f787: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                ; - MissedLoopOptimization::runWithMaxValueMinusOne@-1 (line 36)
  0x000000000254f78c: add    $0x10,%rsp
  0x000000000254f790: pop    %rbp
  0x000000000254f791: test   %eax,-0x241f797(%rip)        # 0x0000000000130000
                                                ;   {poll_return}
  0x000000000254f797: retq   
  0x000000000254f798: hlt    
  0x000000000254f799: hlt    
  0x000000000254f79a: hlt    
  0x000000000254f79b: hlt    
  0x000000000254f79c: hlt    
  0x000000000254f79d: hlt    
  0x000000000254f79e: hlt    
  0x000000000254f79f: hlt    
[Exception Handler]
[Stub Code]
  0x000000000254f7a0: jmpq   0x000000000254e820  ;   {no_reloc}
[Deopt Handler Code]
  0x000000000254f7a5: callq  0x000000000254f7aa
  0x000000000254f7aa: subq   $0x5,(%rsp)
  0x000000000254f7af: jmpq   0x0000000002528d00  ;   {runtime_call}
  0x000000000254f7b4: hlt    
  0x000000000254f7b5: hlt    
  0x000000000254f7b6: hlt    
  0x000000000254f7b7: hlt    
所以我的问题是:为什么Integer.MAX_VALUE有什么特殊之处,使得JIT不能像对Integer.MAX_VALUE-1那样进行优化呢?我猜测这可能与cmp指令有关,该指令用于有符号算术运算,但这本身不是一个令人信服的理由。是否有人能解释一下,并可能给出在OpenJDK HotSpot代码中处理此情况的指针?
(另外一点说一下:我希望答案还可以解释在另一个问题中所要求的i++++i之间的不同行为,假设缺少优化的原因显然是由于Integer.MAX_VALUE循环限制所致)

我想知道如果你执行while (++i <= n) {}会输出什么。 - Volune
1
@Volune 实际上,我尝试了不同的配置,比如 i++<n++i<ni++<=nn<=++i 等等,但没有系统地比较和记录所有可能的配置,以保持实际问题的清晰。 - Marco13
1个回答

37

我没有找出Java语言规范,但我猜测这可能与以下差异有关:

  • i++ < (Integer.MAX_VALUE - 1) 永远不会溢出。一旦i达到Integer.MAX_VALUE - 1,它将递增到Integer.MAX_VALUE,然后循环终止。

  • i++ < Integer.MAX_VALUE 包含整数溢出。一旦i达到Integer.MAX_VALUE,它被递增一次导致溢出,然后循环终止。

我假设JIT编译器“不情愿”优化掉具有这种边缘条件的循环-关于整数溢出条件下的循环优化存在大量问题,因此这种不情愿可能是相当合理的。

也可能存在某些硬性要求,不允许优化掉整数溢出,虽然我对此有所怀疑,因为在Java中直接不能检测或处理整数溢出。


1
我现在找不到参考资料,但是JLS确实有类似于C++的“as-if”规则,因此JIT有权进行优化(在Java中没有办法注意到操作是否溢出,例如检查标志)。但显然,在可能溢出时允许优化会引发很多问题,所以这也是我的猜测。 - Voo
JLS可能不包含这样的信息,因为这主要是JIT的问题。防止溢出的想法符合我的假设,这可能与有符号的cmp指令有关,但我仍然想知道为什么这似乎会导致如此完全不同的行为。 - Marco13
@Marco 我猜测溢出会在常量传播期间引发一些警告标志。HotSpot使用以下算法,但我从未深入了解过细节。 - Voo
@Voo:一段时间以前,关于整数溢出有一堆漏洞。 - thkala
9
即使一个循环不执行任何操作,优化器在删除它时也必须小心谨慎,因为它必须证明该循环不会无限执行。当涉及到循环条件的变量可能会溢出时,这显然更加困难。 - Holger
显示剩余2条评论

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