Java 8: prestazioni degli Stream vs Collections

Sono nuovo di Java 8. Ancora non conosco l’API in profondità, ma ho realizzato un piccolo benchmark informale per confrontare le prestazioni della nuova API di Streams rispetto alle vecchie collezioni.

Il test consiste nel filtrare un elenco di Integer e, per ciascun numero pari, calcolare la radice quadrata e memorizzarla in un List di risultati del Double .

Ecco il codice:

  public static void main(String[] args) { //Calculating square root of even numbers from 1 to N int min = 1; int max = 1000000; List sourceList = new ArrayList(); for (int i = min; i < max; i++) { sourceList.add(i); } List result = new LinkedList(); //Collections approach long t0 = System.nanoTime(); long elapsed = 0; for (Integer i : sourceList) { if(i % 2 == 0){ result.add(Math.sqrt(i)); } } elapsed = System.nanoTime() - t0; System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Stream approach Stream stream = sourceList.stream(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Parallel stream approach stream = sourceList.stream().parallel(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); }. 

E qui ci sono i risultati per una macchina dual core:

  Collections: Elapsed time: 94338247 ns (0,094338 seconds) Streams: Elapsed time: 201112924 ns (0,201113 seconds) Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds) 

Per questo particolare test, i flussi sono circa il doppio di quelli delle raccolte e il parallelismo non aiuta (o lo sto usando nel modo sbagliato?).

Domande:

  • Questo test è giusto? Ho fatto qualche errore?
  • I flussi sono più lenti delle raccolte? Qualcuno ha fatto un buon punto di riferimento formale su questo?
  • Quale approccio dovrei cercare?

Risultati aggiornati

Ho eseguito il test 1k volte dopo il riscaldamento JVM (1k iterazioni) come consigliato da @pveentjer:

  Collections: Average time: 206884437,000000 ns (0,206884 seconds) Streams: Average time: 98366725,000000 ns (0,098367 seconds) Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds) 

In questo caso i flussi sono più performanti. Mi chiedo che cosa si osserverebbe in un’app in cui la funzione di filtro viene richiamata solo una o due volte durante il runtime.

  1. Smetti di usare LinkedList per tutto tranne la rimozione pesante dal centro della lista usando iteratore.

  2. Smetti di scrivere codice di benchmarking a mano, usa JMH .

Parametri di riferimento adeguati:

 @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) @OperationsPerInvocation(StreamVsVanilla.N) public class StreamVsVanilla { public static final int N = 10000; static List sourceList = new ArrayList<>(); static { for (int i = 0; i < N; i++) { sourceList.add(i); } } @Benchmark public List vanilla() { List result = new ArrayList<>(sourceList.size() / 2 + 1); for (Integer i : sourceList) { if (i % 2 == 0){ result.add(Math.sqrt(i)); } } return result; } @Benchmark public List stream() { return sourceList.stream() .filter(i -> i % 2 == 0) .map(Math::sqrt) .collect(Collectors.toCollection( () -> new ArrayList<>(sourceList.size() / 2 + 1))); } } 

Risultato:

 Benchmark Mode Samples Mean Mean error Units StreamVsVanilla.stream avgt 10 17.588 0.230 ns/op StreamVsVanilla.vanilla avgt 10 10.796 0.063 ns/op 

Proprio come mi aspettavo l’implementazione del stream è più lenta. JIT è in grado di allineare tutte le cose lambda ma non produce un codice perfettamente conciso come la versione vaniglia.

Generalmente, i flussi Java 8 non sono una magia. Non potevano accelerare le cose già ben implementate (con, probabilmente, semplici iterazioni o Java 5 per-ogni dichiarazione sostituita con Iterable.forEach() e Collection.removeIf() ). Gli stream riguardano più la codifica della praticità e della sicurezza. Convenienza – il compromesso sulla velocità funziona qui.

