Объявление нескольких массивов с 64 элементами в 1000 раз быстрее, чем объявление массива из 65 элементов

Недавно я заметил, что объявление массива, содержащего 64 элемента, намного быстрее ( > 1000 раз), чем объявление массива того же типа с 65 элементами.

Вот код, который я использовал для тестирования:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

Это работает примерно через 6 мс, если я заменяю new double[64] на new double[65], это занимает приблизительно 7 секунд. Эта проблема становится экспоненциально более серьезной, если задание распространяется на все больше и больше потоков, из которых возникает моя проблема.

Эта проблема также возникает с различными типами массивов, такими как int[65] или String[65]. Эта проблема не возникает при больших строках: String test = "many characters";, но начинает происходить, когда это изменяется на String test = i + "";

Мне было интересно, почему это так, и если можно обойти эту проблему.

Ответ 1

Вы наблюдаете поведение, вызванное оптимизациями, выполненными JIT-компилятором вашей виртуальной машины Java. Это поведение воспроизводимо запускается с помощью скалярных массивов до 64 элементов и не запускается с массивами размером больше 64.

Прежде чем вдаваться в подробности, давайте более подробно рассмотрим тело цикла:

double[] test = new double[64];

Тело не имеет эффекта (наблюдаемое поведение). Это означает, что вне выполнения программы не имеет значения, выполняется ли этот оператор или нет. То же самое верно для всего цикла. Так что может случиться так, что оптимизатор кода переводит цикл в нечто (или ничего) с тем же функциональным и другим режимом синхронизации.

Для контрольных показателей вы должны придерживаться следующих двух рекомендаций. Если бы вы это сделали, разница была бы значительно меньше.

  • Разбавьте компилятор JIT (и оптимизатор), выполнив контрольный тест несколько раз.
  • Используйте результат каждого выражения и распечатайте его в конце теста.

Теперь давайте вдаваться в подробности. Неудивительно, что существует оптимизация, которая запускается для скалярных массивов не более 64 элементов. Оптимизация является частью анализа Escape. Он помещает в стек небольшие объекты и небольшие массивы вместо того, чтобы выделять их на кучу - или даже лучше оптимизировать их полностью. Вы можете найти некоторую информацию об этом в следующей статье Брайана Гетца, написанной в 2005 году:

Оптимизация может быть отключена с помощью опции командной строки -XX:-DoEscapeAnalysis. Магическое значение 64 для скалярных массивов также может быть изменено в командной строке. Если вы выполните свою программу следующим образом, не будет разницы между массивами с 64 и 65 элементами:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

Сказав это, я категорически не рекомендую использовать такие параметры командной строки. Я сомневаюсь, что это имеет огромное значение в реалистичном применении. Я бы использовал его только в том случае, если бы я был абсолютно уверен в необходимости - и не основывался на результатах некоторых псевдо-тестов.

Ответ 2

Существует несколько способов, которыми может быть разница, основанная на размере объекта.

Как указано в nosid, JITC может (скорее всего) выделять небольшие "локальные" объекты в стеке, а отсечка размера для "малых" массивов может состоять из 64 элементов.

Выделение в стеке значительно быстрее, чем распределение в куче, и, более того, стек не нужно собирать мусор, поэтому накладные расходы GC значительно сокращаются. (И для этого тестового случая накладные расходы GC, вероятно, составляют 80-90% от общего времени выполнения.)

Кроме того, после того, как значение будет выделено в стеке, JITC может выполнить "удаление мертвого кода", определить, что результат new никогда не используется нигде, и после того, как завершение отсутствия побочных эффектов, которые будут потеряны, устраните всю операцию new, а затем (теперь пустой).

Даже если JITC не выполняет распределение стека, вполне возможно, что объекты меньшего размера, чем определенный размер, будут распределяться в куче по-разному (например, из другого "пространства" ), чем более крупные объекты. (Обычно это не приводило бы к столь резким временным разницам.)