Запуск Java замедляется после некоторых прогонов/JIT-сбоев?

Итак, я хотел сравнить базовые функциональные возможности Java, чтобы добавить некоторые изменения в этот вопрос: Что такое выигрыш от объявления метода как статического.

Я знаю, что писать тесты иногда непросто, но что здесь происходит, я не могу объяснить.

Обратите внимание, что меня не интересует, как это исправить, но почему это происходит.

Класс Test:

public class TestPerformanceOfStaticVsDynamicCalls {

    private static final long RUNS = 1_000_000_000L;

    public static void main( String [] args ){

        new TestPerformanceOfStaticVsDynamicCalls().run();
    }

    private void run(){

        long r=0;
        long start, end;

        for( int loop = 0; loop<10; loop++ ){

            // Benchmark

            start = System.currentTimeMillis();
            for( long i = 0; i < RUNS; i++ ) {
                r += addStatic( 1, i );
            }
            end = System.currentTimeMillis();
            System.out.println( "Static: " + ( end - start ) + " ms" );

            start = System.currentTimeMillis();
            for( long i = 0; i < RUNS; i++ ) {
                r += addDynamic( 1, i );
            }
            end = System.currentTimeMillis();
            System.out.println( "Dynamic: " + ( end - start ) + " ms" );

            // Do something with r to keep compiler happy
            System.out.println( r );

        }

    }


    private long addDynamic( long a, long b ){

        return a+b;
    }

    private static long addStatic( long a, long b ){

        return a+b;
    }

}

Я ожидал, что первый цикл будет прогреваться, а следующие циклы будут быстрее.

Запуск этого в Eclipse дал эти странные результаты:

Static: 621 ms
Dynamic: 631 ms
1000000001000000000
Static: 2257 ms
Dynamic: 2501 ms
2000000002000000000
Static: 2258 ms
Dynamic: 2469 ms
3000000003000000000
Static: 2231 ms
Dynamic: 2464 ms
4000000004000000000

Итак, wtf? Это стало медленнее. Для перекрестной проверки я запускаю тот же код с java/c 7:

Static: 620 ms
Dynamic: 627 ms
1000000001000000000
Static: 897 ms
Dynamic: 617 ms
2000000002000000000
Static: 901 ms
Dynamic: 615 ms
3000000003000000000
Static: 888 ms
Dynamic: 616 ms
4000000004000000000

Итак, здесь только статические вызовы стали медленнее для следующих циклов. Еще более странно, если я переделаю код только для печати r после последнего цикла, который я получаю в Eclipse:

Static: 620 ms
Dynamic: 635 ms
Static: 2285 ms
Dynamic: 893 ms
Static: 2258 ms
Dynamic: 900 ms
Static: 2280 ms
Dynamic: 905 ms
4000000004000000000

И это с помощью java/c 7:

Static: 620 ms
Dynamic: 623 ms
Static: 890 ms
Dynamic: 614 ms
Static: 890 ms
Dynamic: 616 ms
Static: 886 ms
Dynamic: 614 ms
4000000004000000000

а при изменении порядка динамических/статических тестов в eclipse:

Dynamic: 618 ms
Static: 626 ms
1000000001000000000
Dynamic: 632 ms
Static: 2524 ms
2000000002000000000
Dynamic: 617 ms
Static: 2528 ms
3000000003000000000
Dynamic: 622 ms
Static: 2506 ms
4000000004000000000

и в java/c 7:

Dynamic: 625 ms
Static: 646 ms
1000000001000000000
Dynamic: 2470 ms
Static: 633 ms
2000000002000000000
Dynamic: 2459 ms
Static: 635 ms
3000000003000000000
Dynamic: 2464 ms
Static: 645 ms
4000000004000000000

Итак, что здесь происходит?

EDIT: Некоторые системные данные:

Java version "1.7.0_55"
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1ubuntu1)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)

Intel(R) Core(TM) i7-2720QM CPU @ 2.20GHz

EDIT2:

Использование Java8:

Static: 620 ms
Dynamic: 624 ms
1000000001000000000
Static: 890 ms
Dynamic: 618 ms
2000000002000000000
Static: 891 ms
Dynamic: 616 ms
3000000003000000000
Static: 892 ms
Dynamic: 617 ms
4000000004000000000

