Java 8: prestazioni di stream vs collezioni


140

Sono nuovo di Java 8. Non conosco ancora in modo approfondito l'API, ma ho fatto un piccolo benchmark informale per confrontare le prestazioni della nuova API Streams rispetto alle vecchie vecchie Collezioni.

Il test consiste nel filtrare un elenco di Integer, e per ogni numero pari, calcolare la radice quadrata e memorizzarla in un risultato Listdi 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<Integer> sourceList = new ArrayList<>();
        for (int i = min; i < max; i++) {
            sourceList.add(i);
        }

        List<Double> 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<Integer> 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 due volte più lenti delle raccolte e il parallelismo non aiuta (o 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 lottare?

Risultati aggiornati.

Ho eseguito il test 1k volte dopo il riscaldamento JVM (iterazioni 1k) 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 cosa verrebbe osservato in un'app in cui la funzione di filtro viene chiamata solo una o due volte durante il runtime.


1
l'hai provato con un IntStreaminvece?
Mark Rotteveel,

2
Puoi per favore misurare correttamente? Se tutto ciò che stai facendo è una corsa, i tuoi benchmark saranno ovviamente spenti.
Skiwi,

2
@MisterSmith Possiamo avere un po 'di trasparenza su come hai riscaldato la tua JVM, anche con i test 1K?
Skiwi,

1
E per coloro che sono interessati a scrivere microbenchmark corretti, ecco la domanda: stackoverflow.com/questions/504103/…
Mister Smith,

2
@assylias L'utilizzo toListdeve essere eseguito in parallelo anche se viene raccolto in un elenco non thread-safe, poiché i diversi thread verranno raccolti in elenchi intermedi limitati al thread prima di essere uniti.
Stuart segna il

Risposte:


192
  1. Smetti di usarlo LinkedListper tutto tranne che per rimuoverlo pesantemente dalla metà dell'elenco usando iteratore.

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

Parametri corretti:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> 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<Double> 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 flusso è abbastanza più lenta. JIT è in grado di incorporare tutte le cose lambda ma non produce un codice perfettamente conciso come la versione vaniglia.

Generalmente, i flussi Java 8 non sono magici. Non sono stati in grado di velocizzare cose già ben implementate (con, probabilmente, semplici iterazioni o istruzioni for-Java Java 5 sostituite con Iterable.forEach()e Collection.removeIf()chiamate). Gli stream riguardano più la convenienza e la sicurezza della codifica. Convenienza - il trade-off di velocità sta funzionando qui.


2
Grazie per aver dedicato del tempo alla panchina. Non penso che cambiare LinkedList per ArrayList cambierebbe nulla, poiché entrambi i test dovrebbero aggiungersi ad esso, i tempi non dovrebbero essere influenzati. Ad ogni modo, potresti spiegare i risultati per favore? È difficile dire cosa stai misurando qui (le unità dicono ns / op, ma cosa è considerato un op?).
Mister Smith,

52
Le tue conclusioni sulle prestazioni, sebbene valide, sono esagerate. Ci sono molti casi in cui il codice stream è più veloce del codice iterativo, soprattutto perché i costi di accesso per elemento sono più economici con i flussi che con i semplici iteratori. E in molti casi, la versione degli stream è in linea con qualcosa di equivalente alla versione scritta a mano. Certo, il diavolo è nei dettagli; qualsiasi dato bit di codice potrebbe comportarsi diversamente.
Brian Goetz,

26
@BrianGoetz, potresti specificare casi d'uso, quando i flussi sono più veloci?
Alexandr,

1
Nell'ultima versione di FMH: usa @Benchmarkinvece di@GenerateMicroBenchmark
pdem

3
@BrianGoetz, potresti specificare casi d'uso, quando gli stream sono più veloci?
Kiltek,

17

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

    int max = 10_000_000;

e ha eseguito il tuo benchmark. 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 i int max = 1_000_000risultati di edit ( ) erano

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: il flusso è più lento della raccolta. Conclusione: molto tempo è stato impiegato per l'inizializzazione del flusso / la trasmissione dei valori.

2) Dopo aver aumentato il flusso di attività è diventato più veloce (va bene), ma il flusso parallelo è rimasto troppo lento. Cosa c'è che non va? Nota: hai collect(Collectors.toList())nel tuo comando. La raccolta in un'unica raccolta introduce essenzialmente colli di bottiglia delle prestazioni e spese generali in caso di esecuzione simultanea. È possibile stimare il costo relativo delle spese generali 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 da collezionare ha richiesto la maggior parte del tempo. La parte più lenta viene aggiunta all'elenco. A proposito, semplice ArrayListè usato per Collectors.toList().


