.toArray (new MyClass [0]) czy .toArray (new MyClass [myList.size ()])?

176

Zakładając, że mam ArrayList

ArrayList<MyClass> myList;

I chcę zadzwonić do Array, czy istnieje powód, dla którego warto użyć wydajności

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

nad

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

?

Wolę drugi styl, ponieważ jest mniej szczegółowy i założyłem, że kompilator upewni się, że pusta tablica tak naprawdę nie zostanie utworzona, ale zastanawiałem się, czy to prawda.

Oczywiście w 99% przypadków nie robi to różnicy w taki czy inny sposób, ale chciałbym zachować spójny styl między moim normalnym kodem i zoptymalizowanymi wewnętrznymi pętlami ...

itsadok
źródło
6
Wygląda na to, że problem został rozwiązany w nowym poście na blogu Aleksey Shipilёv, Arrays of Wisdom of the Ancients !
glts
6
Z wpisu na blogu: „Podsumowanie: toArray (nowe T [0]) wydaje się szybsze, bezpieczniejsze i czystsze zgodnie z umową, dlatego powinno być teraz domyślnym wyborem”.
DavidS

Odpowiedzi:

109

Wbrew intuicji najszybsza wersja w Hotspot 8 to:

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

Przeprowadziłem mikro-test porównawczy przy użyciu jmh, wyniki i kod są poniżej, pokazując, że wersja z pustą tablicą konsekwentnie przewyższa wersję z predefiniowaną tablicą. Zwróć uwagę, że jeśli możesz ponownie użyć istniejącej tablicy o odpowiednim rozmiarze, wynik może być inny.

Wyniki testów porównawczych (wynik w mikrosekundach, mniejszy = lepszy):

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

W celach informacyjnych kod:

@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]);
  }
}

Podobne wyniki, pełną analizę i dyskusję znajdziesz w poście na blogu Arrays of Wisdom of the Ancients . Podsumowując: kompilator JVM i JIT zawiera kilka optymalizacji, które umożliwiają tanie tworzenie i inicjowanie nowej tablicy o prawidłowym rozmiarze, a tych optymalizacji nie można użyć, jeśli samodzielnie utworzysz tablicę.

asylias
źródło
2
Bardzo ciekawy komentarz. Dziwię się, że nikt tego nie skomentował. Wydaje mi się, że to dlatego, że zaprzecza innym odpowiedziom tutaj, jeśli chodzi o prędkość. Warto również zauważyć, że reputacja tych gości jest prawie wyższa niż wszystkie inne odpowiedzi łącznie.
Pimp Trizkit
Błądzę. Chciałbym również zobaczyć benchmarki MyClass[] arr = myList.stream().toArray(MyClass[]::new);... które, jak sądzę, byłyby wolniejsze. Chciałbym również zobaczyć testy porównawcze różnicy z deklaracją tablicy. Jak w różnicy między: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);a MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... czy nie powinno być żadnej różnicy? Myślę, że te dwa są problemem, który jest pozatoArray funkcjami. Ale hej! Nie sądziłem, że poznam inne skomplikowane różnice.
Pimp Trizkit
1
@PimpTrizkit Właśnie sprawdzone: użycie dodatkowej zmiennej nie robi różnicy zgodnie z oczekiwaniami, użycie strumienia zajmuje od 60% do 100% więcej czasu niż toArraybezpośrednie wywołanie (im mniejszy rozmiar, tym większy względny narzut)
assylias
Wow, to była szybka odpowiedź! Dzięki! Tak, podejrzewałem to. Konwersja do strumienia nie brzmiała wydajnie. Ale nigdy nie wiesz!
Pimp Trizkit
2
Ten sam wniosek został znaleziony tutaj: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019
122

Od ArrayList w Javie 5 tablica będzie już wypełniona, jeśli ma odpowiedni rozmiar (lub jest większa). w konsekwencji

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

utworzy jeden obiekt tablicy, wypełni go i zwróci do "arr". Z drugiej strony

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

utworzy dwie tablice. Drugi to tablica MyClass o długości 0. Jest więc tworzenie obiektu dla obiektu, który zostanie natychmiast wyrzucony. O ile sugeruje kod źródłowy, kompilator / JIT nie może zoptymalizować tego, aby nie został utworzony. Ponadto użycie obiektu o zerowej długości skutkuje rzutowaniem (rzutami) w ramach metody toArray () -.

Zobacz źródło 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;
}

Użyj pierwszej metody, aby utworzyć tylko jeden obiekt i uniknąć (niejawnych, ale kosztownych) rzutów.

