AXOPEN

JAVA 8 – Parallel Stream – Performance: les types de données

L’utilisation des streams en mode parallèle est très simple d’utilisation. En revanche, il n’est pas forcément évident de savoir si un traitement particulier va bénéficier, ou non, d’une amélioration de performance lors du passage en parallèle. Pour savoir ce qui peut avoir un impact sur les performances, il est nécessaire de comprendre ce qui se passe lorsqu’on lance un traitement en parallèle avec les streams. C’est l’objet de cet article.

Pourquoi utiliser les STREAMS en parallèle?

La vitesse des processeurs actuels ne s’améliore plus de manière aussi rapide que par le passé. Les nouvelles architectures processeur se basent davantage sur la multiplication des cœurs CPU que sur la fréquence. Avant JAVA 7 puis 8 il était très difficile de bénéficier de ces nouvelles architectures. La création de threads étant un processus très complexe et assez dangereux car difficile à débugger, elle était souvent bannie des développements habituels. Grâce à la nouvelle API Stream de JAVA 8 il est devenu presque « trivial » de bénéficier des architectures multicœurs. Il est donc temps d’utiliser nos puissantes machines!

Le fonctionnement de l’API Stream

Le fonctionnement de l’API Stream est un traitement de type « fork / join ». Disponible depuis JAVA 7, ce mode de programmation a été intégré de manière transparente dans l’API STREAM. Il faut bien comprendre ce mécanisme, même sans devenir expert, pour en tirer pleinement parti !

Fork / Join: Fonctionnement général

La complexité d’un problème dépend de deux facteurs : la « difficulté » pour résoudre le problème et la taille de l’entrée. La combinaison des deux s’exprime souvent par une fonction de la forme O(n) où est n est la taille de l’entrée du problème.

L’objectif des algorithmes de type fork / join est de diviser le problème en sous-problèmes de taille inférieure qui sont traités par des processus différents puis de fusionner les résultats pour obtenir le résultat final.

En clair, pour réaliser du multi-threading, il faut diviser le problème en sous-problèmes qui sont eux même répartis sur les différents cœurs de la machine. Plus que le problème, c’est l’ensemble d’entrée du problème qui est divisé. Par exemple, si on souhaite traiter un tableau d’éléments, ce tableau donné en entrée de l’algorithme va être divisé en petits tableaux qui seront donnés aux différents cœurs (CPU) de la machine (phase de fork). Une fois que chaque cœur (CPU) a réalisé son traitement sur sa partie (sous-ensemble du problème de départ) vient la phase de join pour « fusionner » les résultats issus des traitements.  Nous allons voir ici que cette phase de fork joue un rôle déterminant dans les performances des streams parallèles.

Phase de fork: Importance du type de données

Le rôle de la phase de fork est primordial dans les performances. En effet, découper l’espace d’entrée du problème dépend fortement de la structure de données. Par exemple, découper une ArrayList (qui est représentée dans un tableau) peut être réalisé en temps constant. En effet, chaque élément peut être accédé de manière unitaire et en temps constant avec une complexité de O(1).

En revanche, un TreeSet ou un HashSet ne peuvent pas être découpés de manière simple et rapide, ce qui peut impliquer un défaut de performance important. L’exemple le plus marquant est la LinkedList. Une liste chaînée est très complexe à découper car il faut la parcourir pour la découper aux bons endroits.

Ceci peut paraître un peu contre-intuitif mais plus on réfléchit à la structure de données, plus il devient clair que la phase de fork dépend fortement de la capacité à diviser facilement l’ensemble de départ.

En conclusion sur la phase de fork, une ArrayList s’avère plus performante car facilement découpable en sous ArrayList. Ceci n’implique pas que les traitements seront toujours plus rapides avec des ArrayList car comme nous l’avons dit dans l’article précédent, l’utilisation d’ArrayList oblige les streams à garder l’ordre ce qui est très préjudiciable en terme de performance, par opposition à un Set (qui lui n’a pas d’ordre). Il est donc important de prendre tous ces facteurs en compte avant de choisir le type de données en entrée et en sortie de notre Stream.