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


176

Supponendo che io abbia un ArrayList

ArrayList<MyClass> myList;

E voglio chiamare ad Array, c'è un motivo prestazionale da usare

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

al di sopra di

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

?

Preferisco il secondo stile, dal momento che è meno dettagliato, e ho ipotizzato che il compilatore si assicurerà che l'array vuoto non venga realmente creato, ma mi chiedevo se fosse vero.

Certo, nel 99% dei casi non fa differenza in un modo o nell'altro, ma mi piacerebbe mantenere uno stile coerente tra il mio codice normale e i miei loop interni ottimizzati ...


6
Sembra che la domanda sia stata ora risolta in un nuovo post sul blog di Aleksey Shipilёv, Arrays of Wisdom of the Ancients !
inizia il

6
Dal post del blog: "In conclusione: toArray (nuovo T [0]) sembra più veloce, più sicuro e contrattualmente più pulito, e quindi dovrebbe essere la scelta predefinita adesso".
DavidS,

Risposte:


109

Controintuitivamente, la versione più veloce, su Hotspot 8, è:

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

Ho eseguito un micro benchmark usando jmh i risultati e il codice sono sotto, dimostrando che la versione con un array vuoto supera costantemente la versione con un array preselezionato. Si noti che se è possibile riutilizzare un array esistente della dimensione corretta, il risultato potrebbe essere diverso.

Risultati benchmark (punteggio in microsecondi, più piccolo = migliore):

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

Per riferimento, il codice:

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

Puoi trovare risultati simili, analisi complete e discussioni nel post sul blog Arrays of Wisdom of the Ancients . Riassumendo: il compilatore JVM e JIT contiene diverse ottimizzazioni che gli consentono di creare e inizializzare a buon mercato un nuovo array di dimensioni corrette e tali ottimizzazioni non possono essere utilizzate se si crea l'array da soli.


2
Commento molto interessante Sono sorpreso che nessuno abbia commentato questo. Immagino sia perché contraddice le altre risposte qui, per quanto riguarda la velocità. Anche interessante notare che questa reputazione di ragazzi è quasi superiore a tutte le altre risposte (ers) messe insieme.
Pimp Trizkit,

Io divago. Mi piacerebbe anche vedere benchmark per MyClass[] arr = myList.stream().toArray(MyClass[]::new);.. che immagino sarebbero più lenti. Inoltre, vorrei vedere i benchmark per la differenza con la dichiarazione di array. Come nella differenza tra: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);e MyClass[] arr = myList.toArray(new MyClass[myList.size()]);... o non dovrebbe esserci alcuna differenza? Immagino che questi due siano un problema al di fuori degli toArrayavvenimenti delle funzioni. Ma hey! Non pensavo che avrei imparato a conoscere le altre intricate differenze.
Pimp Trizkit,

1
@PimpTrizkit Appena verificato: l'utilizzo di una variabile aggiuntiva non fa alcuna differenza come previsto, l'utilizzo di uno stream richiede tra il 60% e il 100% di tempo in più rispetto alla chiamata toArraydiretta (più piccola è la dimensione, maggiore è il relativo sovraccarico)
assylias

Caspita, è stata una risposta veloce! Grazie! Sì, lo sospettavo. La conversione in stream non sembrava efficiente. Ma non lo sai mai!
Pimp Trizkit,

2
Questa stessa conclusione è stata trovata qui: shipilev.net/blog/2016/arrays-wisdom-ancients
user167019

122

A partire da ArrayList in Java 5 , l'array verrà riempito già se ha le dimensioni giuste (o è più grande). conseguentemente

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

creerà un oggetto array, lo riempirà e lo restituirà a "arr". D'altro canto

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

creerà due array. Il secondo è un array di MyClass con lunghezza 0. Quindi c'è una creazione di oggetto per un oggetto che verrà immediatamente gettato via. Nella misura in cui il codice sorgente suggerisce che il compilatore / JIT non può ottimizzare questo in modo che non venga creato. Inoltre, l'uso dell'oggetto di lunghezza zero comporta il cast (s) all'interno del metodo toArray ().

