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


176

Якщо припустити, що у мене є ArrayList

ArrayList<MyClass> myList;

І я хочу зателефонувати доArray, чи є привід для продуктивності

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

над

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

?

Я віддаю перевагу другому стилю, оскільки він менш багатослівний, і я припускав, що компілятор переконається, що порожній масив дійсно не буде створений, але мені було цікаво, чи це правда.

Звичайно, у 99% випадків це не впливає так чи інакше, але я хотів би зберегти послідовний стиль між моїм звичайним кодом та оптимізованими внутрішніми петлями ...


6
Схоже, це питання вирішено в новій публікації блогу Олексія Шипільова « Масиви мудрості древніх» !
glts

6
З допису в блозі: "Підсумок: 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]);
  }
}

Ви можете знайти подібні результати, повний аналіз та обговорення в блозі " Масиви мудрості древніх" . Підводячи підсумок: компілятор JVM та JIT містить кілька оптимізацій, які дозволяють йому дешево створити та ініціалізувати новий правильний розмір масиву, і ці оптимізації не можна використовувати, якщо ви створюєте масив самостійно.


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функції. Але ей! Я не думав, що дізнаюся про інші заплутані відмінності.
Сутенер Трізкіт

1
@PimpTrizkit Щойно перевірено: використання додаткової змінної не має жодної різниці, як очікувалося, використання потоку займає від 60% до 100% більше часу, як toArrayбезпосередньо дзвонити (чим менший розмір, тим більше відносна накладні витрати)
assylias

Ого, це була швидка реакція! Дякую! Так, я підозрював це. Перетворення на потік не звучало ефективно. Але ти ніколи не знаєш!
Сутенер Трізкіт

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, вказавши розмір **** сюрприз ****. Дивіться SourceCode ArrayList
MyPasswordIsLasercats

3
Відгадування продуктивності без орієнтирів працює лише у тривіальних випадках. Насправді, new Myclass[0]швидше: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S

Це вже неправдива відповідь станом на JDK6 +
Антон Антонов

28

Від перевірки JetBrains Intellij Idea:

Існує два стилі для перетворення колекції в масив: або за допомогою масиву попереднього розміру (наприклад, c.toArray (новий String [c.size ()]) )), або за допомогою порожнього масиву (наприклад, c.toArray (новий рядок [ 0]) .

У старих версіях Java рекомендується використовувати масив попереднього розміру, оскільки дзвінок для відображення, необхідний для створення масиву належного розміру, був досить повільним. Однак з пізніми оновленнями OpenJDK 6 цей виклик був інтенсифікований, що робить продуктивність пустої версії масиву такою ж, а іноді навіть кращою, порівняно з попередньо розмірною версією. Також передача попередньо розмірного масиву небезпечна для одночасного або синхронізованого збору, оскільки можлива перегонка даних між розміром і викликом toArray, що може призвести до додаткових нулів в кінці масиву, якщо колекція одночасно скоротилася під час операції.

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


Якщо все це скопійовано / цитується текст, чи можемо ми його відформатувати відповідно, а також надати посилання на джерело? Я фактично приїхав сюди через перевірку IntelliJ, і мені дуже цікаво посилання, щоб переглянути всі їхні перевірки та міркування, що їх стоять.
Тім Бют

3
Тут Ви можете перевірити тексти перевірок: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов


17

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


2
Оновлення «повторного використання порожнього масиву», оскільки це компроміс між читабельністю та потенційною продуктивністю, який заслуговує на увагу. Передача аргумент оголошений private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]не заважає повертається масив з будується шляхом відображення, але це запобігти додатковий масив будується кожен кожен раз.
Майкл Шепер

Machael вірно, якщо ви використовуєте масив нульової довжини , немає способу обійтись: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); що було б зайвим, якщо розмір буде> = фактичний розмір (JDK7)
Alex

Якщо ви можете навести посилання на тему "сучасні 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 оптимізують такий вид відображення приблизно з тією ж швидкістю, що і невідбивна версія.
Том Хотін - тайклін

Я думаю, що він дійсно використовує рефлексію. JDK 1.5.0_10 робить точно, і відображення - це єдиний спосіб, коли я знаю, щоб створити масив типу, який ви не знаєте під час компіляції.
Панайотис Коррос

Тоді один із прикладів вихідного коду (цей вище чи мій) застарів. На жаль, я не знайшов правильного номера під-версії для мого.
Георгій

1
Георгі, ваш код від JDK 1.6, і якщо ви побачите реалізацію методу Arrays.copyTo, ви побачите, що реалізація використовує відображення.
Панайотис Коррос

-1

Другий - незначно читабельний, але вдосконалення настільки мало, що цього не варто. Перший метод швидший, без недоліків під час виконання, тому я і використовую. Але я пишу це другим способом, тому що це швидше друкувати. Тоді мій IDE відзначає це як попередження і пропонує виправити це. За допомогою одного натискання клавіші він перетворює код з другого типу в перший.


-2

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

Також зауважте, що компілятор javac не проводить жодної оптимізації. У цей час усі оптимізації виконуються компіляторами JIT / HotSpot під час виконання. Я не знаю жодних оптимізацій навколо "toArray" у будь-яких JVM.

Отже, відповідь на ваше запитання значною мірою залежить від стилю, але задля послідовності має бути частиною будь-яких стандартів кодування, яких ви дотримуєтесь (документально чи документально).


OTOH, якщо стандарт повинен використовувати масив нульової довжини, то випадки, що відхиляються, означають, що продуктивність викликає занепокоєння.
Майкл Шепер

-5

зразок коду для цілого числа:

Integer[] arr = myList.toArray(new integer[0]);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.