Другие порядки кода дают похожие результаты (но ohter) здесь.

Ответ 1

Преамбула: написание микрообъектов вручную почти всегда обречено на провал.
Существуют рамки, которые уже решили общие проблемы с бенчмаркингом.

  • Единица компиляции JIT - это метод. Включение нескольких эталонных тестов в один метод приводит к непредсказуемым результатам.

  • JIT сильно зависит от профиля выполнения, т.е. статистики времени выполнения. Если метод запускает первый сценарий в течение длительного времени, JIT оптимизирует сгенерированный код для него. Когда метод внезапно переключается на другой сценарий, не ожидайте, что он будет работать с одинаковой скоростью.

  • JIT может пропустить оптимизацию кода, который не выполняется. Он оставит необычную ловушку для этого кода. Если ловушка когда-либо попадает, JVM будет деоптимизировать скомпилированный метод, переключиться на интерпретатор и после этого перекомпилировать код с новыми знаниями. Например. когда ваш метод run скомпилирован впервые в первом горячем цикле, JIT пока не знает о System.out.println. Как только выполнение достигнет println, более ранний скомпилированный код, скорее всего, будет деоптимизирован.

  • Чем больше метод, тем сложнее оптимизировать его для JIT-компилятора. Например. может показаться, что для хранения всех локальных переменных недостаточно запасных регистров. Что произойдет в вашем случае.

Подводя итог, ваш тест, похоже, проходит по следующему сценарию:

  • Первый горячий цикл (addStatic) запускает компиляцию метода run. Профиль выполнения ничего не знает, кроме метода addStatic.
  • System.out.println вызывает деоптимизацию, и после этого второй горячий цикл (addDynamic) приводит к перекомпиляции метода run.
  • Теперь профиль выполнения содержит информацию только о addDynamic, поэтому JIT оптимизирует второй цикл, и первый цикл, как представляется, имеет дополнительные разливы регистров:

Оптимизированный цикл:

0x0000000002d01054: add    %rbx,%r14
0x0000000002d01057: add    $0x1,%rbx          ;*ladd
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]

0x0000000002d0105b: add    $0x1,%r14          ; OopMap{rbp=Oop off=127}
                                              ;*goto
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]

0x0000000002d0105f: test   %eax,-0x1c91065(%rip)        # 0x0000000001070000
                                              ;*lload
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]
                                              ;   {poll}
0x0000000002d01065: cmp    $0x3b9aca00,%rbx
0x0000000002d0106c: jl     0x0000000002d01054

Петля с дополнительным разливом регистров:

0x0000000002d011d0: mov    0x28(%rsp),%r11  <---- the problem is here
0x0000000002d011d5: add    %r10,%r11
0x0000000002d011d8: add    $0x1,%r10
0x0000000002d011dc: add    $0x1,%r11
0x0000000002d011e0: mov    %r11,0x28(%rsp)    ;*ladd
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]

0x0000000002d011e5: mov    0x28(%rsp),%r11  <---- the problem is here
0x0000000002d011ea: add    $0x1,%r11          ; OopMap{[32]=Oop off=526}
                                              ;*goto
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]

0x0000000002d011ee: test   %eax,-0x1c911f4(%rip)        # 0x0000000001070000
                                              ;*goto
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]
                                              ;   {poll}
0x0000000002d011f4: cmp    $0x3b9aca00,%r10
0x0000000002d011fb: jl     0x0000000002d011d0  ;*ifge
                                              ; - TestPerformanceOfStaticVsDynamicCalls::[email protected]

P.S. Следующие параметры JVM полезны для анализа компиляции JIT:

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+PrintAssembly -XX:CompileOnly=TestPerformanceOfStaticVsDynamicCalls

Ответ 2

Похоже, что Java добавляет значения в переменную r. Я внес несколько изменений, добавив метод run2():

