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:
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.
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.
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.
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()
.
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)
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.