Ich habe ein seltsames Verhalten in einer .NET-Anwendung festgestellt, die eine hochparallele Verarbeitung für eine Menge von speicherinternen Daten durchführt.
Wenn es auf einem Multi-Core-Prozessor (IntelCore2 Quad Q6600 2,4 GHz) ausgeführt wird, zeigt es eine nicht-lineare Skalierung, da mehrere Threads gestartet werden, um die Daten zu verarbeiten.
Wenn der Prozess als Multithread-Schleife auf einem einzelnen Kern ausgeführt wird, kann der Prozess ungefähr 2,4 Millionen Berechnungen pro Sekunde durchführen. Wenn man als vier Threads läuft, würde man viermal so viel Durchsatz erwarten - irgendwo in der Nähe von 9 Millionen Berechnungen pro Sekunde - aber leider nein. In der Praxis sind es nur etwa 4,1 Millionen pro Sekunde, etwas weniger als erwartet.
Außerdem tritt das Verhalten auf, egal ob ich PLINQ, einen Thread-Pool oder vier explizit erstellte Threads verwende. Ziemlich merkwürdig ...
Nichts läuft auf der Maschine mit CPU-Zeit, noch sind irgendwelche Sperren oder andere Synchronisationsobjekte in die Berechnung involviert ... es sollte nur die Daten durchbrechen. Ich habe dies (soweit möglich) bestätigt, indem ich während des Prozesses auf Perfmon-Daten geschaut habe ... und es wurden keine Thread-Konflikte oder Garbage Collection-Aktivitäten gemeldet.
Meine Theorien im Moment:
Im Folgenden finden Sie einen repräsentativen Auszug aus dem Code, der das gleiche Verhalten aufweisen sollte:
%Vor%Also habe ich endlich herausgefunden, was das Problem war - und ich denke, es wäre nützlich, es mit der SO-Community zu teilen.
Das gesamte Problem mit der nichtlinearen Leistung war das Ergebnis einer einzelnen Zeile innerhalb der Methode Evaluate()
:
Da Evaluate()
millionenfach aufgerufen wird, erfolgte diese Speicherzuordnung millionenfach. Wie es passiert, führt die CLR beim Zuweisen von Speicher intern eine gewisse Synchronisation zwischen den Threads durch - andernfalls könnte sich die Zuweisung auf mehreren Threads versehentlich überschneiden. Wenn das Array von einer methodenlokalen Instanz zu einer Klasseninstanz geändert wurde, die nur einmal zugewiesen wurde (aber dann in einer methodenlokalen Schleife initialisiert wurde), wurde das Problem der Skalierbarkeit behoben.
Normalerweise ist es ein Antipattern, um ein Mitglied auf Klassenebene für eine Variable zu erstellen, das nur im Rahmen einer einzelnen Methode verwendet (und sinnvoll) wird. Aber in diesem Fall, da ich die größtmögliche Skalierbarkeit benötige, werde ich mit dieser Optimierung leben (und dokumentieren).
Epilog: Nachdem ich diese Änderung vorgenommen hatte, konnte der gleichzeitige Prozess 12,2 Millionen Berechnungen pro Sekunde erreichen.
P.S. Ein großes Lob an Igor Ostrovsky für seine enge Verbindung zu MSDN-Blogs, die mir geholfen haben, das Problem zu identifizieren und zu diagnostizieren.
Sehen Sie sich diesen Artikel an: Ссылка
Beschränken Sie insbesondere Speicherzuordnungen im parallelen Bereich, und überprüfen Sie die Schreibvorgänge sorgfältig, um sicherzustellen, dass sie nicht in der Nähe von Speicherorten auftreten, die andere Threads lesen oder schreiben.
Bei einem parallelen Algorithmus ist im Vergleich zu einem sequentiellen Algorithmus eine nichtlineare Skalierung zu erwarten, da bei der Parallelisierung ein gewisser Overhead besteht. (Im Idealfall möchten Sie natürlich so nah wie möglich kommen.)
Zusätzlich gibt es normalerweise bestimmte Dinge, auf die Sie in einem parallelen Algorithmus achten müssen, die Sie in einem sequentiellen Algorithmus nicht benötigen. Jenseits der Synchronisation (die Ihre Arbeit wirklich behindern kann), gibt es noch einige andere Dinge, die passieren können:
Soweit ich das beurteilen kann, verwendet Ihre aktuelle explizite Methode einen gemeinsamen Iterator zwischen den Threads. Das ist eine gute Lösung, wenn die Verarbeitung innerhalb des Arrays stark variiert, aber wahrscheinlich ein Synchronisierungsaufwand besteht, um zu verhindern, dass ein Element übersprungen wird (das Abrufen des aktuellen Elements und das Verschieben des internen Zeigers zum nächsten Element muss eine atomare Operation sein) ein Element überspringen).
Daher könnte es eine bessere Idee sein, das Array zu partitionieren, vorausgesetzt, dass die Verarbeitungszeit jedes Elements unabhängig von der Position des Elements ungefähr gleich ist. Wenn Sie 10 Millionen Datensätze haben, bedeutet dies, Thread 1 mit Elementen 0 bis 2.499.999 zu arbeiten, Thread 2 mit Elementen 2.500.000 bis 4.999.999 usw. Sie können jedem Thread eine ID zuweisen und daraus den tatsächlichen Bereich berechnen.
Eine weitere kleine Verbesserung wäre, den Hauptthread als einen der berechneten Threads fungieren zu lassen. Wenn ich mich jedoch richtig erinnere, ist das eine sehr Kleinigkeit.
Ich würde sicherlich keine lineare Beziehung erwarten, aber ich hätte gedacht, dass Sie einen größeren Gewinn als das gesehen hätten. Ich gehe davon aus, dass die CPU-Auslastung auf allen Kernen ausgereizt ist. Nur ein paar Gedanken von meinem Kopf.
Bearbeiten: Entschuldigung, ich habe gerade bemerkt, dass Sie beide Punkte bereits angesprochen haben.
Ich habe hier eine ähnliche Frage gestellt: "Warum skaliert meine App mit Threads in .NET nicht linear, wenn große Speichermengen zugewiesen werden?"
Why doesn ' t meine Gewinde .Net App linear skalieren, wenn große Speichermengen zugewiesen werden?
Tags und Links c# linq performance parallel-processing plinq