649 Stimmen

Sollte ich immer einen parallelen Stream verwenden, wenn möglich?

Mit Java 8 und Lambdas ist es einfach, über Kollektionen als Streams zu iterieren und genauso einfach, einen parallelen Stream zu nutzen. Zwei Beispiele aus den docs, das zweite mit parallelStream:

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

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

Solange die Reihenfolge nicht wichtig ist, wäre es immer vorteilhaft, den parallel zu verwenden? Man könnte denken, dass es schneller ist, die Arbeit auf mehrere Kerne aufzuteilen.

Gibt es andere Überlegungen? Wann sollte parallel stream verwendet werden und wann sollte der nicht-parallele verwendet werden?

(Diese Frage ist gestellt, um eine Diskussion darüber auszulösen, wie und wann parallele Streams verwendet werden sollen, nicht weil ich denke, dass es immer eine gute Idee ist, sie zu verwenden.)

884voto

JB Nizet Punkte 654813

Ein paralleler Stream hat im Vergleich zu einem sequentiellen Stream einen wesentlich höheren Overhead. Die Koordination der Threads erfordert eine bedeutende Menge an Zeit. Ich würde standardmäßig sequentielle Streams verwenden und nur in Betracht ziehen, parallel Streams zu verwenden, wenn

  • Ich eine massive Anzahl von Elementen zu verarbeiten habe (oder die Verarbeitung jedes Elements Zeit in Anspruch nimmt und parallelisierbar ist)

  • Ich bereits ein Leistungsproblem habe

  • Ich den Prozess nicht bereits in einer Multi-Thread-Umgebung ausführe (zum Beispiel: in einem Web-Container, wenn ich bereits viele Anfragen paralle verarbeite, könnte die Hinzufügung einer zusätzlichen Schicht von Parallelismus in jeder Anfrage mehr negative als positive Auswirkungen haben)

In Ihrem Beispiel wird die Leistung sowieso durch den synchronisierten Zugriff auf System.out.println() bestimmt, und die Parallelisierung dieses Prozesses wird keine Wirkung haben oder sogar negativ sein.

Außerdem, denken Sie daran, dass parallele Streams nicht magischerweise alle Synchronisierungsprobleme lösen. Wenn eine gemeinsam genutzte Ressource von den Prädikaten und Funktionen verwendet wird, die im Prozess verwendet werden, müssen Sie sicherstellen, dass alles threadsicher ist. Insbesondere Seiteneffekte sind Dinge, über die Sie sich wirklich Sorgen machen müssen, wenn Sie parallel gehen.

Auf jeden Fall, messen Sie, raten Sie nicht! Nur eine Messung wird Ihnen sagen, ob der Parallelismus es wert ist oder nicht.

298voto

Brian Goetz Punkte 83148

Die Stream-API wurde entwickelt, um es einfach zu machen, Berechnungen in einer Weise zu schreiben, die von der Ausführung abstrahiert war, wodurch das Wechseln zwischen sequentiell und parallel einfach wurde.

Aber nur weil es einfach ist, bedeutet das nicht immer, dass es eine gute Idee ist. Tatsächlich ist es eine schlechte Idee, einfach überall .parallel() einzufügen, nur weil man es kann.

Zunächst einmal bietet Parallelismus keine Vorteile außer der Möglichkeit einer schnelleren Ausführung, wenn mehr Kerne verfügbar sind. Eine parallele Ausführung wird immer mehr Arbeit erfordern als eine sequentielle, denn zusätzlich zur Lösung des Problems muss sie auch die Aufteilung und Koordination von Teilaufgaben durchführen. Die Hoffnung besteht darin, dass Sie schneller zur Antwort gelangen, indem Sie die Arbeit auf mehrere Prozessoren aufteilen; ob dies tatsächlich geschieht, hängt von vielen Dingen ab, einschließlich der Größe Ihres Datensatzes, wie viel Berechnung Sie für jedes Element durchführen, der Art der Berechnung (genauer gesagt, interagiert die Verarbeitung eines Elements mit der Verarbeitung anderer?), der Anzahl der verfügbaren Prozessoren und der Anzahl anderer Aufgaben, die um diese Prozessoren konkurrieren.

Es sei auch darauf hingewiesen, dass Parallelismus oft auch Nichtdeterminismus in der Berechnung sichtbar macht, der von sequentiellen Implementierungen oft verborgen wird; manchmal spielt dies keine Rolle, oder es kann durch Beschränkung der beteiligten Operationen abgemildert werden (d. h., Reduktionsoperatoren müssen zustandslos und assoziativ sein).

In Wirklichkeit wird Parallelismus manchmal Ihre Berechnung beschleunigen, manchmal nicht und manchmal sie sogar verlangsamen. Am besten ist es, zuerst mit sequentieller Ausführung zu entwickeln und dann den Parallelismus anzuwenden, wenn

(A) Sie wissen, dass tatsächlich ein Nutzen für eine gesteigerte Leistung besteht und

(B) dass dies tatsächlich zu einer gesteigerten Leistung führen wird.

(A) ist ein Geschäftsproblem, kein technisches. Wenn Sie ein Performance-Experte sind, sollten Sie in der Regel in der Lage sein, den Code zu betrachten und (B) zu bestimmen, aber der kluge Weg ist es zu messen. (Und verschwenden Sie nicht einmal Ihre Zeit, bis Sie von (A) überzeugt sind; wenn der Code schnell genug ist, ist es besser, Ihre Gehirnzellen anderswo einzusetzen.)

