[Übersetzung] Wann werden parallele Streams verwendet?

Quelle
Autoren: Doug Lea mit Brian Goetz, Paul Sandoz, Alexei Shipilev, Heinz Kabutz, Joe Bowbeer, ...

Das Framework java.util.streams enthält datengesteuerte Operationen für Sammlungen und andere Datenquellen. Die meisten Stream-Methoden führen für jedes Element dieselbe Operation aus. Wenn Sie mit der Erfassungsmethode parallelStream() mehrere Kerne haben, können Sie datengesteuert in datenparallel umwandeln . Aber wann lohnt es sich?


S.parallelStream().operation(F) Sie die Verwendung von S.parallelStream().operation(F) anstelle von S.stream().operation(F) , vorausgesetzt, die Operationen sind unabhängig voneinander und entweder rechenintensiv oder werden auf eine große Anzahl von Elementen angewendet, die effektiv aufgeteilt werden (teilbare) Datenstrukturen oder beides. Genauer gesagt:


  • F : Eine Funktion zum Arbeiten mit einem einzelnen Element, normalerweise einem Lambda, ist unabhängig, d.h. Die Operation für eines der Elemente ist unabhängig und wirkt sich nicht auf Operationen für andere Elemente aus (Empfehlungen zur Verwendung nicht störender zustandsloser Funktionen finden Sie in der Dokumentation zum Stream- Paket ).
  • S : Die ursprüngliche Sammlung wird effektiv aufgeteilt. Neben Sammlungen gibt es andere, die für die Parallelisierung geeignet sind und Datenquellen streamen, z. B. java.util.SplittableRandom (für deren Parallelisierung Sie die stream.parallel() -Methode verwenden können). Die meisten Quellen mit E / A im Kern sind jedoch hauptsächlich für den sequentiellen Betrieb ausgelegt.
  • Die Gesamtlaufzeit im sequentiellen Modus überschreitet die minimal zulässige Grenze. Heutzutage liegt das Limit für die meisten Plattformen ungefähr gleich (innerhalb von x10) bei 100 Mikrosekunden. Genaue Messungen sind in diesem Fall nicht erforderlich. Für praktische Zwecke reicht es aus, N (die Anzahl der Elemente) einfach mit Q (der Betriebszeit eines F ) zu multiplizieren, und Q kann ungefähr durch die Anzahl der Operationen oder die Anzahl der Codezeilen geschätzt werden. Danach müssen Sie überprüfen, ob N * Q mindestens 10000 beträgt (wenn Sie schüchtern sind, fügen Sie eine oder mehrere Nullen hinzu). Wenn also F eine kleine Funktion wie x -> x + 1 , ist eine parallele Ausführung sinnvoll, wenn N >= 10000 . Wenn umgekehrt F eine gewichtige Berechnung ist, ähnlich wie beim Finden des nächstbesten Zuges in einer Schachpartie, dann ist der Wert von Q so groß, dass N vernachlässigt werden kann, aber bis die Sammlung vollständig aufgeteilt ist.

