JAVA 8 – Stream et ParallelStream – Performance sur des String

Etude de la performance de l'utilisation de la nouvelle API JAVA 8 Stream pour le traitement de string (chaîne de caractère)
Pierre LISERONMis à jour le 11 Mai 2014

Etude de la performance de lutilisation de la nouvelle APIUne API est un programme permettant à deux applications distinctes de communiquer entre elles et d’échanger des données. JAVA 8 Stream pour le traitement de string (chaîne de caractère)

But de larticle

Nous allons essayer détudier les différences de performances pour réaliser des traitements sur des ensembles ordonnées ou non (list, hahset, treeset) avec lutilisation des Stream et lambda JAVA 8. Puis nous comparerons lutilisons classique des streams avec les parallelStream pour voir si la parallélisation permet de gros gain de performance.

Stream Méthodologie de test

Nous allons décrire dans cette partie la méthodologie de test que nous avons appliqué pour obtenir nos résultats. Bien sûr ce test na pas vocation à servir de référence pour des performances mais de voir un peu les impacts dune utilisation ou dune autre dans le monde réel des développements. Volontairement donc, il na pas été choisi doptimiser particulièrement les routines de test afin de coller au plus près de ce que font les développeurs dans des programmes de tous les jours. Par exemple, le traitement de test retenu nest pas particulièrement palpitant et ne met pas forcément lutilisation des stream et des lambda à lhonneur mais ce traitement sur des chaînes de caractères peut se retrouver nimporte où dans une application, doù lintérêt de tester ce genre de code ainsi que limpact sur les performances. Il est vrai que les lambdas et les streams ne sont pas là que pour les performances et que la facilité décriture, de lecture et de maintenance sont à prendre en compte en premier lieu surtout sur des applications ou le volume de données traitées est faible, ce qui avouons le est la majorité des applications que les développeurs réalisent.

Le traitement effectué pour le test de performance

Le traitement effectué correspond au filtrage dans un ensemble de String contenant la sous chaîne « test ». Pour toutes ces String qui contiennent « test » nous passons la première lettre en majuscule et concaténons « _test ».

Traitement en JAVA 7 avec les collections

Historiquement ce traitement en JAVA 7 avec des collections sécrit de la manière suivante:

On retrouve lutilisation habituelle des itérators avec la création dune liste dans laquelle on vient accumuler les éléments nouvellement créés.