1) Puoi vedere il tempo in meno di 1 secondo usando il tuo benchmark. Ciò significa che ci può essere una forte influenza degli effetti collaterali sui risultati. Quindi, ho aumentato il tuo compito 10 volte

  int max = 10000000; 

e ha gestito il tuo punto di riferimento. I miei risultati:

 Collections: Elapsed time: 8592999350 ns (8.592999 seconds) Streams: Elapsed time: 2068208058 ns (2.068208 seconds) Parallel streams: Elapsed time: 7186967071 ns (7.186967 seconds) 

senza edit ( int max = 1000000 ) risultati

 Collections: Elapsed time: 113373057 ns (0.113373 seconds) Streams: Elapsed time: 135570440 ns (0.135570 seconds) Parallel streams: Elapsed time: 104091980 ns (0.104092 seconds) 

È come i tuoi risultati: lo streaming è più lento della raccolta. Conclusione: sono stati spesi molto tempo per l’inizializzazione del stream / trasmissione dei valori.

2) Dopo aver aumentato il stream di attività è diventato più veloce (che è OK), ma il stream parallelo è rimasto troppo lento. Cosa c’è che non va? Nota: hai collect(Collectors.toList()) nel tuo comando. La raccolta su singola raccolta introduce essenzialmente il collo di bottiglia delle prestazioni e il sovraccarico in caso di esecuzione simultanea. È ansible stimare il costo relativo del sovraccarico sostituendolo

 collecting to collection -> counting the element count 

Per i flussi può essere fatto da collect(Collectors.counting()) . Ho ottenuto risultati:

 Collections: Elapsed time: 41856183 ns (0.041856 seconds) Streams: Elapsed time: 546590322 ns (0.546590 seconds) Parallel streams: Elapsed time: 1540051478 ns (1.540051 seconds) 

Questo è per un grande compito! ( int max = 10000000 ) Conclusione: la raccolta degli articoli per la raccolta ha richiesto la maggior parte del tempo. La parte più lenta è aggiunta alla lista. A proposito, semplice ArrayList viene utilizzato per Collectors.toList() .

  public static void main(String[] args) { //Calculating square root of even numbers from 1 to N int min = 1; int max = 10000000; List sourceList = new ArrayList<>(); for (int i = min; i < max; i++) { sourceList.add(i); } List result = new LinkedList<>(); //Collections approach long t0 = System.nanoTime(); long elapsed = 0; for (Integer i : sourceList) { if(i % 2 == 0){ result.add( doSomeCalculate(i)); } } elapsed = System.nanoTime() - t0; System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Stream approach Stream stream = sourceList.stream(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i)) .collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); //Parallel stream approach stream = sourceList.stream().parallel(); t0 = System.nanoTime(); result = stream.filter(i -> i%2 == 0).map(i -> doSomeCalculate(i)) .collect(Collectors.toList()); elapsed = System.nanoTime() - t0; System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9)); } static double doSomeCalculate(int input) { for(int i=0; i<100000; i++){ Math.sqrt(i+input); } return Math.sqrt(input); } 

Cambio leggermente il codice, ho eseguito il mio Mac Book Pro che ha 8 core, ho ottenuto un risultato ragionevole:

Collezioni: tempo trascorso: 1522036826 ns (1,522037 secondi)

Streams: tempo trascorso: 4315833719 ns (4,315834 secondi)

Stream paralleli: tempo trascorso: 261152901 ns (0,261153 secondi)

Per quello che stai cercando di fare, non userei comunque le normali API di Java. C’è un sacco di boxe / unboxing in corso, quindi c’è un enorme sovraccarico di prestazioni.

Personalmente penso che molte API progettate siano schifose perché creano molti rifiuti di oggetti.

Prova ad usare un array primitivo di double / int e prova a farlo con un singolo thread e vedi qual è la performance.

PS: potresti voler dare un’occhiata a JMH per occuparsi del benchmark. Si prende cura di alcune delle insidie ​​tipiche come il riscaldamento della JVM.