Das Streaming-Verarbeitungs-Framework wird (und kann) auf keinem der oben genannten Punkte bestehen. Wenn die Berechnungen voneinander abhängig sind, ist ihre parallele Ausführung nicht sinnvoll oder überhaupt schädlich und führt zu Fehlern. Weitere Kriterien, die sich aus den oben genannten technischen Problemen und Kompromissen ergeben, sind:


  • Inbetriebnahme
    Das Auftreten zusätzlicher Kerne in Prozessoren ging in den meisten Fällen mit der Hinzufügung eines Energieverwaltungsmechanismus einher, der zu einer Verlangsamung des Kernelstarts führen kann, manchmal mit zusätzlichen Überlagerungen von JVM, Betriebssystem und Hypervisor. In diesem Fall entspricht die Grenze, bei der der Parallelmodus sinnvoll ist, in etwa der Zeit, die erforderlich ist, um die Verarbeitung der Unteraufgaben mit einer ausreichenden Anzahl von Kernen zu starten. Danach kann paralleles Rechnen energieeffizienter als sequentielles sein (abhängig von den Details der Prozessoren und Systeme. Ein Beispiel finden Sie im Artikel ).
  • Detaillierung (Granularität)
    Es ist selten sinnvoll, kleine Berechnungen aufzuteilen. Das Framework teilt die Aufgabe normalerweise so auf, dass die einzelnen Teile auf allen verfügbaren Systemkernen arbeiten können. Wenn nach dem Start praktisch keine Arbeit für jeden Kern vorhanden ist, werden (normalerweise sequentielle) Anstrengungen zur Organisation des parallelen Rechnens verschwendet. Da in der Praxis die Anzahl der Kerne zwischen 2 und 256 Schwellenwerten liegt, wird auch der unerwünschte Effekt einer übermäßigen Aufteilung der Aufgabe verhindert.
  • Teilbarkeit
    Zu den am effizientesten aufgeteilten Sammlungen gehören ArrayList und {Concurrent}HashMap sowie reguläre Arrays ( T[] , die mithilfe statischer java.util.Arrays Methoden in Teile aufgeteilt werden). Die am wenigsten effizienten Splitter sind LinkedList , BlockingQueue und die meisten Quellen mit E / A-Basis. Der Rest befindet sich irgendwo in der Mitte (Datenstrukturen, die Direktzugriff und / oder effiziente Suche unterstützen, werden normalerweise effizient aufgeteilt). Wenn das Aufteilen von Daten länger dauert als die Verarbeitung, ist der Aufwand vergeblich. Wenn Q groß genug ist, können Sie aufgrund der Parallelisierung auch für LinkedList einen Anstieg LinkedList . Dies ist jedoch ein eher seltener Fall. Darüber hinaus können einige Quellen nicht in ein einzelnes Element aufgeteilt werden, und daher kann der Zersetzungsgrad des Problems eingeschränkt sein.

Es kann schwierig sein, die genauen Eigenschaften dieser Effekte zu ermitteln (wenn Sie dies jedoch versuchen, können Sie Tools wie JMH verwenden ). Der kumulative Effekt ist jedoch leicht zu bemerken. Um es selbst zu fühlen - machen Sie ein Experiment. Wenn Sie beispielsweise auf einem 32-Core-Testcomputer kleine Funktionen wie max() oder sum() über der ArrayList beträgt ArrayList Break-Even-Punkt ungefähr 10.000. Für mehr Elemente wird eine bis zu 20-fache Beschleunigung angegeben. Die Öffnungszeiten für Sammlungen mit weniger als 10.000 Artikeln sind nicht viel geringer als für 10.000 und daher langsamer als die sequentielle Verarbeitung. Das schlechteste Ergebnis tritt bei weniger als 100 Elementen auf - in diesem Fall werden die beteiligten Threads gestoppt, ohne etwas Nützliches zu tun, weil Berechnungen werden abgeschlossen, bevor sie beginnen. Wenn Operationen an Elementen zeitaufwändig sind und effizient und vollständig aufteilbare Sammlungen wie ArrayList , sind die Vorteile sofort sichtbar.


Um all das zu paraphrasieren: Die Verwendung von parallel() im Fall einer unangemessen kleinen Rechenmenge kann etwa 100 Mikrosekunden kosten. Andernfalls sollte die Verwendung mindestens diese Zeit selbst (oder möglicherweise Stunden für sehr große Aufgaben) sparen. Die spezifischen Kosten und Nutzen variieren im Laufe der Zeit für verschiedene Plattformen und auch je nach Kontext. Wenn Sie beispielsweise kleine Berechnungen innerhalb eines sequentiellen Zyklus parallel ausführen, wird der Effekt von Höhen und Tiefen verstärkt (Leistungsmikrotests, bei denen dies auftritt, spiegeln möglicherweise nicht die tatsächliche Situation wider).


Fragen und Antworten


  • Warum kann die JVM nicht verstehen, wann Operationen parallel ausgeführt werden müssen?

