Java 8: performances des flux vs Collections


Je suis nouveau sur Java 8. Je ne connais toujours pas l'API en profondeur, mais j'ai fait un petit benchmark informel pour comparer les performances de la nouvelle API Streams par rapport aux bonnes vieilles Collections.

Le test consiste à filtrer une liste de Integer, et pour chaque nombre pair, calculer la racine carrée et le stocker dans une suite List de Double.

Voici le code:

    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));      
    }.

Et voici les résultats pour une machine 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)

Pour ce test particulier, les flux sont deux fois plus lent que les collections, et le parallélisme n'aide pas (ou je l'utilise dans le mauvais sens?).

Questions:

  • Ce test est-il juste? Ai-je fait une erreur?
  • Les flux sont-ils plus lents que les collections? Quelqu'un a-t-il fait une bonne référence formelle à ce sujet?
  • Quelle approche devrais-je viser?

Résultats mis à jour.

J'ai exécuté le test 1k fois après l'échauffement de la JVM (1k itérations) comme conseillé par @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)

Dans ce cas, les flux sont plus performants. Je me demande ce qui serait observé dans une application où la fonction de filtrage n'est appelée qu'une ou deux fois pendant l'exécution.

Author: Stuart Marks, 2014-03-26

4 answers

  1. Arrêtez d'utiliser LinkedList pour tout sauf une suppression lourde du milieu de la liste en utilisant iterator.

  2. Arrêtez d'écrire du code d'analyse comparative à la main, utilisez JMH.

Repères appropriés:

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

Résultat:

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

Tout comme je m'y attendais, l'implémentation du flux est assez lente. JIT est capable d'intégrer toutes les choses lambda mais ne produit pas de code aussi parfaitement concis que la version vanilla.

Généralement, Java 8 streams n'est pas une magie. Ils impossible d'accélérer les choses déjà bien implémentées (avec, probablement, des itérations simples ou des instructions for-each de Java 5 remplacées par des appels Iterable.forEach() et Collection.removeIf()). Les flux sont davantage axés sur la commodité et la sécurité du codage. Commodité-le compromis de vitesse fonctionne ici.

 142
Author: leventov, 2016-12-02 23:44:53

1) Vous voyez le temps inférieur à 1 seconde en utilisant votre benchmark. Cela signifie qu'il peut y avoir une forte influence des effets secondaires sur vos résultats. Donc, j'ai augmenté votre tâche 10 fois

    int max = 10000000;

Et a exécuté votre benchmark. Mes résultats:

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)

Sans modifier (int max = 1000000) les résultats étaient

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)

C'est comme vos résultats: stream est plus lent que collection. Conclusion: beaucoup de temps ont été consacrés à l'initialisation du flux/à la transmission des valeurs.

2) Après avoir augmenté le flux de tâches est devenu plus rapide (c'est OK), mais le flux parallèle est resté trop lent. Quel est le problème? Remarque: vous avez collect(Collectors.toList()) dans votre commande. La collecte vers une collecte unique introduit essentiellement un goulot d'étranglement et une surcharge de performances en cas d'exécution simultanée. Il est possible d'estimer le coût relatif des frais généraux en remplaçant

collecting to collection -> counting the element count

Pour les flux, cela peut être fait par collect(Collectors.counting()). J'ai obtenu des résultats:

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)

C'est pour une grosse tâche! (int max = 10000000) Conclusion: collecte des éléments à la collecte a pris la majorité du temps. La partie la plus lente est l'ajout à la liste. BTW, simple ArrayList est utilisé pour Collectors.toList().

 11
Author: Sergey Fedorov, 2016-07-08 16:26:18
    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);
}

Je change un peu le code, j'ai couru sur mon mac book pro qui a 8 cœurs, j'ai obtenu un résultat raisonnable:

Collections: Temps écoulé: 1522036826 ns (1.522037 secondes)

Flux: Temps écoulé: 4315833719 ns (4.315834 secondes)

Flux parallèles: Temps écoulé: 261152901 ns (0.261153 secondes)

 3
Author: Mellon, 2015-11-19 19:31:12

Pour ce que vous essayez de faire, je n'utiliserais pas les api java régulières de toute façon. Il y a une tonne de boxe/unboxing en cours, donc il y a une énorme surcharge de performance.

Personnellement, je pense que beaucoup d'API conçues sont de la merde parce qu'elles créent beaucoup de litière d'objets.

Essayez d'utiliser des tableaux primitifs de double/int et essayez de le faire single threaded et voyez quelles sont les performances.

PS: Vous voudrez peut-être jeter un oeil à JMH pour prendre soin de faire le benchmark. Il faut attention à certains des pièges typiques comme réchauffer la JVM.

 1
Author: pveentjer, 2014-03-26 10:41:59