Das einfachste Performance-Modell für Parallelismus ist das "NQ"-Modell, bei dem N die Anzahl der Elemente und Q die Berechnung pro Element ist. Im Allgemeinen müssen Sie das Produkt NQ überschreiten, bevor Sie einen Leistungsbeitrag erhalten. Bei einem Problem mit niedrigem Q wie "Summieren von Zahlen von 1 bis N" sehen Sie in der Regel einen Breakeven zwischen N=1000 und N=10000. Bei Problemen mit höherem Q sehen Sie Breakevens bei niedrigeren Schwellenwerten.

Aber die Realität ist ziemlich kompliziert. Also, bis Sie Expertise erlangen, identifizieren Sie zuerst, wann die sequentielle Verarbeitung tatsächlich etwas kostet, und messen dann, ob Parallelismus hilfreich sein wird.

85voto

Ram Patra Punkte 15496

Ich habe eine der Präsentationen von Brian Goetz (Java-Spracharchitekt und Spezifikationsleiter für Lambda-Ausdrücke) angeschaut. Er erklärt im Detail die folgenden 4 Punkte, die vor der Parallelisierung zu beachten sind:

Aufteilungs- / Zerlegungskosten
– Manchmal sind die Aufteilungskosten teurer als die eigentliche Arbeit!
Aufgabenverteilungs- / -verwaltungskosten
– Man kann in der Zeit, die es benötigt, um die Arbeit an einen anderen Thread zu übergeben, viel Arbeit erledigen.
Kosten für die Kombination der Ergebnisse
– Manchmal erfordert die Kombination das Kopieren von vielen Daten. Zum Beispiel ist das Hinzufügen von Zahlen billig, während das Zusammenführen von Mengen teuer ist.
Lokalität
– Das "Elephant im Raum". Dies ist ein wichtiger Punkt, den jeder übersehen könnte. Man sollte sich Gedanken über Cache-Misses machen. Wenn eine CPU aufgrund von Cache-Misses auf Daten wartet, würde man nichts durch die Parallelisierung gewinnen. Deshalb ist es so, dass arraybasierte Quellen am besten parallelisieren, da die nächsten Indizes (in der Nähe des aktuellen Index) gecacht sind und es weniger Chancen gibt, dass die CPU einen Cache-Miss erlebt.

Er erwähnt auch eine relativ einfache Formel, um die Chance auf eine parallele Beschleunigung zu bestimmen.

NQ-Modell:

N x Q > 10000

wo,
N = Anzahl der Datenpunkte
Q = Menge der Arbeit pro Punkt

5voto

tkruse Punkte 9304

Nie parallelisieren Sie einen unendlichen Stream mit einem Limit. Hier passiert:

    public static void main(String[] args) {
        // Zählen wir bis 1 parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

Ergebnis

    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 ...

Gleiches gilt, wenn Sie .limit(...) verwenden.

Erklärung hier: Java 8, Verwendung von .parallel in einem Stream verursacht einen OOM-Fehler

Ebenso, verwenden Sie nicht parallel, wenn der Stream geordnet ist und viel mehr Elemente hat als Sie verarbeiten möchten, z.B.

public static void main(String[] args) {
    // Zählen wir bis 1 parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

Dies kann viel länger dauern, da die parallelen Threads an vielen Zahlenbereichen arbeiten können, anstatt an dem entscheidenden Bereich 0-100, was dazu führt, dass es sehr lange dauert.

4voto

ruhong Punkte 1683

Andere Antworten haben bereits das Profiling behandelt, um eine vorzeitige Optimierung und eine erhöhte Kosten bei der parallelen Verarbeitung zu vermeiden. Diese Antwort erläutert die ideale Wahl der Datenstrukturen für paralleles Streaming.

Im Allgemeinen sind Leistungssteigerungen durch Parallelität am besten bei Streams über Instanzen von ArrayList, HashMap, HashSet und ConcurrentHashMap; Arrays; int Bereiche; und long Bereiche. Was diese Datenstrukturen gemeinsam haben, ist, dass sie alle genau und kostengünstig in Teilbereiche beliebiger Größe aufgeteilt werden können, was es einfach macht, die Arbeit unter parallelen Threads aufzuteilen. Die Abstraktion, die von der Streams-Bibliothek verwendet wird, um diese Aufgabe auszuführen, ist der Spliterator, der von der Methode spliterator in Stream und Iterable zurückgegeben wird.

Ein weiterer wichtiger Faktor, den all diese Datenstrukturen gemeinsam haben, ist, dass sie eine gute bis hervorragende Speicherortreferenz bieten, wenn sie sequenziell verarbeitet werden: sequenzielle Elementreferenzen sind gemeinsam im Speicher gespeichert. Die von diesen Referenzen verwiesenen Objekte können sich möglicherweise nicht nahe beieinander im Speicher befinden, was die Speicherortreferenz verringert. Die Speicherortreferenz erweist sich als äußerst wichtig für die Parallelisierung von Massenoperationen: Ohne sie verbringen Threads einen Großteil ihrer Zeit im Leerlauf, während sie darauf warten, dass Daten vom Speicher in den Cache des Prozessors übertragen werden. Die Datenstrukturen mit der besten Speicherortreferenz sind primitive Arrays, weil die Daten selbst zusammenhängend im Speicher gespeichert sind.

Quelle: Eintrag #48 Verwenden Sie Vorsicht bei der Parallelisierung von Streams, Effective Java 3e von Joshua Bloch

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X