Georgi
źródło
1
Dwa komentarze, które mogą kogoś zainteresować: 1) LinkedList.toArray (T [] a) jest jeszcze wolniejszy (używa refleksji: Array.newInstance) i bardziej złożony; 2) Z drugiej strony, w wydaniu JDK7 byłem bardzo zaskoczony, gdy dowiedziałem się, że zwykle boleśnie powolny Array.newInstance wykonuje prawie tak szybko, jak zwykle tworzenie tablic!
java.is.for.desktop
1
@ktaria size jest prywatnym członkiem ArrayList, określając **** niespodziankę **** rozmiar. Zobacz ArrayList SourceCode
MyPasswordIsLasercats
3
Zgadywanie wydajności bez testów porównawczych działa tylko w trywialnych przypadkach. Właściwie new Myclass[0]jest szybszy: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S
To już nie jest poprawna odpowiedź od JDK6 +
Антон Антонов
28

Z inspekcji JetBrains Intellij Idea:

Istnieją dwa style konwersji kolekcji na tablicę: albo przy użyciu tablicy o ustalonym rozmiarze (np. C.toArray (new String [c.size ()]) ), albo przy użyciu pustej tablicy (np. C.toArray (new String [ 0]) .

W starszych wersjach Javy zalecane było używanie tablic o wstępnie ustalonym rozmiarze, ponieważ wywołanie odbicia, które jest konieczne do utworzenia tablicy o odpowiednim rozmiarze, było dość powolne. Jednak od późnych aktualizacji OpenJDK 6 wywołanie to było zintensyfikowane, sprawiając, że wydajność wersji z pustą tablicą była taka sama, a czasem nawet lepsza, w porównaniu z wersją o wcześniejszym rozmiarze. Również przekazywanie tablicy o wstępnie ustalonym rozmiarze jest niebezpieczne dla zbierania współbieżnego lub zsynchronizowanego, ponieważ wyścig danych jest możliwy między rozmiarem a wywołaniem toArray, co może skutkować dodatkowymi wartościami zerowymi na końcu tablicy, jeśli kolekcja została jednocześnie zmniejszona podczas operacji.

Ta inspekcja pozwala na przestrzeganie jednolitego stylu: albo użycie pustej tablicy (co jest zalecane we współczesnej Javie), albo użycie tablicy o ustalonym rozmiarze (co może być szybsze w starszych wersjach Javy lub maszynach JVM nie opartych na HotSpot).

Антон Антонов
źródło
Jeśli to wszystko jest tekstem skopiowanym / zacytowanym, czy możemy go odpowiednio sformatować, a także podać link do źródła? Właściwie przyjechałem tutaj z powodu inspekcji IntelliJ i bardzo interesuje mnie link do wszystkich ich inspekcji i ich uzasadnienia.
Tim Büthe
3
Tutaj możesz sprawdzić teksty z inspekcji: github.com/JetBrains/intellij-community/tree/master/plugins/…
Антон Антонов
17

Nowoczesne maszyny JVM optymalizują w tym przypadku konstrukcję tablicy odblaskowej, więc różnica w wydajności jest niewielka. Dwukrotne nazwanie kolekcji w takim standardowym kodzie nie jest dobrym pomysłem, więc unikałbym pierwszej metody. Kolejną zaletą drugiej jest to, że działa ona z kolekcjami synchronizowanymi i współbieżnymi. Jeśli chcesz dokonać optymalizacji, użyj ponownie pustej tablicy (puste tablice są niezmienne i mogą być współużytkowane) lub użyj profilera (!).

Tom Hawtin - haczyk
źródło
2
Głosowanie za „ponownym użyciem pustej tablicy”, ponieważ jest to kompromis między czytelnością a potencjalną wydajnością, który warto rozważyć. Przechodząc argument uznane private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]nie zapobiega zwracana tablica z skonstruowany przez odbicie, ale nie zapobiega dodatkowa tablica jest co każdorazowo wykonana.
Michael Scheper
Machael ma rację, jeśli używasz tablicy o zerowej długości, nie da się tego obejść: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); co byłoby zbędne, gdyby rozmiar był> = currentSize (JDK7)
Alex
Jeśli możesz podać cytat dla „nowoczesnych maszyn JVM optymalizujących konstrukcję tablicy odblaskowej w tym przypadku”, z przyjemnością zagłosuję za tą odpowiedzią.
Tom Panning
Tutaj się uczę. Jeśli zamiast tego użyję: MyClass[] arr = myList.stream().toArray(MyClass[]::new);Czy to pomogłoby lub zaszkodziło w przypadku zsynchronizowanych i współbieżnych kolekcji. I dlaczego? Proszę.
Pimp Trizkit
3

