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

91

Недавно я заметил, что объявление массива, содержащего 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 + "";

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

Сипко
источник
3
Не примечание: System.nanoTime()предпочтительнее System.currentTimeMillis()для сравнительного анализа.
rocketboy
4
Мне просто интересно ? Ты под линуксом? Меняется ли поведение с ОС?
bsd
9
Как вообще этот вопрос получил голос против?
Рохит Джайн
2
FWIW, я вижу аналогичные расхождения в производительности, если я запускаю этот код byteвместо double.
Оливер Чарльзворт
3
@ThomasJungblut: Так чем же объясняется несоответствие в эксперименте ОП?
Оливер Чарльзворт

Ответы:

88

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

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

double[] test = new double[64];

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

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

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

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

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

java -XX:EliminateAllocationArraySizeLimit=65 Tests

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

носид
источник
9
Но почему оптимизатор обнаруживает, что массив размером 64, а не 65, является съемным
ug_
10
@nosid: хотя код OP может быть нереалистичным, он явно вызывает интересное / неожиданное поведение в JVM, которое может иметь последствия в других ситуациях. Думаю, уместно спросить, почему это происходит.
Оливер Чарльзуорт
1
@ThomasJungblut Я не думаю, что петля удаляется. Вы можете добавить int total вне цикла и добавить total + = test [0]; к примеру выше. Затем, распечатав результат, вы увидите, что общее количество = 100 миллионов, и он запускается менее чем за секунду.
Сипко
1
Замена в стеке заключается в замене интерпретируемого кода на скомпилированный «на лету» вместо замены распределения кучи распределением стека. EliminateAllocationArraySizeLimit - это предельный размер массивов, которые считаются скалярными заменяемыми в анализе выхода. Таким образом, основной момент, заключающийся в том, что эффект связан с оптимизацией компилятора, верен, но это происходит не из-за выделения стека, а из-за того, что на этапе анализа выхода не удается заметить, что выделение не требуется.
kiheru
2
@Sipko: Вы пишете, что приложение не масштабируется по количеству потоков. Это признак того, что проблема не связана с микрооптимизациями, о которых вы спрашиваете. Я рекомендую смотреть на картину в целом, а не на мелкие детали.
nosid
2

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

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

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

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

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

Горячие лижет
источник
Поздно к этой теме. Почему размещение в стеке происходит быстрее, чем в куче? Согласно нескольким статьям, для размещения в куче требуется ~ 12 инструкций. Здесь мало возможностей для улучшения.
Vortex
@Vortex - Для размещения в стеке требуется 1-2 инструкции. Но это для выделения всего кадра стека. Кадр стека должен быть выделен в любом случае, чтобы иметь область сохранения регистров для подпрограммы, поэтому любые другие переменные, выделенные в то же время, «свободны». Как я уже сказал, стек не требует сборки мусора. Накладные расходы GC для элемента кучи намного превышают затраты на операцию выделения кучи.
Hot Licks