Рубрики
Без рубрики

Для циклов, распределений и анализа побега

Для приложений Java в определенных доменах действительно важно создание объектов/мусора… Помеченный java, escapeanalysis, производительность.

Для приложений Java в определенных доменах действительно важно, чтобы создание объектов/мусора оставалось минимальным. Эти приложения обычно не могут позволить себе паузы GC, поэтому они используют специальные методы и методологии, чтобы избежать создания мусора. Один из этих методов связан с итерацией по коллекции или массиву элементов. Предпочтительный способ – использовать классический цикл for. Цикл enhanced-for избегается, так как “он создает мусор”, используя Итератор коллекции под обложкой.

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

До того, как я экспериментировал с этим, я (ошибочно?) сделал некоторые предположения:

  • Используя обычный цикл for для массива или коллекции, он не создает никаких новых распределений
  • Использование расширенного цикла for для массива(?) или коллекции это действительно выделяет
  • Используя расширенный цикл for для массива или набора примитивов, путем случайного автоматического добавления значения примитива, он приводит к довольно высокой скорости создания новых объектов

Чтобы лучше понять различия и особенно тот факт, что массив не имеет итератора, следовательно, как работает расширенный цикл for, я выполнил следующие действия.

Шаг 1: Усовершенствованный -для Петли Под Крышкой

Расширенный цикл for – это просто синтаксический сахар, но к чему он на самом деле приводит при использовании для массива и при использовании в коллекции элементов?

Ответ на этот вопрос можно найти в Спецификации языка Java .

Основными двумя моментами из приведенной выше ссылки являются:

Если тип выражения является подтипом Iterable, то перевод выглядит следующим образом. Если тип выражения является подтипом Iterable для некоторого аргумента типа X, то пусть I будет типом java.util. Итератор; в противном случае пусть я буду необработанным типом java.util. Итератор. Расширенный оператор for эквивалентен базовому оператору for вида:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
    {VariableModifier} TargetType Identifier =
        (TargetType) #i.next();
    Statement
}

и

В противном случае Выражение обязательно имеет тип массива, T[]. Пусть L1… Lm – (возможно, пустая) последовательность меток, непосредственно предшествующая расширенному оператору for. Расширенный оператор for эквивалентен базовому оператору for вида:

T[] #a = Expression;
L1: L2: ... Lm:
for (int #i = 0; #i < #a.length; #i++) {
    {VariableModifier} TargetType Identifier = #a[#i];
    Statement
}

Из вышесказанного кто-то может заметить, что действительно Итератор используется в расширенном цикле для коллекций. Однако для массива расширенный цикл for – это просто синтаксический сахар, который эквивалентен обычному циклу for.

После понимания того, как JVM фактически реализует расширенный цикл for в разных вариантах использования, наши предположения изменились:

  • Используя обычный цикл for над массивом или коллекцией, он НЕ создает никаких новых распределений
  • Используя расширенный цикл for над массивом , он НЕ создает никаких новых распределений
  • Использование расширенного цикла для над коллекцией это действительно выделяет
  • Используя расширенный цикл for для массива или набора примитивов, путем случайного автоматического добавления значения примитива, он приводит к довольно высокой скорости создания новых объектов

Шаг 2: Определение Теста

Чтобы протестировать различные сценарии, я создал очень простой тест, который можно увидеть здесь

Сам тест очень прост, основными моментами, на которые следует обратить внимание, являются:

  • Тест создает статический массив и статический ArrayList и предварительно заполняет их 100 000 целыми числами. В случае массива это примитивы, но в случае коллекции мы используем обычный Список массивов это реальные Целое число объекты
  • Тест выполняет различные сценарии для примеров цикла 1 000 000 раз
  • Используемая память считывается до начала итераций и сравнивается на протяжении всего выполнения (каждые 100 вызовов) программы, чтобы определить, изменился ли профиль памяти
  • Сценарии тестирования включают:
    • Цикл for по массиву
    • Расширенный цикл for по массиву
    • Расширенный цикл for по массиву, также автоматически блокируя элементы
    • Цикл for для коллекции
    • Расширенный цикл “для” над коллекцией
    • Итератор, основанный на цикле for для коллекции, воспроизводящий поведение синтаксического сахара расширенного цикла for

Шаг 3: Запуск Теста

Мы провели тест с приведенной ниже настройкой:

  • ОС: macOS Каталина (10.15.3), Core i5 @2,6 Гц, 8 ГБ DDR3
  • JDK: версия openjdk “13.0.2” 2020-01-14
  • JVM_OPTS: -XMS512M -xmx512m -XX:+Разблокировать экспериментальные варианты -XX:+Использовать длительное время

Мы используем Epsilon для того, чтобы исключить любую сборку мусора и позволить памяти просто увеличиться.

При выполнении теста некоторые сценарии было легко проверить в соответствии с нашими ожиданиями:

  • Цикл for по массиву или коллекции не создает никаких распределений
  • Расширенный цикл for по массиву не создает никаких распределений
  • Усовершенствованный цикл for над массивом с автоматической блокировкой, он действительно создает новые объекты

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

Первым шагом исследования было убедиться, что байтовый код указывает на создание нового объекта. Ниже приведен байт-код, который можно использовать для проверки того, что вызов для получения итератора выполняется в строке 5:

  private static long forEachLoopListIterator();
    Code:
       0: lconst_0
       1: lstore_0
       2: getstatic     #5                  // Field LIST_VALUES:Ljava/util/List;
       5: invokeinterface #9,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      10: astore_2
      11: aload_2
      12: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      17: ifeq          39
      20: lload_0
      21: aload_2
      22: invokeinterface #11,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      27: checkcast     #8                  // class java/lang/Integer
      30: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
      33: i2l
      34: ladd
      35: lstore_0
      36: goto          11
      39: lload_0
      40: lreturn

Поскольку мы используем ArrayList , следующий шаг – посмотреть, что делает вызов #iterator() . Это действительно создает новый объект итератора, как видно в Исходный код списка массивов

    public Iterator iterator() {
        return new Itr();
    }

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

Моей последней мыслью был “маловероятный” сценарий, при котором объекты были помещены в стек. Известно, что Hotspot выполняет такого рода оптимизацию, используя вывод Escape-анализа . Честно говоря, я никогда не видел, чтобы это происходило (или, по крайней мере, у меня никогда не было реального времени, чтобы убедиться, что это действительно происходит) до сих пор.

Шаг 4: Запуск Без Анализа Побега

Самый простой и быстрый способ проверить приведенное выше предположение о том, что анализ побега подавался в JIT и приводил к распределению объектов в стеке, – это отключить анализ побега. Это можно сделать, добавив -XX:-Делает ли Capeanalysis в наших вариантах JVM.

Действительно, на этот раз, повторив тот же тест, мы видим, что профиль памяти для расширенного цикла for для коллекции неуклонно увеличивается. Объекты Итератора , созданные из ArrayList#iterator() , размещаются в куче в каждом цикле.

Вывод

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

Наконец, стоит сказать, что описанное выше поведение наблюдалось в эксперименте, а не в реальном коде. Я бы предположил, что в большинстве случаев в производственной системе такое поведение не проявляется (т.Е. Распределение в стеке), но тот факт, что JIT является такой сложной частью программного обеспечения, очень обнадеживает, поскольку он может упреждающе оптимизировать код, не позволяя нам реализовать дополнительные выгоды.

Этот пост также размещен в моем личном блоге Этот пост также размещен в моем личном блоге

Оригинал: “https://dev.to/nikos_katsanos/for-loops-allocations-and-escape-analysis-4428”