public class TestPerformanceOfStaticVsDynamicCalls {
    private static final long RUNS = 1_000_000_000L;
    public static void main(String[] args) {
        System.out.println("Test run 1 =================================");
        new TestPerformanceOfStaticVsDynamicCalls().run();
        System.out.println("Test run 2 =================================");
        new TestPerformanceOfStaticVsDynamicCalls().run2();
    }
    private void run2() {
        long r = 0;
        long start, end;
        for (int loop = 0; loop < 10; loop++) {
            // Benchmark
            long stat = 0;
            start = System.currentTimeMillis();
            for (long i = 0; i < RUNS; i++) {
                stat += addStatic(1, i);
            }
            end = System.currentTimeMillis();
            System.out.println("Static: " + (end - start) + " ms");
            long dyna = 0;
            start = System.currentTimeMillis();
            for (long i = 0; i < RUNS; i++) {
                dyna += addDynamic(1, i);
            }
            end = System.currentTimeMillis();
            System.out.println("Dynamic: " + (end - start) + " ms");
            // If you really want to have values in "r" then...
            r += stat + dyna;
            // Do something with r to keep compiler happy
            System.out.println(r);
        }
    }
    private void run() {
        long r = 0;
        long start, end;
        for (int loop = 0; loop < 10; loop++) {
            // Benchmark
            start = System.currentTimeMillis();
            for (long i = 0; i < RUNS; i++) {
                r += addStatic(1, i);
            }
            end = System.currentTimeMillis();
            System.out.println("Static: " + (end - start) + " ms");
            start = System.currentTimeMillis();
            for (long i = 0; i < RUNS; i++) {
                r += addDynamic(1, i);
            }
            end = System.currentTimeMillis();
            System.out.println("Dynamic: " + (end - start) + " ms");
            // If you really want to have values in "r" then...
            // Do something with r to keep compiler happy
            System.out.println(r);
        }
    }
    private long addDynamic(long a, long b) {
        return a + b;
    }
    private static long addStatic(long a, long b) {
        return a + b;
    }
}

Результаты для:

Test run 1 =================================
Static: 582 ms
Dynamic: 579 ms
1000000001000000000
Static: 2065 ms
Dynamic: 2352 ms
2000000002000000000
Static: 2084 ms
Dynamic: 2345 ms
3000000003000000000
Static: 2095 ms
Dynamic: 2347 ms
4000000004000000000
Static: 2102 ms
Dynamic: 2338 ms
5000000005000000000
Static: 2073 ms
Dynamic: 2345 ms
6000000006000000000
Static: 2074 ms
Dynamic: 2341 ms
7000000007000000000
Static: 2102 ms
Dynamic: 2355 ms
8000000008000000000
Static: 2062 ms
Dynamic: 2354 ms
9000000009000000000
Static: 2057 ms
Dynamic: 2350 ms
-8446744063709551616
Test run 2 =================================
Static: 584 ms
Dynamic: 582 ms
1000000001000000000
Static: 587 ms
Dynamic: 577 ms
2000000002000000000
Static: 577 ms
Dynamic: 579 ms
3000000003000000000
Static: 577 ms
Dynamic: 577 ms
4000000004000000000
Static: 578 ms
Dynamic: 579 ms
5000000005000000000
Static: 578 ms
Dynamic: 580 ms
6000000006000000000
Static: 577 ms
Dynamic: 579 ms
7000000007000000000
Static: 578 ms
Dynamic: 577 ms
8000000008000000000
Static: 580 ms
Dynamic: 578 ms
9000000009000000000
Static: 576 ms
Dynamic: 579 ms
-8446744063709551616

Что касается того, почему добавление непосредственно к r, я понятия не имею. Может быть, кто-то может дать больше информации о том, почему доступ к r внутри loop block делает вещи намного медленнее.

Ответ 3

Только одно примечание. Я могу наблюдать только это странное поведение, если я использую long для r и i s. Если я преобразую их в int, тогда я получу эти тайминги:

Static: 352 ms
Dynamic: 353 ms
Static: 348 ms
Dynamic: 349 ms
Static: 349 ms
Dynamic: 348 ms
Static: 349 ms
Dynamic: 344 ms

Таким образом, один из возможных решений заключается в том, чтобы избежать long в этих ситуациях. По крайней мере, с Linux/Amd64 Java7, где важна производительность.