web-dev-qa-db-fra.com

Devrais-je toujours utiliser un flux parallèle lorsque cela est possible?

Avec Java 8 et lambdas, il est facile de parcourir des collections en tant que flux, et tout aussi facile d'utiliser un flux parallèle. Deux exemples de the docs , le second utilisant parallelStream:

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

Tant que l'ordre m'importe peu, serait-il toujours avantageux d'utiliser le parallèle? On pourrait penser que le travail est divisé plus rapidement en plusieurs cœurs.

Y a-t-il d'autres considérations? Quand faut-il utiliser le flux parallèle et quand utiliser le non-parallèle?

(Cette question est posée pour déclencher une discussion sur comment et quand utiliser des flux parallèles, pas parce que je pense que les utiliser est une bonne idée.)

432
Matsemann

Un flux parallèle a un temps système beaucoup plus élevé qu'un flux séquentiel. La coordination des discussions prend beaucoup de temps. J'utiliserais des flux séquentiels par défaut et n'envisagerais que des flux parallèles si

  • J'ai une énorme quantité d'articles à traiter (ou le traitement de chaque article prend du temps et peut être parallélisé)

  • J'ai un problème de performance en premier lieu

  • Je n'exécute pas déjà le processus dans un environnement multi-thread (par exemple: dans un conteneur Web, si j'ai déjà de nombreuses demandes à traiter en parallèle, l'ajout d'une couche supplémentaire de parallélisme dans chaque demande pourrait avoir des effets plus négatifs que positifs. )

Dans votre exemple, la performance sera de toute façon pilotée par l'accès synchronisé à System.out.println(), et rendre ce processus parallèle n'aura aucun effet, ni même un effet négatif.

De plus, rappelez-vous que les flux parallèles ne résolvent pas comme par magie tous les problèmes de synchronisation. Si une ressource partagée est utilisée par les prédicats et les fonctions utilisés dans le processus, vous devez vous assurer que tout est thread-safe. En particulier, les effets secondaires sont des choses sur lesquelles vous devez vraiment vous inquiéter si vous allez en parallèle.

En tout cas, mesure, ne devine pas! Seule une mesure vous dira si le parallélisme en vaut la peine.

622
JB Nizet

L’API Stream a été conçue pour faciliter l’écriture des calculs de manière abstraite, en évitant la façon dont ils seraient exécutés, facilitant ainsi la commutation entre séquentiel et parallèle.

Cependant, juste parce que c'est facile, cela ne signifie pas que c'est toujours une bonne idée, et en fait, c'est une mauvaise idée de simplement laisser tomber .parallel() partout parce que vous le pouvez.

Tout d'abord, notez que le parallélisme n'offre aucun autre avantage que la possibilité d'une exécution plus rapide lorsque davantage de cœurs sont disponibles. Une exécution en parallèle impliquera toujours plus de travail qu'une exécution séquentielle, car en plus de résoudre le problème, elle doit également assurer la répartition et la coordination des sous-tâches. L'espoir est que vous pourrez trouver la réponse plus rapidement en répartissant le travail entre plusieurs processeurs; Le fait que cela se produise dépend de nombreux facteurs, notamment la taille de votre ensemble de données, le nombre de calculs que vous effectuez sur chaque élément, la nature du calcul (spécifiquement, le traitement d'un élément interagit-il avec le traitement des autres?) , le nombre de processeurs disponibles et le nombre d’autres tâches en concurrence pour ces processeurs.