Sie könnte es versuchen, aber zu oft war die Entscheidung falsch. Die Suche nach vollautomatischer Multi-Core-Parallelität hat in den letzten dreißig Jahren nicht zu einer universellen Lösung geführt. Daher verwendet das Framework einen zuverlässigeren Ansatz, bei dem der Benutzer nur zwischen Ja und Nein wählen muss. Diese Wahl basiert auf technischen Problemen, die bei der sequentiellen Programmierung ständig auftreten und die wahrscheinlich nie vollständig verschwinden werden. Beispielsweise kann es zu einer hundertfachen Verlangsamung kommen, wenn Sie nach dem Maximalwert in einer Sammlung suchen, die ein einzelnes Element enthält, und diesen Wert direkt (ohne Sammlung) verwenden. Manchmal kann die JVM solche Fälle für Sie optimieren. Dies geschieht jedoch selten in aufeinanderfolgenden Fällen und niemals im Parallelmodus. Auf der anderen Seite können wir erwarten, dass die Tools den Benutzern bei ihrer Entwicklung helfen, bessere Entscheidungen zu treffen.


  • Was ist, wenn ich für eine gute Entscheidung nicht genügend Kenntnisse über die Parameter ( F , N , Q , S ) habe?

Auch dies ähnelt Problemen, die bei der sequentiellen Programmierung auftreten. Beispielsweise wird die S.contains(x) -Methode der Collection Klasse normalerweise schnell ausgeführt, wenn S ein HashSet , langsam, wenn LinkedList , und in anderen Fällen durchschnittlich. Für den Autor einer Komponente, die die Sammlung verwendet, besteht der beste Ausweg aus dieser Situation normalerweise darin, sie zu kapseln und nur einen bestimmten Vorgang darauf zu veröffentlichen. Dann werden Benutzer von der Notwendigkeit der Auswahl isoliert. Gleiches gilt für Paralleloperationen. Beispielsweise kann eine Komponente mit einer internen Preiserfassung eine Methode bestimmen, die ihre Größe bis zum Limit überprüft. Dies ist sinnvoll, bis die bitweise Berechnung zu teuer ist. Ein Beispiel:


 public long getMaxPrice() { return priceStream().max(); } private Stream priceStream() { return (prices.size() < MIN_PAR) ? prices.stream() : prices.parallelStream(); } 

Diese Idee kann auf andere Überlegungen zum Zeitpunkt und zur Verwendung der Parallelität erweitert werden.


  • Was ist, wenn meine Funktion wahrscheinlich E / A- oder synchronisierte Vorgänge ausführt?

Ein Extrem sind Funktionen, die die Unabhängigkeitskriterien nicht erfüllen, einschließlich sequentieller E / A-Operationen, Zugriff auf das Blockieren synchronisierter Ressourcen und Fälle, in denen ein Fehler in einer parallelen Unteraufgabe, die E / A ausführt, andere betrifft. Ihre Parallelisierung macht wenig Sinn. Andererseits gibt es Berechnungen, die gelegentlich E / A durchführen oder die Synchronisation selten blockieren (z. B. die meisten Fälle der Protokollierung und die Verwendung wettbewerbsfähiger Sammlungen wie ConcurrentHashMap ). Sie sind harmlos. Was zwischen ihnen ist, erfordert mehr Forschung. Wenn jede Unteraufgabe für eine beträchtliche Zeit blockiert werden kann und auf E / A oder Zugriff wartet, sind die CPU-Ressourcen ohne die Möglichkeit ihrer Verwendung durch das Programm oder die JVM inaktiv. Davon ist schlecht für alle. In diesen Fällen ist die parallele Streaming-Verarbeitung nicht immer die richtige Wahl. Es gibt jedoch gute Alternativen - zum Beispiel asynchrone E / A und den CompletableFuture Ansatz.


  • Was ist, wenn meine Quelle auf E / A basiert?

Derzeit werden sie mithilfe der JDK- Stream / I / O-Generatoren (z. B. BufferedReader.lines() ) hauptsächlich für die Verwendung im sequentiellen Modus angepasst und verarbeiten Elemente nacheinander, BufferedReader.lines() sie verfügbar sind. Die Unterstützung der Hochleistungs-Massenverarbeitung von gepufferten E / A ist möglich, erfordert jedoch derzeit die Entwicklung spezieller Generatoren Stream , Spliterator und Collector . Unterstützung für einige häufige Fälle wird möglicherweise in zukünftigen Versionen des JDK hinzugefügt.


  • Was ist, wenn mein Programm auf einem ausgelasteten Computer ausgeführt wird und alle Kernel ausgelastet sind?

