Wenn in Umgebungen mit gemeinsam genutztem Speicher (z. B. Threading über OpenMP, Pthreads oder TBB) Berechnungen mit begrenzter Speicherbandbreite durchgeführt werden, besteht ein Dilemma dahingehend, wie sichergestellt werden kann, dass der Speicher korrekt auf den physischen Speicher verteilt wird, sodass jeder Thread hauptsächlich auf einen Speicher zugreift "lokaler" Speicherbus. Obwohl die Schnittstellen nicht portabel sind, haben die meisten Betriebssysteme Möglichkeiten, die Thread-Affinität festzulegen (z. B. pthread_setaffinity_np()
auf vielen POSIX-Systemen, sched_setaffinity()
unter Linux, SetThreadAffinityMask()
unter Windows). Es gibt auch Bibliotheken wie hwloc zum Bestimmen der Speicherhierarchie, aber leider bieten die meisten Betriebssysteme noch keine Möglichkeiten zum Festlegen von NUMA-Speicherrichtlinien. Linux ist mit libnuma eine bemerkenswerte AusnahmeErmöglichen, dass die Anwendung die Speicherrichtlinie und die Seitenmigration mit der Seitengranularität manipuliert (seit 2004 in der Hauptzeile, daher weit verbreitet). Andere Betriebssysteme erwarten, dass Benutzer eine implizite "First Touch" -Richtlinie einhalten.
Das Arbeiten mit einer "First Touch" -Richtlinie bedeutet, dass der Aufrufer Threads mit der Affinität erstellen und verteilen soll, die er später beim ersten Schreiben in den neu zugewiesenen Speicher verwenden möchte. (Nur sehr wenige Systeme sind so konfiguriert, dass sie malloc()
tatsächlich Seiten finden. Es wird lediglich versprochen, sie zu finden, wenn sie tatsächlich fehlerhaft sind, möglicherweise durch verschiedene Threads.) Dies impliziert, dass die Zuweisung mit calloc()
oder die sofortige Initialisierung des Speichers nach der Zuweisung mit memset()
schädlich ist, da dies zu Fehlern führen kann Der gesamte Speicher auf dem Speicherbus des Kerns, auf dem der Zuordnungsthread ausgeführt wird, führt zu einer Speicherbandbreite im ungünstigsten Fall, wenn auf den Speicher von mehreren Threads aus zugegriffen wird. Gleiches gilt für den C ++ - new
Operator, der darauf besteht, viele neue Zuordnungen zu initialisieren (zstd::complex
). Einige Beobachtungen zu dieser Umgebung:
- Die Zuweisung kann als "Thread-Kollektiv" erfolgen, aber die Zuweisung wird nun in das Threading-Modell gemischt, was für Bibliotheken unerwünscht ist, die mit Clients unter Verwendung verschiedener Threading-Modelle interagieren müssen (möglicherweise mit jeweils eigenen Thread-Pools).
- RAII wird als wichtiger Bestandteil von idiomatischem C ++ angesehen, scheint jedoch die Speicherleistung in einer NUMA-Umgebung aktiv zu beeinträchtigen. Die Platzierung
new
kann mit dem übermalloc()
oder von zugewiesenen Speicher verwendet werdenlibnuma
, dies ändert jedoch den Zuweisungsprozess (was ich für notwendig halte). - BEARBEITEN: Meine frühere Aussage zum Operator
new
war falsch, er kann mehrere Argumente unterstützen, siehe Chetans Antwort. Ich glaube, es gibt immer noch Bedenken, Bibliotheken oder STL-Container dazu zu bringen, eine bestimmte Affinität zu verwenden. Es können mehrere Felder gepackt sein, und es kann unpraktisch sein, sicherzustellen, dass z. B. einestd::vector
Neuzuweisung mit dem richtigen aktiven Kontextmanager erfolgt. - Jeder Thread kann seinen eigenen privaten Speicher zuordnen und fehlerhaft behandeln, aber dann ist die Indizierung in benachbarte Regionen komplizierter. (Betrachten Sie ein dünn besetztes Matrixvektorprodukt mit einer Zeilenpartition der Matrix und der Vektoren. Die Indizierung des nicht besetzten Teils von x erfordert eine kompliziertere Datenstruktur, wenn x im virtuellen Speicher nicht zusammenhängend ist.)
Werden Lösungen für die NUMA-Zuweisung / -Initialisierung als idiomatisch betrachtet? Habe ich andere kritische Fallstricke ausgelassen?
(Ich meine nicht, dass meine C ++ - Beispiele eine Betonung dieser Sprache implizieren, jedoch codiert die C ++ - Sprache einige Entscheidungen über die Speicherverwaltung, die eine Sprache wie C nicht enthält. Daher besteht tendenziell ein größerer Widerstand, wenn vorgeschlagen wird, dass C ++ - Programmierer diese ausführen Dinge anders.)
quelle
Diese Antwort ist eine Antwort auf zwei C ++ - bezogene Missverständnisse in der Frage.
Es ist keine direkte Antwort auf von Ihnen erwähnte Multi-Core-Probleme. Nur auf Kommentare reagieren, die C ++ - Programmierer als C ++ - Fanatiker klassifizieren, damit die Reputation erhalten bleibt;).
Zu Punkt 1. C ++ "new" oder Stack Allocation bestehen nicht darauf, neue Objekte zu initialisieren, egal ob PODs oder nicht. Der vom Benutzer definierte Standardkonstruktor der Klasse trägt diese Verantwortung. Der erste Code unten zeigt Junk-Print, ob die Klasse POD ist oder nicht.
Zu Punkt 2. C ++ erlaubt das Überladen von "new" mit mehreren Argumenten. Der zweite Code unten zeigt einen solchen Fall für die Zuordnung einzelner Objekte. Es sollte eine Idee geben und möglicherweise für die Situation nützlich sein, die Sie haben. operator new [] kann ebenfalls entsprechend geändert werden.
// Code für Punkt 1.
Intels 11.1-Compiler zeigt diese Ausgabe (was natürlich nicht initialisierter Speicher ist, auf den "a" zeigt).
// Code für Punkt 2.
quelle
std::complex
die sind explizit initialisiert.std::complex
?In deal.II haben wir die Software-Infrastruktur, um die Assemblierung jeder Zelle auf mehrere Kerne mithilfe der Threading-Bausteine zu parallelisieren (im Wesentlichen haben Sie eine Aufgabe pro Zelle und müssen diese Aufgaben auf verfügbaren Prozessoren planen - so ist es nicht implementiert, aber es ist die allgemeine Idee). Das Problem ist, dass Sie für die lokale Integration eine Reihe von temporären Objekten (Scratch-Objekten) benötigen und mindestens so viele bereitstellen müssen, wie Tasks parallel ausgeführt werden können. Wir sehen eine schlechte Beschleunigung, wahrscheinlich, weil eine Aufgabe, wenn sie auf einen Prozessor gelegt wird, eines der Arbeitsobjekte erfasst, die sich normalerweise im Cache eines anderen Kerns befinden. Wir hatten zwei Fragen:
(i) Ist das wirklich der Grund? Wenn wir das Programm unter Cachegrind ausführen, sehe ich, dass ich im Grunde die gleiche Anzahl von Anweisungen verwende wie beim Ausführen des Programms auf einem einzelnen Thread, aber die Gesamtlaufzeit, die über alle Threads angesammelt ist, ist viel größer als die auf einem einzelnen Thread. Liegt es wirklich daran, dass ich ständig Fehler im Cache habe?
(ii) Wie kann ich herausfinden, wo ich bin, wo sich die einzelnen Arbeitsobjekte befinden und welches Arbeitsobjekt ich benötigen würde, um auf das Objekt zuzugreifen, das sich im Cache meines aktuellen Kerns befindet?
Letztendlich haben wir keine Antwort auf eine dieser Lösungen gefunden und nach einigen Arbeiten festgestellt, dass uns die Werkzeuge fehlen, um diese Probleme zu untersuchen und zu lösen. Ich weiß, wie man das Problem (ii) zumindest im Prinzip löst (und zwar unter der Annahme, dass Threads an Prozessorkerne gebunden bleiben - eine andere Vermutung, die nicht trivial zu testen ist), aber ich habe keine Tools zum Testen des Problems (ich).
Aus unserer Sicht ist der Umgang mit NUMA immer noch eine ungelöste Frage.
quelle
Über hwloc hinaus gibt es einige Tools, die Berichte zur Speicherumgebung eines HPC-Clusters erstellen und zum Festlegen verschiedener NUMA-Konfigurationen verwendet werden können.
Ich würde LIKWID als ein solches Tool empfehlen, da es einen Code-basierten Ansatz vermeidet, mit dem Sie beispielsweise einen Prozess an einen Kern anheften können. Mit diesem Tool-Ansatz zur Adressierung der rechnerspezifischen Speicherkonfiguration können Sie die Portabilität Ihres Codes über Cluster hinweg sicherstellen.
Sie finden eine kurze Präsentation aus ISC'13 " LIKWID - Lightweight Performance Tools ", und die Autoren haben einen Artikel über Arxiv " Best Practices für HPM-unterstütztes Performance Engineering auf modernen Multicore-Prozessoren " veröffentlicht. In diesem Dokument wird ein Ansatz zur Interpretation der Daten von Hardwarezählern beschrieben, um einen performanten Code zu entwickeln, der für die Architektur und die Speichertopologie Ihres Computers spezifisch ist.
quelle