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.
4 answers
Arrêtez d'utiliser
LinkedList
pour tout sauf une suppression lourde du milieu de la liste en utilisant iterator.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.
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()
.
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)
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.