toArray sprawdza, czy przekazana tablica ma właściwy rozmiar (czyli wystarczająco dużą, aby zmieścić elementy z Twojej listy), a jeśli tak, to używa. W konsekwencji, jeśli rozmiar tablicy pod warunkiem, że jest mniejszy niż wymagany, nowa tablica zostanie utworzona odruchowo.

W twoim przypadku tablica o rozmiarze zero jest niezmienna, więc można ją bezpiecznie podnieść do statycznej zmiennej końcowej, co może sprawić, że kod będzie trochę czystszy, co pozwoli uniknąć tworzenia tablicy przy każdym wywołaniu. Nowa tablica i tak zostanie utworzona wewnątrz metody, więc jest to optymalizacja czytelności.

Prawdopodobnie szybszą wersją jest przekazywanie tablicy o odpowiednim rozmiarze, ale jeśli nie możesz udowodnić, że ten kod stanowi wąskie gardło wydajności, preferuj czytelność od wydajności w czasie wykonywania, dopóki nie zostanie udowodnione, że jest inaczej.

Dave Cheney
źródło
2

Pierwszy przypadek jest bardziej skuteczny.

Dzieje się tak, ponieważ w drugim przypadku:

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

Środowisko wykonawcze faktycznie tworzy pustą tablicę (o rozmiarze zerowym), a następnie wewnątrz metody toArray tworzy kolejną tablicę, aby dopasować rzeczywiste dane. To tworzenie odbywa się za pomocą odbicia przy użyciu następującego kodu (pobranego z 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;
}

Korzystając z pierwszego formularza, unikasz tworzenia drugiej tablicy, a także unikasz kodu odbicia.

Panagiotis Korros
źródło
toArray () nie używa odbicia. Przynajmniej tak długo, jak nie liczysz „rzucania” do refleksji ;-).
Georgi
toArray (T []) tak. Musi utworzyć tablicę odpowiedniego typu. Nowoczesne maszyny JVM optymalizują ten rodzaj odbicia, aby był mniej więcej tak samo szybki, jak wersja nieodblaskowa.
Tom Hawtin - tackline
Myślę, że używa refleksji. JDK 1.5.0_10 z pewnością to robi, a odbicie jest jedynym znanym mi sposobem tworzenia tablicy typu, którego nie znasz w czasie kompilacji.
Panagiotis Korros
Wtedy jeden z jej przykładów kodu źródłowego (ten powyżej lub mój) jest nieaktualny. Niestety, nie znalazłem poprawnego numeru podwersji dla mojej.
Georgi
1
Georgi, twój kod pochodzi z JDK 1.6 i jeśli zobaczysz implementację metody Arrays.copyTo, zobaczysz, że implementacja używa odbicia.
Panagiotis Korros
-1

Drugi jest nieznacznie bardziej czytelny, ale poprawa jest tak mała, że ​​nie jest tego warta. Pierwsza metoda jest szybsza i nie ma żadnych wad w czasie wykonywania, więc właśnie tego używam. Ale piszę to w drugi sposób, ponieważ pisanie jest szybsze. Wtedy moje IDE oznacza to jako ostrzeżenie i proponuje naprawienie tego. Jednym naciśnięciem klawisza konwertuje kod z drugiego typu na pierwszy.

MiguelMunoz
źródło
-2

Użycie „toArray” z tablicą o odpowiednim rozmiarze będzie działać lepiej, ponieważ alternatywa utworzy najpierw tablicę o rozmiarze zerowym, a następnie tablicę o odpowiednim rozmiarze. Jednak, jak mówisz, różnica będzie prawdopodobnie nieistotna.

Należy również zauważyć, że kompilator javac nie przeprowadza żadnej optymalizacji. Obecnie wszystkie optymalizacje są wykonywane przez kompilatory JIT / HotSpot w czasie wykonywania. Nie znam żadnych optymalizacji dotyczących „toArray” w żadnej JVM.

Odpowiedź na twoje pytanie jest zatem w dużej mierze kwestią stylu, ale ze względu na spójność powinna stanowić część wszelkich standardów kodowania, których się przestrzegasz (udokumentowanych lub innych).

Matthew Murdoch
źródło
OTOH, jeśli standardem jest użycie tablicy o zerowej długości, to przypadki, które odbiegają, oznaczają, że problemem jest wydajność.
Michael Scheper
-5

przykładowy kod dla liczby całkowitej:

Integer[] arr = myList.toArray(new integer[0]);
Rasol
źródło