Nichtlineare Skalierung von .NET-Operationen auf Multi-Core-Rechnern

8

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:

  1. Der Overhead aller Techniken (Thread-Context-Switches usw.) ist überwältigend für die Berechnungen
  2. Die Threads werden nicht jedem der vier Kerne zugewiesen und verbringen einige Zeit damit, auf denselben Prozessorkern zu warten. Ich bin nicht sicher, wie ich diese Theorie testen soll ...
  3. .NET CLR-Threads laufen nicht mit der erwarteten Priorität oder haben einen versteckten internen Overhead.

Im Folgenden finden Sie einen repräsentativen Auszug aus dem Code, der das gleiche Verhalten aufweisen sollte:

%Vor%     
LBushkin 20.09.2009, 00:13
quelle

5 Antworten

5

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() :

%Vor%

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.

    
LBushkin 21.09.2009, 03:10
quelle
5

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.

    
Igor ostrovsky 20.09.2009 19:03
quelle
3

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:

  • Die CPU und das Betriebssystem können nicht alle Zeit für Ihre Anwendung aufwenden. Daher muss es immer wieder Kontextwechsel durchführen, damit andere Prozesse etwas Arbeit erledigen können. Wenn Sie nur einen einzelnen Kern verwenden, ist es weniger wahrscheinlich, dass Ihr Prozess ausgetauscht wird, da drei andere Kerne zur Auswahl stehen. Beachten Sie, dass, obwohl Sie vielleicht denken, dass nichts anderes läuft, das Betriebssystem oder einige Dienste noch einige Hintergrundarbeiten ausführen könnten.
  • Wenn jeder Ihrer Threads auf viele Daten zugreift und diese Daten zwischen Threads nicht üblich sind, werden Sie wahrscheinlich nicht in der Lage sein, all dies im CPU-Cache zu speichern. Das bedeutet, dass viel mehr Speicherzugriff erforderlich ist, was (relativ) langsam ist.

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.

    
Michael Madsen 20.09.2009 00:58
quelle
0

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.

  • Verwenden Sie gemeinsame Datenstrukturen (explizit oder implizit), die eine Synchronisierung erfordern?
  • Haben Sie versucht, Leistungsindikatoren zu erstellen oder zu protokollieren, um festzustellen, wo der Engpass liegt? Kannst du noch mehr Hinweise geben?

Bearbeiten: Entschuldigung, ich habe gerade bemerkt, dass Sie beide Punkte bereits angesprochen haben.

    
Brian Gideon 20.09.2009 00:31
quelle
0

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?

    
user141682 16.01.2010 12:16
quelle