Vedi l'origine di 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;
}

Utilizzare il primo metodo in modo tale da creare un solo oggetto ed evitare lanci (impliciti ma comunque costosi).


1
Due commenti, potrebbero essere di interesse per qualcuno: 1) LinkedList.toArray (T [] a) è ancora più lento (usa la riflessione: Array.newInstance) e più complesso; 2) D'altra parte, nella versione JDK7, sono stato molto sorpreso di scoprire che Array.newInstance, solitamente dolorosamente lento, si comporta quasi alla stessa velocità della creazione di array!
java.is.for.desktop,

1
@ktaria size è un membro privato di ArrayList, specificando **** sorpresa **** la dimensione. Vedi ArrayList SourceCode
MyPasswordIsLasercats

3
Indovinare le prestazioni senza benchmark funziona solo in casi banali. In realtà, new Myclass[0]è più veloce: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S

Questa non è più una risposta valida a partire da JDK6 +
Антон Антонов

28

Dall'ispezione di JetBrains Intellij Idea:

Esistono due stili per convertire una raccolta in un array: utilizzare un array pre-dimensionato (come c.toArray (new String [c.size ()]) ) o usare un array vuoto (come c.toArray (new String [ 0]) .

Nelle versioni precedenti di Java era consigliato l'uso di un array di dimensioni ridotte, poiché la chiamata di riflessione necessaria per creare un array di dimensioni adeguate era piuttosto lenta. Tuttavia, a partire dagli ultimi aggiornamenti di OpenJDK 6, questa chiamata è stata intrinseca, rendendo le prestazioni della versione dell'array vuota uguali e talvolta persino migliori, rispetto alla versione pre-dimensionata. Anche il passaggio di array pre-dimensionati è pericoloso per una raccolta simultanea o sincronizzata in quanto è possibile una corsa di dati tra la dimensione e la chiamata all'array che può comportare nulli aggiuntivi alla fine dell'array, se la raccolta è stata contemporaneamente ridotta durante l'operazione.

Questa ispezione consente di seguire lo stile uniforme: utilizzando un array vuoto (consigliato nella moderna Java) o utilizzando un array pre-dimensionato (che potrebbe essere più veloce nelle versioni Java precedenti o nelle JVM non basate su HotSpot).


Se tutto questo è testo copiato / citato, possiamo formattarlo di conseguenza e fornire anche un link alla fonte? In realtà sono venuto qui a causa dell'ispezione IntelliJ e sono molto interessato al link per cercare tutte le loro ispezioni e il ragionamento dietro di loro.
Tim Büthe



17

Le moderne JVM ottimizzano la costruzione di array riflettenti in questo caso, quindi la differenza di prestazioni è minima. Dare un nome alla raccolta due volte in un codice di questo tipo non è un'ottima idea, quindi eviterei il primo metodo. Un altro vantaggio del secondo è che funziona con raccolte sincronizzate e simultanee. Se si desidera effettuare l'ottimizzazione, riutilizzare l'array vuoto (gli array vuoti sono immutabili e possono essere condivisi) oppure utilizzare un profiler (!).


2
L'upgrade di "riutilizzare l'array vuoto", perché è un compromesso tra leggibilità e prestazioni potenziali che è degno di considerazione. Passando un argomento dichiarato private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]non impedisce la matrice restituita venga costruita per riflessione, ma non impedisce un array addizionale essendo costruito ogni ogni volta.
Michael Scheper,

Machael ha ragione, se usi un array di lunghezza zero non c'è modo di aggirare: (T []) java.lang.reflect.Array.newInstance (a.getClass (). GetComponentType (), size); che sarebbe superfluo se la dimensione fosse> = actualSize (JDK7)
Alex

Se puoi dare una citazione per "le moderne JVM ottimizzano la costruzione di array riflettenti in questo caso", voterò volentieri questa risposta.
Tom Panning,

Sto imparando qui. Se invece io uso: MyClass[] arr = myList.stream().toArray(MyClass[]::new);aiuterebbe o danneggerebbe le raccolte sincronizzate e simultanee. E perché? Per favore.
Pimp Trizkit

3

toArray verifica che l'array passato abbia le dimensioni giuste (ovvero abbastanza grande da contenere gli elementi dell'elenco) e, in tal caso, lo utilizza. Di conseguenza, se la dimensione dell'array lo ha fornito più piccolo del necessario, verrà creato in modo riflessivo un nuovo array.