De plus, notez que le parallélisme expose souvent le non-déterminisme dans le calcul qui est souvent caché par les implémentations séquentielles; parfois cela n'a pas d'importance ou peut être atténué en limitant les opérations impliquées (c'est-à-dire que les opérateurs de réduction doivent être sans état et associatifs).

En réalité, parfois, le parallélisme accélère les calculs, parfois non, et parfois même les ralentit. Il est préférable de développer d'abord en utilisant une exécution séquentielle, puis d'appliquer un parallélisme où (A) vous savez qu'il y a réellement un avantage à une performance accrue et (B) que cela produira réellement une performance accrue. (A) est un problème commercial, pas un problème technique. Si vous êtes un expert en performances, vous serez généralement en mesure d’examiner le code et de déterminer (B), mais le chemin intelligent est à mesurer. (Et, ne vous inquiétez même pas jusqu'à ce que vous soyez convaincu de (A); si le code est assez rapide, il est préférable d'appliquer vos cycles cérébraux ailleurs.)

Le modèle de performance le plus simple pour le parallélisme est le modèle "NQ", où N est le nombre d'éléments et Q le calcul par élément. En général, vous devez que le NQ du produit dépasse un certain seuil avant de commencer à obtenir un avantage de performance. Pour un problème de faible Q tel que "additionner les nombres de 1 à N", vous verrez généralement un seuil de rentabilité compris entre N = 1000 et N = 10000. Avec des problèmes de Q plus élevé, vous verrez des seuils de rentabilité inférieurs.

Mais la réalité est assez compliquée. Donc, jusqu’à ce que vous deveniez expert, identifiez d’abord le moment où le traitement séquentiel vous coûte réellement quelque chose, puis mesurez si le parallélisme vous aidera.

212
Brian Goetz

J'ai regardé une des présentations de Brian Goetz (architecte de langage Java et chef des spécifications pour Lambda Expressions). Il explique en détail les 4 points suivants à prendre en compte avant de passer à la parallélisation:

Coûts de fractionnement/décomposition
- Parfois, le fractionnement coûte plus cher que de simplement faire le travail!
Coûts d'envoi/de gestion des tâches
- Peut faire beaucoup de travail dans le temps nécessaire pour passer le travail à un autre fil.
Coût de la combinaison de résultats
- Parfois, une combinaison implique la copie de nombreuses données. Par exemple, l’ajout de chiffres est peu coûteux alors que la fusion d’ensembles coûte cher.
Localité
- L'éléphant dans la pièce. C'est un point important que tout le monde peut manquer. Vous devez prendre en compte les erreurs de cache. Si un processeur attend des données à cause des erreurs de cache, vous ne gagnerez rien grâce à la parallélisation. C'est pourquoi les sources basées sur les tableaux sont les plus parallèles, car les prochains index (proches de l'index actuel) sont mis en cache et il y a moins de chances que le processeur connaisse un cache manquant.

Il mentionne également une formule relativement simple pour déterminer une chance d'accélération parallèle.

Modèle NQ :

N x Q > 10000

où,
N = nombre d'éléments de données
Q = quantité de travail par élément

53
Ram Patra

JB a frappé le clou sur la tête. La seule chose que je puisse ajouter, c'est que Java 8 ne fait pas de traitement parallèle pur, mais paraquentiel . Oui, j'ai écrit l'article et je fais du F/J depuis trente ans, alors je comprends le problème.

12
edharned

D'autres réponses ont déjà couvert le profilage afin d'éviter une optimisation prématurée et des frais généraux lors du traitement en parallèle. Cette réponse explique le choix idéal des structures de données pour la diffusion en parallèle.

En règle générale, les gains de performance du parallélisme sont optimaux pour les flux sur les instances ArrayList, HashMap, HashSet et ConcurrentHashMap; des tableaux; int plages; et long plages. Le point commun de ces structures de données est qu'elles peuvent toutes être divisées avec précision et à moindre coût en sous-gammes de toutes tailles, ce qui facilite la division du travail entre des threads parallèles. L'abstraction utilisée par la bibliothèque de flux pour effectuer cette tâche est le séparateur, qui est renvoyé par la méthode spliterator sur Stream et Iterable.

Un autre facteur important commun à toutes ces structures de données est qu'elles fournissent une localité de référence bonne à excellente lors du traitement séquentiel: les références d'éléments séquentielles sont stockées ensemble dans la mémoire. Les objets référencés par ces références peuvent ne pas être proches les uns des autres en mémoire, ce qui réduit la localité de référence. La localisation-de-référence s’avère d’une importance cruciale pour la parallélisation des opérations en bloc: sans cela, les threads passent une grande partie de leur temps inactif à attendre que les données soient transférées de la mémoire dans la mémoire cache du processeur. Les structures de données avec la meilleure localité de référence sont des tableaux primitifs car les données elles-mêmes sont stockées de manière contiguë en mémoire.

Source: élément n ° 48 Soyez prudent lors de la création de flux en parallèle, efficace Java 3e par Joshua Bloch

1
ruhong

Ne jamais paralléliser un flux infini avec une limite. Voici ce qui se passe:

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

Résultat

    Exception in thread "main" Java.lang.OutOfMemoryError
        at ...
        at Java.base/Java.util.stream.IntPipeline.findFirst(IntPipeline.Java:528)
        at InfiniteTest.main(InfiniteTest.Java:24)
    Caused by: Java.lang.OutOfMemoryError: Java heap space
        at Java.base/Java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.Java:750)
        at ...

Idem si vous utilisez .limit(...)

Explication ici: Java 8, l’utilisation de .parallel dans un flux provoque une erreur de MOO

De même, n'utilisez pas parallèle si le flux est commandé et contient beaucoup plus d'éléments que vous ne souhaitez traiter, par exemple.

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

Cela peut durer beaucoup plus longtemps car les threads parallèles peuvent fonctionner sur de nombreuses plages de nombres au lieu de la plage cruciale 0-100, ce qui prend beaucoup de temps.

0
tkruse