web-dev-qa-db-fra.com

Java 8: performances des flux par rapport aux collections

Je suis nouveau dans Java 8. Je ne connais toujours pas l'API en profondeur, mais j'ai créé un petit repère 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 à la stocker dans un résultat 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 environ deux fois plus lents que les collections et le parallélisme n’aide en rien (ou bien je ne l’utilise pas correctement?).

Des questions:

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

Résultats mis à jour.

J'ai exécuté le test 1k fois après l'échauffement de la machine virtuelle Java (itérations de 1k) 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.

121
Mister Smith
  1. Arrêtez d'utiliser LinkedList pour tout ce qui n'est pas lourd, supprimez-le du milieu de la liste à l'aide de l'itérateur.

  2. Arrêtez d’écrire le code de benchmarking à 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, la mise en œuvre de flux est assez lente JIT peut intégrer tous les éléments lambda mais ne produit pas un code aussi concis que la version Vanilla.

En règle générale, Java 8 flux n'est pas une magie. Ils ne pouvaient pas accélérer les choses déjà bien implémentées (avec, probablement, de simples itérations ou des instructions Java 5 pour-each remplacées par Iterable.forEach() et Collection.removeIf().). Les flux concernent davantage la commodité et la sécurité du codage. Commodité - compromis de vitesse fonctionne ici.

172
leventov

1) Vous voyez le temps inférieur à 1 seconde en utilisant votre point de repère. 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 = 10_000_000;

et a couru votre point de repère. 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 édition (int max = 1_000_000) les résultats ont été

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: le flux est plus lent que la collecte. Conclusion: beaucoup de temps a été consacré à l'initialisation du flux/à la transmission des valeurs.

2) Après l’augmentation du nombre de tâches, le flux est devenu plus rapide (ce qui est OK), mais le flux parallèle est resté trop lent. Qu'est-ce qui ne va pas? Remarque: vous avez collect(Collectors.toList()) dans votre commande. La collecte dans une collection 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 eu 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: la collecte des articles à la collecte a pris la majorité du temps. La partie la plus lente ajoute à la liste. BTW, simple ArrayList est utilisé pour Collectors.toList().

16
Sergey Fedorov
    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, je lance mon mac book pro qui a 8 cœurs, j’ai 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)

4
Mellon

Pour ce que vous essayez de faire, je n’utiliserais pas de toute façon les Java api classiques. Il y a une tonne de boxe/unboxing en cours, donc il y a une surcharge de performances énorme.

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 un tableau primitif de double/int et essayez de le faire avec un seul thread pour voir quelle est la performance.

PS: Vous voudrez peut-être jeter un œil sur JMH pour prendre en charge la référence. Il prend en charge certains des pièges typiques tels que le réchauffement de la machine virtuelle.

2
pveentjer