Подтвердить что ты не робот

Почему Java быстрее, если он повторяет один и тот же код?

С учетом следующего кода:

public class Test{

    static int[] big = new int [10000];

    public static void main(String[] args){
        long time;
        for (int i = 0; i < 16; i++){
            time = System.nanoTime();
            getTimes();
            System.out.println(System.nanoTime() - time);
        }    
    }
    public static void getTimes(){
        int d;
        for (int i = 0; i < 10000; i++){
            d = big[i];
        }    
    }
}

Выход показывает тенденцию к уменьшению продолжительности:

171918
167213
165930
165502
164647
165075
203991
70563
45759
43193
45759
44476
45759
52601
47897
48325

Почему один и тот же код в getTimes выполняется менее чем через треть после того, как он был выполнен 8 раз или более? (Edit: Это не всегда происходит в 8-й раз, но с 5-го по 10-й)

4b9b3361

Ответ 1

Тот факт, что то, что вы видите, является результатом некоторой оптимизации JIT, должно быть ясно, глядя на все полученные вами комментарии. Но что действительно происходит и почему этот код оптимизирован всегда почти после того же количества итераций внешнего for?

Я постараюсь ответить на оба вопроса, но помните, что все объяснение здесь относительное только для Oracle Hotspot VM. Нет спецификации Java, которая определяет, как должна работать JVM JIT.

Прежде всего, посмотрим, что делает JIT, выполняющая программу тестирования с некоторым дополнительным флагом (достаточно простого JVM для ее запуска, нет необходимости загружать разделяемую библиотеку отладки, необходимую для некоторых параметров UnlockDiagnosticVMOptions):

java -XX:+PrintCompilation Test

Выполнение завершается с помощью этого вывода (удаление нескольких строк в начале, которые показывают, что компилируются другие методы):

[...]
195017
184573
184342
184262
183491
189494
    131   51%      3       Test::getTimes @ 2 (22 bytes)
245167
    132   52       3       Test::getTimes (22 bytes)
165144  

65090
    132   53       1       java.nio.Buffer::limit (5 bytes)
59427
    132   54%      4       Test::getTimes @ 2 (22 bytes)  
75137
48110    
    135   51%     3        Test::getTimes @ -2 (22 bytes)   made not entrant

    142   55       4       Test::getTimes (22 bytes)
150820
86951
90012
91421

printlns из вашего кода чередуются с диагностической информацией, связанной с компиляцией, выполняемой JIT. Просмотр одной строки:

131    51%      3       Test::getTimes @ 2 (22 bytes)

Каждый столбец имеет следующее значение:

  • Отметка
  • Идентификатор компиляции (с дополнительными атрибутами при необходимости)
  • Уровень многоуровневой сборки
  • Краткое краткое имя метода (при наличии < <26 > , если доступно)
  • Скомпилированный размер метода

Сохранение только строк, связанных с getTimes:

    131   51%      3       Test::getTimes @ 2 (22 bytes)
    132   52       3       Test::getTimes (22 bytes)
    132   54%      4       Test::getTimes @ 2 (22 bytes)     
    135   51%      3       Test::getTimes @ -2 (22 bytes)   made not entrant
    142   55       4       Test::getTimes (22 bytes)

Ясно, что getTimes компилируется более одного раза, но каждый раз, когда он скомпилирован по-другому.

Этот символ % означает, что была выполнена замена на стеке (OSR), что означает, что цикл 10k, содержащийся в getTimes, был скомпилирован отдельно от остальной части метода и что JVM заменил этот раздел код метода с скомпилированной версией. osr_bci - это индекс, который указывает на этот новый скомпилированный блок кода.

Следующая компиляция представляет собой классическую компиляцию JIT, которая компилирует весь метод getTimes (размер все тот же, поскольку в этом методе не существует ничего другого, кроме цикла).

В третий раз выполняется другая OSR, но на другом уровне. Многоуровневая компиляция была добавлена ​​в Java7 и в основном позволяет JVM выбирать режим JIT клиента или сервера во время выполнения, свободно переключаясь между ними, когда это необходимо. Режим "Клиент" выполняет более простой набор стратегий оптимизации, в то время как режим сервера способен применять более сложные оптимизации, которые, с другой стороны, имеют большую стоимость с точки зрения времени, затрачиваемого на компиляцию.

Я не буду вдаваться в подробности о разных режимах или о многоуровневой компиляции, если вам нужна дополнительная информация, я рекомендую Производительность Java: окончательное руководство Scott Oaks, а также проверить этот вопрос, который объясняет, какие изменения между уровнями.

Возвращаясь к выводу PrintCompilation, суть в том, что с определенного момента времени последовательность компиляций с возрастающей сложностью выполняется до тех пор, пока метод не станет, по-видимому, стабильным (т.е. JIT не скомпилирует его снова).

Итак, почему все это начинается в тот определенный момент времени, после 5-10 повторений основного цикла?

Поскольку внутренний цикл getTimes стал "горячим".

Hotspot VM обычно определяет "горячие" те методы, которые были вызваны не менее чем 10k раз (что порог исторического значения по умолчанию можно изменить с помощью -XX:CompileThreshold=<num>, при многоуровневой компиляции теперь имеется несколько пороговых значений), но в случае из OSR я предполагаю, что он выполнялся, когда блок кода считается "горячим" достаточно, в течение абсолютного или относительного времени выполнения внутри метода содержит его.

Дополнительные ссылки

Руководство по PrintCompilation от Krystal Mok

Производительность Java: окончательное руководство

Ответ 2

Компилятор виртуальной машины JIT (Just in Time) оптимизирует интуицию Java-байтового кода. Например, если у вас есть оператор if(), который является ложным примерно в 99% случаев, jit оптимизирует ваш код для ложного случая, что делает ваши истинные случаи в конечном итоге более медленными. Извините за плохой английский.

Ответ 3

Пример: код перед оптимизацией

class A {
  B b;
  public void newMethod() {
    y = b.get();  //calling get() function
    ...do stuff...
    z = b.get();   // calling again
    sum = y + z;
  }
}
class B {
   int value;
   final int get() {
      return value;
   }
}

Пример: Code After Optimization

class A {
B b;
public void newMethod() {
   y = b.value;
   ...do stuff...
   sum = y + y; 
}
}
class B {
   int value;
   final int get() {
      return value;
   }
}

Первоначально код содержал два вызова метода b.get(). После оптимизация, вызов двух методов оптимизирован в один операция с переменной копией; то есть оптимизированный код не требуется выполнить вызов метода для получения значения поля класса B.

Подробнее