È necessario contrassegnare con microbench questo test, il che significa che dovrebbe essere prima riscaldato molte volte, quindi eseguito un sacco di temi e una media.
Skiwi,

@skiwi certo, hai ragione, soprattutto perché ci sono grandi deviazioni nelle misurazioni. Ho fatto solo indagini di base e non pretendo risultati precisi.
Sergey Fedorov,

JIT in modalità server, entra in funzione dopo 10k esecuzioni. E poi ci vuole del tempo per compilare il codice e scambiarlo.
pveentjer,

A proposito di questa frase: " hai collect(Collectors.toList())nel tuo comando, cioè potrebbe esserci una situazione in cui devi indirizzare la raccolta singola con molti thread. " Sono quasi sicuro che toListraccoglie in parallelo diverse istanze di lista. Solo come ultimo passo nella raccolta gli elementi vengono trasferiti in un elenco e quindi restituiti. Quindi non ci dovrebbe essere sovraccarico di sincronizzazione. Ecco perché i collezionisti hanno sia un fornitore, un accumulatore che una funzione combinatrice. (Potrebbe essere lento per altri motivi, ovviamente.)
Lii

@Lii Penso allo stesso modo collectsull'implementazione qui. Ma alla fine diverse liste dovrebbero essere unite in una sola, e sembra che la fusione sia l'operazione più pesante in un dato esempio.
Sergey Fedorov,

4
    public static void main(String[] args) {
    //Calculating square root of even numbers from 1 to N       
    int min = 1;
    int max = 10000000;

    List<Integer> sourceList = new ArrayList<>();
    for (int i = min; i < max; i++) {
        sourceList.add(i);
    }

    List<Double> 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<Integer> 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 un po 'il codice, ho eseguito sul mio mac book pro che ha 8 core, ho ottenuto un risultato ragionevole:

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

Flussi: tempo trascorso: 4315833719 ns (4.315834 secondi)

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


Penso che il tuo test sia corretto, hai solo bisogno di una macchina con più core di CPU.
Mellon,

3

Per quello che stai cercando di fare, non userei comunque i normali java api. C'è un sacco di boxe / unboxing in corso, quindi c'è un enorme sovraccarico di prestazioni.

Personalmente penso che molte API progettate siano una schifezza perché creano molta lettiera.

Prova a usare una matrice primitiva di double / int e prova a farlo a thread singolo e guarda qual è la performance.

PS: potresti dare un'occhiata a JMH per occuparti del benchmark. Si occupa di alcune delle insidie ​​tipiche come il riscaldamento della JVM.


Le liste collegate sono persino peggiori delle liste array perché è necessario creare tutti gli oggetti nodo. L'operatore mod è anche cane lento. Credo che qualcosa come 10/15 cicli + svuota la pipeline di istruzioni. Se vuoi fare una divisione molto veloce per 2, sposta il numero 1 a destra. Questi sono trucchi di base, ma sono sicuro che ci sono trucchi avanzati in modalità per accelerare le cose, ma questi probabilmente sono più specifici del problema.
pveentjer,

Sono a conoscenza del pugilato. Questo è solo un punto di riferimento informale. L'idea è quella di avere la stessa quantità di boxing / unboxing sia nelle raccolte che nei test dei flussi.
Mister Smith,

Per prima cosa mi assicurerei che non stia misurando l'errore. Prova a eseguire il benchmark alcune volte prima di eseguire il benchmark reale. Quindi almeno il riscaldamento JVM è fuori mano e il codice è JITTED correttamente. Senza questo, probabilmente fai delle conclusioni sbagliate.
pveentjer,

Ok, posterò nuovi risultati seguendo i tuoi consigli. Ho dato un'occhiata a JMH ma richiede Maven e ci vuole del tempo per la configurazione. Grazie comunque.
Mister Smith,

Penso che sia meglio evitare di pensare ai test di riferimento in termini di "Per quello che stai cercando di fare". cioè, di solito questi tipi di esercizi sono abbastanza semplificati da essere dimostrabili, ma abbastanza complessi da sembrare che possano / debbano essere semplificati.
ryvantage il
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.