Ecco un'altra tecnica che ho incontrato l'altro giorno:
Collections.nCopies(8, 1)
.stream()
.forEach(i -> System.out.println(i));
La Collections.nCopieschiamata crea una copia Listcontenente nqualsiasi valore fornito. In questo caso è il Integervalore boxed 1. Ovviamente in realtà non crea una lista con nelementi; crea un elenco "virtualizzato" che contiene solo il valore e la lunghezza, e qualsiasi chiamata a getall'interno dell'intervallo restituisce semplicemente il valore. Il nCopiesmetodo esiste da quando il framework delle raccolte è stato introdotto in JDK 1.2. Ovviamente, la possibilità di creare un flusso dal suo risultato è stata aggiunta in Java SE 8.
Grande affare, un altro modo per fare la stessa cosa in circa lo stesso numero di righe.
Tuttavia, questa tecnica è più veloce dell'approccio IntStream.generatee IntStream.iteratee, sorprendentemente, è anche più veloce IntStream.rangedell'approccio.
Perché iteratee generateil risultato forse non è troppo sorprendente. Il framework dei flussi (in realtà, gli Spliterator per questi flussi) si basa sul presupposto che i lambda genereranno potenzialmente valori diversi ogni volta e che genereranno un numero illimitato di risultati. Ciò rende la divisione parallela particolarmente difficile. Il iteratemetodo è problematico anche in questo caso perché ogni chiamata richiede il risultato di quella precedente. Quindi i flussi che utilizzano generatee iteratenon funzionano molto bene per la generazione di costanti ripetute.
La prestazione relativamente scarsa di rangeè sorprendente. Anche questo è virtualizzato, quindi gli elementi in realtà non esistono tutti in memoria e la dimensione è nota in anticipo. Questo dovrebbe creare uno spliterator veloce e facilmente parallelizzabile. Ma sorprendentemente non è andata molto bene. Forse il motivo è che rangedeve calcolare un valore per ogni elemento dell'intervallo e quindi chiamare una funzione su di esso. Ma questa funzione ignora semplicemente il suo input e restituisce una costante, quindi sono sorpreso che non sia inline e ucciso.
La Collections.nCopiestecnica deve fare boxing / unboxing per poter gestire i valori, poiché non ci sono specializzazioni primitive di List. Poiché il valore è lo stesso ogni volta, è fondamentalmente inscatolato una volta e quella casella è condivisa da tutte le ncopie. Sospetto che il pugilato / unboxing sia altamente ottimizzato, persino intrinseco e possa essere integrato bene.
Ecco il codice:
public static final int LIMIT = 500_000_000;
public static final long VALUE = 3L;
public long range() {
return
LongStream.range(0, LIMIT)
.parallel()
.map(i -> VALUE)
.map(i -> i % 73 % 13)
.sum();
}
public long ncopies() {
return
Collections.nCopies(LIMIT, VALUE)
.parallelStream()
.mapToLong(i -> i)
.map(i -> i % 73 % 13)
.sum();
}
Ed ecco i risultati JMH: (2.8GHz Core2Duo)
Benchmark Mode Samples Mean Mean error Units
c.s.q.SO18532488.ncopies thrpt 5 7.547 2.904 ops/s
c.s.q.SO18532488.range thrpt 5 0.317 0.064 ops/s
C'è una discreta quantità di varianza nella versione ncopies, ma nel complesso sembra comodamente 20 volte più veloce della versione range. (Sarei abbastanza disposto a credere di aver fatto qualcosa di sbagliato, però.)
Sono sorpreso di quanto bene nCopiesfunzioni la tecnica. Internamente non fa molto speciale, con il flusso dell'elenco virtualizzato che viene semplicemente implementato usando IntStream.range! Mi aspettavo che sarebbe stato necessario creare uno spliteratore specializzato per farlo funzionare velocemente, ma sembra già essere abbastanza buono.