Maschinen haben normalerweise eine feste Anzahl von Kernen und können bei parallelen Operationen keine magischen neuen erstellen. Solange jedoch die Kriterien für die Wahl eines Parallelmodus eindeutig sprechen, besteht kein Zweifel. Ihre parallelen Aufgaben konkurrieren mit anderen um die CPU und Sie werden weniger Beschleunigung bemerken. In den meisten Fällen ist dies immer noch effektiver als andere Alternativen. Der zugrunde liegende Mechanismus ist so konzipiert, dass Sie, wenn keine Kerne verfügbar sind, im Vergleich zur sequentiellen Version nur eine geringfügige Verlangsamung bemerken, es sei denn, das System ist so überlastet, dass es seine gesamte Zeit damit verbringt, Kontexte zu wechseln, anstatt echte Arbeit zu leisten, oder konfiguriert in der Erwartung, dass die gesamte Verarbeitung nacheinander ausgeführt wird. Wenn Sie über ein solches System verfügen, hat der Administrator möglicherweise die Verwendung von Multithreading / Nuklearität in den JVM-Einstellungen bereits deaktiviert. Und wenn Sie der Systemadministrator sind, ist es sinnvoll, dies zu tun.


  • Sind alle Operationen im Parallelmodus parallelisiert?

Ja Zumindest bis zu einem gewissen Grad. Es ist jedoch zu berücksichtigen, dass das Stream-Framework die Einschränkungen von Quellen und Methoden bei der Auswahl berücksichtigt. Im Allgemeinen ist das Potenzial für Parallelität umso größer, je weniger Einschränkungen bestehen. Andererseits gibt es keine Garantie dafür, dass das Framework alle verfügbaren Möglichkeiten für Parallelität identifiziert und anwendet. In einigen Fällen kann Ihre eigene Lösung, wenn Sie Zeit und Kompetenz haben, die Möglichkeiten der Parallelität viel besser nutzen.


  • Welche Beschleunigung bekomme ich durch Parallelität?

Wenn Sie sich an diese Tipps halten, reicht dies normalerweise aus, um einen Sinn zu ergeben. Vorhersagbarkeit ist keine Stärke moderner Hardware und Systeme, und daher gibt es keine universelle Antwort. Die Cache-Lokalität, GC-Eigenschaften, JIT-Kompilierung, Speicherzugriffskonflikte, Datenspeicherort, Planungsrichtlinien für das Betriebssystem und das Vorhandensein eines Hypervisors sind einige der Faktoren, die einen erheblichen Einfluss haben. Die Leistung des sequentiellen Modus unterliegt auch ihrem Einfluss, der bei Verwendung der Parallelität häufig verstärkt wird: Das Problem, das bei der sequentiellen Ausführung einen Unterschied von 10 Prozent verursacht, kann zu einem 10-fachen Unterschied bei der parallelen Verarbeitung führen.


Das Stream-Framework enthält einige Funktionen, mit denen sich die Beschleunigungschancen erhöhen lassen. Beispielsweise hat die Verwendung der Spezialisierung für IntStream wie IntStream im IntStream normalerweise einen größeren Effekt als im sequentiellen Modus. Der Grund dafür ist, dass in diesem Fall nicht nur der Ressourcen- (und Speicher-) Verbrauch abnimmt, sondern auch die Cache-Lokalität verbessert wird. Die Verwendung von ConcurrentHashMap anstelle von HashMap reduziert im Fall des Parallelbetriebs des collect die internen Kosten. Neue Tipps und Tricks werden als Erfahrungen mit dem Framework angezeigt.


  • Das alles ist zu beängstigend! Können wir nicht einfach Regeln für die Verwendung von JVM-Eigenschaften zum Deaktivieren der Parallelität entwickeln?

Wir möchten Ihnen nicht sagen, was Sie tun sollen. Die Entstehung neuer Möglichkeiten für Programmierer, etwas falsch zu machen, kann beängstigend sein. Fehler in Code, Architektur und Auswertungen werden sicherlich auftreten. Vor Jahrzehnten sagten einige Leute voraus, dass Parallelität auf Anwendungsebene zu großen Katastrophen führen würde. Aber es wurde nie wahr.

Source: https://habr.com/ru/post/de420805/


All Articles