for (Iterator iterator = strings.iterator(); iterator.hasNext();) {
 string = (String) iterator.next();
 if (string.contains("test")) {
 lList.add(string.substring(0, 1).toUpperCase() + "_test");
 }

Traitement en JAVA 8 avec les stream

En JavaLangage de développement très populaire ! 8 ce traitement peut sécrire de manière plus lisible et plus simple de la manière suivante:

On retrouve ici lutilisation classique du linstruction filter pour ne garder que les éléments ne contenant « test » et linstruction map, pour la création des nouvelles chaînes de caractères.

lList = strings
 .stream()
.filter(x -> x.contains("test"))
 .map(x -> x.substring(0, 1).toUpperCase() + "_test")
 .collect(Collectors.toList());<span > </span>

Traitement en JAVA 8 avec les parallelStream

De même, on peut nativement paralléliser ce traitement avec les parallelStream de la manière suivante:

 .parallelstream()
.filter(x -> x.contains("test"))
 .map(x -> x.substring(0, 1).toUpperCase() + "_test")
 .collect(Collectors.toList());

La volumétrie des tests

Afin de tester les performances, nous allons tester ces différents traitements avec les listes ou ensembles contenant dans 10 éléments puis 100 éléments, 1 000 éléments, 10 000 éléments, 100 000 éléments et 1 000 000 éléments. Ainsi nous aurons une idée de linfluence de la volumétrie sur les performances des streams.

Les conteneurs

En parallèle de ces tests, nous allons jouer sur les conteneurs pour voir les impacts sur les performances. Ainsi nous allons tester à chaque fois, avec les collections suivantes:

  • Une ArrayList (très utilisée dans les applications)
  • Un HashSet
  • Un TreeSet

Et enfin pour ces trois collections, nous allons faire le traitement en ouvrant un stream et un parellelStream pour voir les impacts sur les performances.

Les résultats

Pour chaque quantité dans lensemble de départ nous obtenons donc 7 résultats

Le traitement par liste classique

  • Le traitement par une collection classique
  • Le traitement par Stream sur un TreeSet
  • Le traitement par ParallelStream sur un TreeSet
  • Le traitement par Stream sur un HashSet
  • Le traitement par ParallelStream sur un HasSet
  • Le traitement par Stream sur une ArrayList
  • Le traitement par ParallelStream sur un ArrayList

Afin davoir des résultats moyennés, nous effectuons 50 fois les tests pour obtenir une moyenne des temps. Les temps absolus ne sont bien sur pas à prendre en compte, cest surtout la rapport qui sont pertinents.

La machine de test

Pour information ces tests sont réalisés avec la machine suivante:

  • Processeur Intel Core i7-3770S 4 cœurs 8 cœurs logiques
  • 8 Go RAM
  • Windows 8.1
  • JVM HotSpot 64 Bit 1.8.0_0

Stream / ParallelStream : Résultats performances

Les résultats sont données dans ce tableau brut en millisecondes. Pour rappel chaque test a été effectué 50 fois daffilée pour réaliser ces moyennes de temps. En rouge, les résultats les plus lent et en vert les plus rapide. La première ligne correspond au traitement classique en JAVA 7 sur une collection avec des itérateurs. Il est donné ici à titre de référence. | Type de test | 10 | 100 | 1k | 10k | 100k | 1000k | |----|----|----|----|----|----|----| | List Collections | 0.16 |0.24|1.24|12.36|146.82|4067.68| | TreeSet Stream |0.1|0.16|1.48|13.34|131.34|2300.90| | TreeSet Parallel Stream |0.12|0.14 | 0.56 | 4.48 | 43.34 | 1420.12 | | HashSet Stream | 0.08 | 0.24 | 1.5 | 13.92 | 135.84 | 3057.62 | | HashSet Parallel Stream | 0.1 | 0.18 | 0.44 | 4.34 | 42.42 | 3148.04| | ArrayList Stream | 0.04 | 0.14 | 1.18 | 10.44 | 100.18 | 4020.40 | | ArrayList Parallel | 0.12 | 0.12 | 0.52 | 3.68 | 39.84 | 2965.76 |

Stream performance Interprétation des résultats:

Pour 10 et 100 éléments:

Ce quon constate immédiatement, cest que pour des faible volumétrie, lutilisation des différents techniques ne change pas grand chose. En effet pour moins de 1000 éléments, les temps de traitement sont similaire < 1ms (ce qui avouons le est déjà extrêmement performant!)

A partir de 1 000 éléments dans lensemble de départ, on commence à voir émerger une certain différence de temps de traitement.

Pour 1 000 éléments (1k):

En regardant de plus prêt, on constate que lutilisation des stream par rapport à la liste classique prend environ le même temps. De lordre de 1,4 ms et ce quelque soit le conteneur (List, TreeSet, HashSet). Par contre le temps des traitements en parallèle est sensiblement plus petit avec presque un rapport de 3! Donc dès 1 000 éléments, le traitement par des parallelStream améliore sensiblement les performances!

=> Avantage ParallelStream

Pour 10 000 éléments (10 k):

On constat la même chose que pour 1000 éléments avec cette fois une différence qui se creuse avec lutilisation de la liste comme conteneur 3,68 ms significativement plus rapide que le HashSet et le TreeSet.

=> Avantage ParallelStream ArrayList

Pour 100 000 éléments (100 k):

Pas de changement dans les résultats au détail près que les stream vont légèrement plus vite que le traitement par collection même non parallèle.

=> Avantage Stream!

Pour 1 000 000 déléments (1 000 k):

Là, les résultats sont toujours très en faveur des parallelStream avec pour le coup une démarcation assez surprenant du TreeSet.

=> Avantage TreeSet ParallelStream !

Conclusion

Nous pouvons ici très clairement voir que dans notre cas de test lutilisation des stream peut significativement améliorer les performances dès lors que nous traitons plus de 1 000 éléments. A moins de 1 000 éléments, il est difficile de voir un véritable gain de performance. De plus lutilisation des parallelStream sur plus de 1 000 éléments améliorent significativement les performance. Plus le volume dinformation a traité est important plus de la gain de performance de lutilisation des stream se fait sentir. Pour 1 000 000 déléments, le rapport de gain est de 3.14 en faveur des streams parallélisés. Dans ce test, il na pas été facile de mettre en évidence une grande différence de traitement entre les List, HashSet et TresSet. Le traitement effectué est surement trop simple pour montrer de réelle différence. Nous verrons dans un autre article si les conteneurs jouent un rôle plus important. De même dans cet article, nous ne nous sommes pas occupé de lutilisation mémoire de ces tests, ce qui pourrait avoir de limportance pour la mesure des performances.

Néanmoins, en conclusion, les tests montrent que lutilisation des streams ne dégrade pas les performances (même si elle ne les améliore pas) sur des petits ensembles mais améliore significativement sur des gros volume de données. Rajouté aux gains décriture de codes et de maintenance, cet article pousse donc à lutilisation des stream et des lambdas dans le développements des applications de tous les jours en remplacement des bonnes vielles collections!