Gegeben die folgende einfache Aufgabe, ungerade Zahlen in einem eindimensionalen Array zu finden:
%Vor%Es sieht so aus, als wäre dies ein guter Kandidat für die parallele Verarbeitung. So könnte man versucht sein, die folgende TParallel.For-Version zu verwenden:
%Vor%Das Ergebnis dieser parallelen Berechnung ist in zweierlei Hinsicht überraschend:
Die Anzahl der gezählten Quoten ist falsch
Die Ausführungszeit ist länger als in der seriellen Version
1) Ist erklärbar, weil wir die Wahrscheinlichkeitsvariable für den gleichzeitigen Zugriff nicht geschützt haben. Um dies zu beheben, sollten wir stattdessen TInterlocked.Increment(odds);
verwenden.
2) Ist auch erklärbar: Es zeigt die Auswirkungen von falschem Teilen .
Idealerweise wäre die Lösung für das Problem der falschen Freigabe, eine lokale Variable zu verwenden, um Zwischenergebnisse zu speichern, und nur am Ende aller parallelen Aufgaben diese Vermittler zusammenzufassen. Und hier ist meine eigentliche Frage, die ich nicht verstehen kann: Gibt es eine Möglichkeit, eine lokale Variable in meine anonyme Methode zu bekommen? Beachten Sie, dass das einfache Deklarieren einer lokalen Variablen im Textkörper der anonymen Methode nicht funktioniert, da der anonyme Methodenhauptteil für jede Iteration aufgerufen wird. Und wenn das irgendwie machbar wäre, gäbe es eine Möglichkeit, mein Zwischenergebnis am Ende jeder Task-Iteration aus der anonymen Methode zu holen?
Edit: Ich bin eigentlich nicht wirklich daran interessiert, Odds oder Evans zu zählen. Ich benutze das nur, um den Effekt zu demonstrieren.
Und aus Gründen der Vollständigkeit hier ist eine Konsolen-App, die die Auswirkungen demonstriert:
%Vor%Der Schlüssel für dieses Problem ist die korrekte Partitionierung und Freigabe so wenig wie möglich.
Mit diesem Code läuft es fast 4 mal schneller als das serielle.
%Vor%Sie können ähnlichen Code mit dem TParallel.For schreiben, aber es läuft immer noch ein bisschen langsamer (wie 3 mal schneller als seriell) als nur mit TTask.
Btw Ich habe die Funktion verwendet, um das Worker-TProc zurückzugeben, um das Index-Capturing-Recht zu erhalten. Wenn Sie es in einer Schleife in derselben Routine ausführen, erfassen Sie die Schleifenvariable.
Update am 19.12.2014:
Da wir herausgefunden haben, dass die richtige Partitionierung die entscheidende Sache ist, kann diese sehr einfach in eine for-Schleife eingefügt werden, ohne sie an einer bestimmten Datenstruktur zu verankern:
%Vor%Das Wichtigste ist, eine lokale Variable für die Zählung zu verwenden und erst am Ende die gemeinsam genutzte Variable einmal zu verwenden, um die Untersumme hinzuzufügen.
Mit OmniThreadLibrary aus dem SVN (dies ist noch in keiner offiziellen Version enthalten), können Sie dies auf eine Weise schreiben, die keinen verblockten Zugriff auf den gemeinsamen Zähler erfordert.
%Vor%Dies ist jedoch immer noch im besten Fall mit der sequentiellen Schleife und im schlimmsten Fall ein paar Mal langsamer.
Ich habe das mit Stefan's Lösung (XE7 Tasks) und mit einem einfachen XE7 Parallel verglichen.Für mit verzahnten Inkrementen (XE7 for).
Ergebnisse von meinem Notebook mit 4 Hyperthread-Kernen:
Seriell: 49999640 ungerade Elemente in 543 ms gefunden
Parallel (OTL): 49999640 ungerade Elemente in 555 ms gefunden
Parallel (XE7-Tasks): 49999640 ungerade Elemente in 136 ms gefunden
Parallel (XE7 für): 49999640 ungerade Elemente in 1667 ms gefunden
Ergebnisse meiner Workstation mit 12 Hyperthread-Kernen:
Seriell: 50005291 ungerade Elemente in 685 ms gefunden
Parallel (OTL): 50005291 ungerade Elemente in 1309 ms gefunden
Parallel (XE7-Tasks): 50005291 ungerade Elemente in 62 ms gefunden
Parallel (XE7 für): 50005291 ungerade Elemente in 3379 ms gefunden
Es gibt eine große Verbesserung gegenüber System.Threading Paralell.Für, weil es keine verzahnte Schrittweite gibt, aber die handgefertigte Lösung ist viel viel schneller.
Vollständiges Testprogramm:
%Vor% Ich denke, wir haben das schon einmal über OmniThreadLibrary diskutiert. Die Hauptursache für die Zeit, die für die Multithread-Lösung länger ist, ist der Overhead von TParallel.For
im Vergleich zu der Zeit, die für die tatsächliche Berechnung benötigt wird.
Eine lokale Variable hilft hier nicht, während eine globale threadvar
das Problem der falschen Freigabe lösen kann. Ach, vielleicht findest du keine Möglichkeit, all diese Schritte nach Abschluss der Schleife zusammenzufassen.
IIRC, der beste Ansatz besteht darin, die Aufgabe in vernünftigen Teilen zu zerlegen und für jede Iteration eine Reihe von Array-Einträgen zu bearbeiten und eine Variable zu erhöhen, die diesem Teil gewidmet ist. Das allein löst das Problem des falschen Teilens nicht, wie es selbst bei bestimmten Variablen auftritt, wenn sie zufällig nur Teil derselben Cache-Zeile sind.
Eine andere Lösung könnte darin bestehen, eine Klasse zu schreiben, die eine bestimmte Teilmenge des Arrays seriell behandelt, parallel auf mehrere Instanzen dieser Klasse einwirkt und anschließend die Ergebnisse auswertet.
BTW: Ihr Code zählt nicht die Chancen - es zählt die Evens.
Und: Es gibt eine eingebaute Funktion namens Odd
, die normalerweise eine bessere Leistung hat als der mod
Code, den Sie verwenden.
Ok, ich habe, inspiriert von Stefan Glienkes Antwort, eine wiederverwendbare TParalleEx-Klasse entworfen, die anstelle von ITasks IFutures verwendet. Die Klasse ist auch etwas nach dem C # -TPL mit einem Aggregationsdelegaten modelliert. Dies ist nur ein erster Entwurf, zeigt aber, wie der vorhandene PPL relativ einfach erweitert werden kann. Diese Version skaliert jetzt perfekt auf meinem System - ich würde mich freuen, wenn andere es auf verschiedenen Konfigurationen testen könnten. Danke an alle für Ihre fruchtbaren Antworten und Kommentare.
%Vor%In Bezug auf die Aufgabe, lokale Variablen zu verwenden, um die Summen zu sammeln und sie dann am Ende zu sammeln, können Sie ein separates Array für diesen Zweck verwenden:
%Vor%Tags und Links multithreading delphi parallel-processing delphi-xe7