JIT не оптимизирует цикл, который включает Integer.MAX_VALUE

При написании ответа на еще один вопрос я заметил странный пограничный случай для оптимизации JIT.

Следующая программа не является "Microbenchmark" и не предназначена для надежного измерения времени выполнения (как указано в ответах на другой вопрос). Он предназначен только для 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.

При выполнении этого на Win7/64 с JDK 1.7.0_21 и

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::[email protected] (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::[email protected] (line 30)
  0x000000000254fb63: test   %eax,-0x241fb69(%rip)        # 0x0000000000130000
                                                ;*goto
                                                ; - MissedLoopOptimization::[email protected] (line 30)
                                                ;   {poll}
  0x000000000254fb69: cmp    $0x7fffffff,%r11d
  0x000000000254fb70: jl     0x000000000254fb60  ;*if_icmpge
                                                ; - MissedLoopOptimization::[email protected] (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::[email protected] (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)

Ответ 1

Я не выкопал спецификацию языка Java, но я бы предположил, что он имеет отношение к этой разнице:

  • i++ < (Integer.MAX_VALUE - 1) никогда не переполняется. Как только i достигает Integer.MAX_VALUE - 1, он увеличивается до Integer.MAX_VALUE, а затем цикл завершается.

  • i++ < Integer.MAX_VALUE содержит целочисленное переполнение. Как только i достигает Integer.MAX_VALUE, он увеличивается на один, вызывая переполнение, а затем цикл завершается.

Я предполагаю, что компилятор JIT "неохотно" оптимизирует циклы с такими угловыми условиями - там была целая куча ошибок w.r.t. оптимизация цикла в условиях переполнения целого числа, так что нежелание, вероятно, вполне оправдано.

Также может быть какое-то жесткое требование, которое не позволяет оптимизировать переполнение целых чисел, хотя я как-то сомневаюсь, что поскольку переполнение целых чисел не может быть непосредственно обнаружено или иным образом обрабатываться на Java.