Nel tuo caso, un array di dimensioni zero è immutabile, quindi potrebbe tranquillamente essere elevato a una variabile finale statica, che potrebbe rendere il tuo codice un po 'più pulito, evitando così di creare l'array su ogni invocazione. Un nuovo array verrà comunque creato all'interno del metodo, quindi è un'ottimizzazione della leggibilità.

Probabilmente la versione più veloce è quella di passare l'array di dimensioni corrette, ma a meno che non sia possibile dimostrare che questo codice è un collo di bottiglia delle prestazioni, preferire la leggibilità alle prestazioni di runtime fino a prova contraria.


2

Il primo caso è più efficiente.

Questo perché nel secondo caso:

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

il runtime in realtà crea un array vuoto (con dimensione zero) e quindi all'interno del metodo toArray crea un altro array per adattarsi ai dati effettivi. Questa creazione viene eseguita usando reflection utilizzando il seguente codice (tratto da 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;
}

Utilizzando il primo modulo, si evita la creazione di un secondo array e si evita anche il codice di riflessione.


toArray () non usa la riflessione. Almeno fino a quando non conti il ​​"casting" per la riflessione, comunque ;-).
Georgi,

toArray (T []) lo fa. Deve creare una matrice del tipo appropriato. Le moderne JVM ottimizzano quel tipo di riflessione alla stessa velocità della versione non riflettente.
Tom Hawtin - tackline

Penso che usi la riflessione. JDK 1.5.0_10 lo fa di sicuro e la riflessione è l'unico modo che conosco per creare un array di un tipo che non conosci al momento della compilazione.
Panagiotis Korros,

Quindi uno dei codici sorgente di lei (quello sopra o il mio) non è aggiornato. Purtroppo, però, non ho trovato un numero di sotto-versione corretto per il mio.
Georgi,

1
Georgi, il tuo codice proviene da JDK 1.6 e se vedi l'implementazione del metodo Arrays.copyTo vedrai che l'implementazione usa la riflessione.
Panagiotis Korros,

-1

Il secondo è leggermente leggibile, ma ci sono così pochi miglioramenti che non ne vale la pena. Il primo metodo è più veloce, senza svantaggi in fase di esecuzione, quindi è quello che uso. Ma lo scrivo nel secondo modo, perché è più veloce da scrivere. Quindi il mio IDE lo segnala come un avvertimento e si offre di risolverlo. Con un solo tasto, converte il codice dal secondo tipo al primo.


-2

L'uso di "toArray" con un array della dimensione corretta funzionerà meglio poiché l'alternativa creerà prima l'array di dimensioni zero, quindi l'array della dimensione corretta. Tuttavia, come dici tu, è probabile che la differenza sia trascurabile.

Inoltre, si noti che il compilatore javac non esegue alcuna ottimizzazione. In questi giorni tutte le ottimizzazioni vengono eseguite dai compilatori JIT / HotSpot in fase di esecuzione. Non sono a conoscenza di alcuna ottimizzazione relativa a "toArray" in qualsiasi JVM.

La risposta alla tua domanda, quindi, è in gran parte una questione di stile, ma per motivi di coerenza dovrebbe far parte di tutti gli standard di codifica a cui aderisci (sia documentato o meno).


OTOH, se lo standard deve utilizzare un array di lunghezza zero, i casi che si discostano implicano che le prestazioni sono un problema.
Michael Scheper,

-5

codice di esempio per intero:

Integer[] arr = myList.toArray(new integer[0]);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.