Ecco un'altra tecnica che ho incontrato l'altro giorno:
Collections.nCopies(8, 1)
.stream()
.forEach(i -> System.out.println(i));
La Collections.nCopies
chiamata crea una copia List
contenente n
qualsiasi valore fornito. In questo caso è il Integer
valore boxed 1. Ovviamente in realtà non crea una lista con n
elementi; crea un elenco "virtualizzato" che contiene solo il valore e la lunghezza, e qualsiasi chiamata a get
all'interno dell'intervallo restituisce semplicemente il valore. Il nCopies
metodo 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.generate
e IntStream.iterate
e, sorprendentemente, è anche più veloce IntStream.range
dell'approccio.
Perché iterate
e generate
il 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 iterate
metodo è problematico anche in questo caso perché ogni chiamata richiede il risultato di quella precedente. Quindi i flussi che utilizzano generate
e iterate
non 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 range
deve 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.nCopies
tecnica 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 n
copie. 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 nCopies
funzioni 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.