.toArray (новый MyClass [0]) или .toArray (новый MyClass [myList.size ()])?

176

Предполагая, что у меня есть ArrayList

ArrayList<MyClass> myList;

И я хочу вызвать toArray, есть ли причина для использования производительности

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

над

MyClass[] arr = myList.toArray(new MyClass[0]);

?

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

Конечно, в 99% случаев это не имеет значения, так или иначе, но я хотел бы сохранить согласованный стиль между моим нормальным кодом и оптимизированными внутренними циклами ...

itsadok
источник
7
Похоже, что этот вопрос уже решен в новом сообщении в блоге Алексея Шипилёва, Массивы Древних !
glts
7
Из поста блога: «Итог: toArray (новый T [0]) кажется быстрее, безопаснее и чище по контракту, и поэтому должен быть выбором по умолчанию сейчас».
DavidS

Ответы:

109

Как ни странно, самая быстрая версия на Hotspot 8:

MyClass[] arr = myList.toArray(new MyClass[0]);

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

Результаты тестов (оценка в микросекундах, меньше = лучше):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025  0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155  0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512  0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884  0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147  0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977  5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019  0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133  0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075  0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318  0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652  0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692  8.957  us/op

Для справки, код:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
  @Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
  private final List<Integer> list = new ArrayList<>();
  @Setup public void populateList() {
    for (int i = 0; i < n; i++) list.add(0);
  }
  @Benchmark public Integer[] preSize() {
    return list.toArray(new Integer[n]);
  }
  @Benchmark public Integer[] resize() {
    return list.toArray(new Integer[0]);
  }
}

Вы можете найти похожие результаты, полный анализ и обсуждение в блоге Arrays of Wisdom of the Ancients . Подводя итог: компилятор JVM и JIT содержит несколько оптимизаций, которые позволяют ему дешево создавать и инициализировать новый массив правильного размера, и эти оптимизации нельзя использовать, если вы создаете массив самостоятельно.

assylias
источник
2
Очень интересный комментарий. Я удивлен, что никто не прокомментировал это. Я предполагаю, что это потому, что это противоречит другим ответам здесь, насколько скорость. Также интересно отметить, что репутация этого парня почти выше, чем все остальные ответы вместе взятые.
Тачка Тризкит
Я отвлекся. Я также хотел бы видеть контрольные показатели для MyClass[] arr = myList.stream().toArray(MyClass[]::new);.., который, я думаю, будет медленнее. Кроме того, я хотел бы видеть тесты различий с объявлением массива. Как в разнице между: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);и MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... или не должно быть никакой разницы? Я полагаю, что эти два вопроса находятся за пределами toArrayфункций. Но эй! Я не думал, что узнаю о других запутанных различиях.
Pimp Trizkit
1
@PimpTrizkit Только что проверил: использование дополнительной переменной не имеет значения, как и ожидалось. Использование потока занимает от 60% до 100% больше времени при toArrayпрямом вызове (чем меньше размер, тем больше относительная
нагрузка
Вау, это был быстрый ответ! Спасибо! Да, я подозревал это. Преобразование в поток не звучало эффективно. Но вы никогда не знаете!
Тачка Тризкит
2
Такой же вывод был найден здесь: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019
122

Что касается ArrayList в Java 5 , массив будет заполнен уже, если он имеет правильный размер (или больше). следовательно

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

создаст один объект массива, заполнит его и вернет его в «arr». С другой стороны

MyClass[] arr = myList.toArray(new MyClass[0]);

создаст два массива. Второй - это массив MyClass с длиной 0. Таким образом, существует объект для объекта, который будет немедленно отброшен. Поскольку исходный код предполагает, что компилятор / JIT не может оптимизировать этот, чтобы он не был создан. Кроме того, использование объекта нулевой длины приводит к приведению типов в методе toArray ().

Смотрите источник ArrayList.toArray ():

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

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

Георгий
источник
1
Два комментария могут быть интересны кому-то: 1) LinkedList.toArray (T [] a) еще медленнее (использует отражение: Array.newInstance) и более сложный; 2) С другой стороны, в выпуске JDK7 я был очень удивлен, обнаружив, что обычно мучительно медленный Array.newInstance выполняет почти так же быстро, как и обычное создание массива!
java.is.for.desktop
1
@ktaria size является частным членом ArrayList, определяя **** размер сюрприза ****. См. ArrayList SourceCode
MyPasswordIsLasercats
3
Гадание производительности без эталонов работает только в тривиальных случаях. На самом деле, new Myclass[0]это быстрее: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S
Это больше не действительный ответ на JDK6 +
Антон Антонов
28

Из JetBrains Intellij Идея осмотра:

Существует два стиля для преобразования коллекции в массив: либо с использованием предварительно заданного размера (например, c.toArray (new String [c.size ()]) ), либо с использованием пустого массива (например, c.toArray (new String [ 0]) .

В более старых версиях Java было рекомендовано использование массива с предварительно заданным размером, поскольку вызов отражения, необходимый для создания массива правильного размера, был довольно медленным. Однако после поздних обновлений OpenJDK 6 этот вызов был встроен, что сделало производительность пустой версии массива такой же, а иногда даже лучше, по сравнению с версией предварительного размера. Также передача массива предварительно заданного размера опасна для одновременной или синхронизированной коллекции, так как возможна гонка данных между вызовом size и toArray, что может привести к появлению дополнительных нулей в конце массива, если коллекция одновременно сокращалась во время операции.

Эта проверка позволяет следовать единому стилю: либо с использованием пустого массива (что рекомендуется в современной Java), либо с использованием массива предварительно заданного размера (который может быть быстрее в более старых версиях Java или JVM не на основе HotSpot).

Антон Антонов
источник
Если все это скопированный / цитируемый текст, можем ли мы отформатировать его соответствующим образом, а также предоставить ссылку на источник? На самом деле я приехал сюда из-за проверки IntelliJ, и мне очень интересна ссылка для просмотра всех их проверок и причин их возникновения.
Тим Бют
3
Здесь вы можете проверить тексты проверок: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов
17

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

Том Хотин - Tackline
источник
2
Upvoting «повторно использует пустой массив», потому что это компромисс между читабельностью и потенциальной производительностью, который заслуживает рассмотрения. Передача аргумент объявлен private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]не мешает возвращаемый массив из строится путем отражения, но это предотвратить дополнительный массив строится каждый каждый раз.
Майкл Шепер
Machael прав, если вы используете массив нулевой длины, пути назад нет: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); что было бы лишним, если бы размер был> = actualSize (JDK7)
Алекс
Если вы можете привести цитату «современные JVM оптимизируют конструкцию отражающей матрицы в этом случае», я с удовольствием поддержу этот ответ.
Том Паннинг
Я учусь здесь. Если вместо этого я использую: MyClass[] arr = myList.stream().toArray(MyClass[]::new);это помогло бы или повредило бы с синхронизированными и параллельными коллекциями. И почему? Пожалуйста.
Сутенер Тризкит
3

toArray проверяет, что переданный массив имеет правильный размер (то есть достаточно большой, чтобы вместить элементы из вашего списка) и, если это так, использует это. Следовательно, если размер массива будет меньше требуемого, будет создан рефлексивно новый массив.

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

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

Дейв Чейни
источник
2

Первый случай более эффективен.

Это потому, что во втором случае:

MyClass[] arr = myList.toArray(new MyClass[0]);

среда выполнения фактически создает пустой массив (с нулевым размером), а затем внутри метода toArray создает другой массив для соответствия фактическим данным. Это создание выполняется с использованием отражения с использованием следующего кода (взятого из jdk1.5.0_10):

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.
    newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Используя первую форму, вы избегаете создания второго массива, а также избегаете кода отражения.

Панагиотис Коррос
источник
toArray () не использует отражение. По крайней мере, до тех пор, пока ты не считаешь "приведение" к размышлению, во всяком случае ;-).
Георгий
toArray (T []) делает. Для этого необходимо создать массив соответствующего типа. Современные JVM оптимизируют этот вид отражения примерно с той же скоростью, что и неотражающая версия.
Том Хотин - tackline
Я думаю, что это использует отражение. JDK 1.5.0_10 работает точно, и рефлексия - единственный способ, которым я знаю, создать массив типа, который вы не знаете во время компиляции.
Панагиотис Коррос
Тогда один из примеров исходного кода ее (тот, что выше или мой) устарел. К сожалению, я не нашел правильный номер под-версии для моего, хотя.
Георгий
1
Георгий, твой код из JDK 1.6, и если ты увидишь реализацию метода Arrays.copyTo, то увидишь, что реализация использует отражение.
Панагиотис Коррос
-1

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

MiguelMunoz
источник
-2

Использование 'toArray' с массивом правильного размера будет работать лучше, так как альтернатива создаст сначала массив нулевого размера, а затем массив правильного размера. Однако, как вы говорите, разница, вероятно, будет незначительной.

Также обратите внимание, что компилятор javac не выполняет никакой оптимизации. В настоящее время все оптимизации выполняются компиляторами JIT / HotSpot во время выполнения. Я не знаю каких-либо оптимизаций вокруг 'toArray' в каких-либо JVM.

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

Мэтью Мердок
источник
OTOH, если стандартом является использование массива нулевой длины, то случаи, которые отклоняются, подразумевают, что производительность является проблемой.
Майкл Шепер
-5

Пример кода для целого числа:

Integer[] arr = myList.toArray(new integer[0]);
Rasol
источник