Was sollte jeder Programmierer über Speicher wissen?


163

Ich frage mich, wie viel von Ulrich Dreppers Was jeder Programmierer über Speicher aus dem Jahr 2007 wissen sollte, noch gültig ist. Außerdem konnte ich keine neuere Version als 1.0 oder Errata finden.


1
Weiß jemand, ob ich diesen Artikel irgendwo im Mobi-Format herunterladen kann, damit ich ihn einfach auf Kindle lesen kann? "pdf" ist aufgrund von Problemen mit Zoom / Formatierung sehr schwer zu lesen
javapowered

1
Es ist kein Mobi, aber LWN hat das Papier als eine Reihe von Artikeln ausgeführt, die auf einem Telefon / Tablet leichter zu lesen sind. Der erste ist bei lwn.net/Articles/250967
Nathan

Antworten:


111

Soweit ich mich erinnere, beschreibt Dreppers Inhalt grundlegende Konzepte zum Speicher: Wie funktioniert der CPU-Cache, was ist physischer und virtueller Speicher und wie behandelt der Linux-Kernel diesen Zoo? Wahrscheinlich gibt es in einigen Beispielen veraltete API-Referenzen, aber das spielt keine Rolle. Dies hat keinen Einfluss auf die Relevanz der grundlegenden Konzepte.

Ein Buch oder ein Artikel, der etwas Grundlegendes beschreibt, kann daher nicht als veraltet bezeichnet werden. "Was jeder Programmierer über das Gedächtnis wissen sollte" ist definitiv lesenswert, aber ich denke nicht, dass es für "jeden Programmierer" ist. Es ist besser für System- / Embedded- / Kernel-Leute geeignet.


3
Ja, ich verstehe wirklich nicht, warum ein Programmierer wissen muss, wie SRAM und DRAM auf analoger Ebene funktionieren - das hilft beim Schreiben von Programmen nicht viel. Und Leute, die dieses Wissen wirklich brauchen, verbringen die Zeit besser damit, die Handbücher über Details zu den tatsächlichen Timings usw. zu lesen. Aber für Leute, die sich für das HW-Low-Level-Zeug interessieren? Vielleicht nicht nützlich, aber zumindest unterhaltsam.
Voo

47
Heutzutage ist Leistung == Speicherleistung, daher ist das Verständnis des Speichers das Wichtigste in jeder Hochleistungsanwendung. Dies macht das Papier für alle Beteiligten unverzichtbar: Spieleentwicklung, wissenschaftliches Rechnen, Finanzen, Datenbanken, Compiler, Verarbeitung großer Datenmengen, Visualisierung, alles, was mit vielen Anforderungen zu tun hat ... Also ja, wenn Sie in einer Anwendung arbeiten Das ist die meiste Zeit untätig, wie bei einem Texteditor. Das Papier ist völlig uninteressant, bis Sie schnell etwas tun müssen, z. B. ein Wort finden, die Wörter zählen, die Rechtschreibprüfung durchführen ... oh, warte ... vergiss es.
Gnzlbg

144

Der Leitfaden in PDF-Form befindet sich unter https://www.akkadia.org/drepper/cpumemory.pdf .

Es ist im Allgemeinen immer noch ausgezeichnet und sehr zu empfehlen (von mir und anderen Experten für Leistungsoptimierung). Es wäre cool, wenn Ulrich (oder jemand anderes) ein Update für 2017 schreiben würde, aber das wäre eine Menge Arbeit (z. B. das erneute Ausführen der Benchmarks). Siehe auch andere Links zur x86-Leistungsoptimierung und SSE / asm- (und C / C ++) Optimierung in der Tag Wiki . (Ulrichs Artikel ist nicht x86-spezifisch, aber die meisten (alle) seiner Benchmarks beziehen sich auf x86-Hardware.)

Die einfachen Hardwaredetails zur Funktionsweise von DRAM und Caches gelten weiterhin . DDR4 verwendet dieselben Befehle wie für DDR1 / DDR2 (Lese- / Schreibburst) beschrieben. Die DDR3 / 4-Verbesserungen sind keine grundlegenden Änderungen. AFAIK, alle archunabhängigen Dinge gelten immer noch allgemein, z. B. für AArch64 / ARM32.

Siehe auch die Latenz Bound Platforms Abschnitt dieser Antwort für wichtige Details über die Wirkung von Speicher / L3 Latenz auf Single-Threaded - Bandbreite: bandwidth <= max_concurrency / latencyund das ist eigentlich die primäre Engpass für Single-Threaded - Bandbreite auf einem modernen Mehrkern - CPU wie ein Xeon . Ein Quad-Core-Skylake-Desktop kann jedoch die DRAM-Bandbreite mit einem einzigen Thread nahezu ausschöpfen. Dieser Link enthält einige sehr gute Informationen zu NT-Stores im Vergleich zu normalen Stores auf x86. Warum ist Skylake für den Single-Threaded-Speicherdurchsatz so viel besser als Broadwell-E? ist eine Zusammenfassung.

Daher ist Ulrichs Vorschlag in 6.5.8, die gesamte Bandbreite für die Verwendung von Remote-Speicher auf anderen NUMA-Knoten sowie auf Ihrem eigenen zu verwenden, auf moderner Hardware kontraproduktiv, bei der Speichercontroller mehr Bandbreite haben, als ein einzelner Kern verwenden kann. Möglicherweise können Sie sich eine Situation vorstellen, in der es von Vorteil ist, mehrere speicherhungrige Threads auf demselben NUMA-Knoten für die Kommunikation zwischen Threads mit geringer Latenz auszuführen, diese jedoch Remote-Speicher für nicht latenzempfindliche Inhalte mit hoher Bandbreite verwenden. Dies ist jedoch ziemlich unklar. Normalerweise teilen Sie Threads nur zwischen NUMA-Knoten auf und lassen sie lokalen Speicher verwenden. Die Bandbreite pro Kern reagiert aufgrund der maximalen Parallelität (siehe unten) empfindlich auf Latenz, aber alle Kerne in einem Socket können die Speichercontroller in diesem Socket normalerweise mehr als überlasten.


(normalerweise) Verwenden Sie keinen Software-Prefetch

Eine wichtige Änderung ist, dass das Hardware-Prefetch viel besser ist als beim Pentium 4 und schrittweise Zugriffsmuster bis zu einem relativ großen Schritt und mehrere Streams gleichzeitig erkennen kann (z. B. ein Vorwärts- / Rückwärtslauf pro 4 KB-Seite). Das Optimierungshandbuch von Intel beschreibt einige Details der HW-Prefetchers in verschiedenen Cache-Ebenen für ihre Mikroarchitektur der Sandybridge-Familie. Ivybridge und später haben Hardware-Prefetch auf der nächsten Seite, anstatt auf einen Cache-Fehler auf der neuen Seite zu warten, um einen Schnellstart auszulösen. Ich gehe davon aus, dass AMD einige ähnliche Dinge in seinem Optimierungshandbuch hat. Beachten Sie, dass das Handbuch von Intel auch viele alte Ratschläge enthält, von denen einige nur für P4 geeignet sind. Die Sandybridge-spezifischen Abschnitte sind natürlich für SnB genau, aber zDie Unlaminierung von mikroverschmolzenen Uops wurde in HSW geändert und im Handbuch nicht erwähnt .

Heutzutage ist es üblich, den gesamten SW-Prefetch aus dem alten Code zu entfernen und ihn nur dann wieder einzufügen, wenn bei der Profilerstellung Cache-Fehler auftreten (und die Speicherbandbreite nicht gesättigt ist). Das Vorabrufen beider Seiten des nächsten Schritts einer binären Suche kann weiterhin hilfreich sein. Wenn Sie beispielsweise entschieden haben, welches Element als Nächstes betrachtet werden soll, rufen Sie die 1/4 und 3/4 Elemente vorab ab, damit sie parallel zum Laden / Überprüfen der Mitte geladen werden können.

Der Vorschlag, einen separaten Prefetch-Thread (6.3.4) zu verwenden, ist meiner Meinung nach völlig veraltet und war auf Pentium 4 immer nur gut. P4 hatte Hyperthreading (2 logische Kerne, die sich einen physischen Kern teilen), aber nicht genügend Trace-Cache (und / oder Ausführungsressourcen außerhalb der Reihenfolge), um den Durchsatz zu erzielen, indem zwei vollständige Berechnungsthreads auf demselben Kern ausgeführt werden. Moderne CPUs (Sandybridge-Familie und Ryzen) sind jedoch viel leistungsfähiger und sollten entweder einen echten Thread ausführen oder kein Hyperthreading verwenden (lassen Sie den anderen logischen Kern im Leerlauf, damit der Solo-Thread über die vollen Ressourcen verfügt, anstatt den ROB zu partitionieren).

Software-Prefetch war schon immer "spröde" : Die richtigen magischen Tuning-Zahlen für eine Beschleunigung hängen von den Details der Hardware und möglicherweise von der Systemlast ab. Zu früh und es wird vor der Bedarfslast vertrieben. Zu spät und es hilft nicht. Dieser Blog-Artikel zeigt Code + Diagramme für ein interessantes Experiment zur Verwendung des SW-Prefetch auf Haswell zum Prefetch des nicht sequentiellen Teils eines Problems. Siehe auch Wie verwende ich Prefetch-Anweisungen richtig? . NT-Prefetch ist interessant, aber noch spröder, weil eine frühe Räumung von L1 bedeutet, dass Sie den ganzen Weg zu L3 oder DRAM gehen müssen, nicht nur zu L2. Wenn Sie den letzten Leistungsabfall benötigen und sich auf einen bestimmten Computer einstellen können , sollten Sie sich den SW-Prefetch für den sequentiellen Zugriff ansehen, aber es ist wichtigDies kann immer noch eine Verlangsamung sein, wenn Sie über genügend ALU-Arbeit verfügen, während Sie sich einem Speicherengpass nähern.


Die Cache-Zeilengröße beträgt immer noch 64 Byte. (Die Lese- / Schreibbandbreite von L1D ist sehr hoch, und moderne CPUs können 2 Vektorladungen pro Takt + 1 Vektorspeicher ausführen, wenn alles in L1D trifft. Siehe Wie kann der Cache so schnell sein? ) Mit AVX512 ist Zeilengröße = Vektorbreite, So können Sie eine gesamte Cache-Zeile in einer Anweisung laden / speichern. Somit überschreitet jedes falsch ausgerichtete Laden / Speichern eine Cache-Zeilengrenze anstelle jeder anderen für 256b AVX1 / AVX2, wodurch die Schleife über ein Array, das nicht in L1D enthalten war, häufig nicht verlangsamt wird.

Nicht ausgerichtete Ladeanweisungen haben keine Strafe, wenn die Adresse zur Laufzeit ausgerichtet ist, aber Compiler (insbesondere gcc) liefern beim Autovektorieren besseren Code, wenn sie über Ausrichtungsgarantien Bescheid wissen. Tatsächlich sind nicht ausgerichtete Operationen im Allgemeinen schnell, aber Seitenaufteilungen tun immer noch weh (viel weniger bei Skylake; nur ~ 11 zusätzliche Zyklen Latenz gegenüber 100, aber immer noch eine Durchsatzstrafe).


Wie Ulrich vorausgesagt hat, ist heutzutage jedes Multi-Socket- System NUMA: Integrierte Speichercontroller sind Standard, dh es gibt keine externe Northbridge. SMP bedeutet jedoch nicht mehr Multi-Socket, da Multi-Core-CPUs weit verbreitet sind. Intel-CPUs von Nehalem bis Skylake haben einen großen inklusive L3-Cache als Backstop für die Kohärenz zwischen Kernen verwendet. AMD-CPUs sind unterschiedlich, aber die Details sind mir nicht so klar.

Skylake-X (AVX512) hat kein integratives L3 mehr, aber ich denke, es gibt immer noch ein Tag-Verzeichnis, mit dem überprüft werden kann, was irgendwo auf dem Chip zwischengespeichert ist (und wenn ja, wo), ohne tatsächlich Snoops an alle Kerne zu senden. SKX verwendet eher ein Netz als einen Ringbus , wobei die Latenz im Allgemeinen leider noch schlechter ist als bei früheren Xeons mit vielen Kernen.

Grundsätzlich gelten weiterhin alle Ratschläge zur Optimierung der Speicherplatzierung. Nur die Details darüber, was genau passiert, wenn Sie Cache-Fehler oder Konflikte nicht vermeiden können, variieren.


6.4.2 Atomic Ops : Der Benchmark, der eine CAS-Wiederholungsschleife als 4x schlechter als Hardware-Arbitrated lock addzeigt, spiegelt wahrscheinlich immer noch einen maximalen Konfliktfall wider . In echten Multithread-Programmen wird die Synchronisation jedoch auf ein Minimum beschränkt (weil sie teuer ist), sodass die Konkurrenz gering ist und eine CAS-Wiederholungsschleife normalerweise erfolgreich ist, ohne dass ein erneuter Versuch erforderlich ist.

C ++ 11 std::atomic fetch_addwird zu a kompiliert lock add(oder lock xaddwenn der Rückgabewert verwendet wird), aber ein Algorithmus, der CAS verwendet, um etwas zu tun, das mit einer locked-Anweisung nicht möglich ist, ist normalerweise keine Katastrophe. Verwenden Sie C ++ 11std::atomic oder C11 stdatomicanstelle von gcc Legacy __sync-integrierten oder neueren __atomicintegrierten Funktionen, es sei denn, Sie möchten atomaren und nichtatomaren Zugriff auf denselben Speicherort mischen ...

8.1 DWCAS ( cmpxchg16b) : Sie können gcc dazu überreden , es zu emittieren. Wenn Sie jedoch nur eine Hälfte des Objekts effizient laden möchten, benötigen Sie hässliche unionHacks: Wie kann ich einen ABA-Zähler mit c ++ 11 CAS implementieren? . (Verwechseln Sie DWCAS nicht mit DCAS mit 2 separaten Speicherorten . Eine sperrfreie atomare Emulation von DCAS ist mit DWCAS nicht möglich, aber Transaktionsspeicher (wie x86 TSX) machen es möglich.)

8.2.4 Transaktionsspeicher : Nach einigen Fehlstarts (freigegeben und dann durch ein Mikrocode-Update aufgrund eines selten ausgelösten Fehlers deaktiviert) verfügt Intel über einen funktionierenden Transaktionsspeicher in Broadwell und allen Skylake-CPUs der neuesten Generation . Das Design ist immer noch das, was David Kanter für Haswell beschrieben hat . Es gibt eine Lock-Ellision-Methode, um Code zu beschleunigen, der eine reguläre Sperre verwendet (und auf diese zurückgreifen kann) (insbesondere mit einer einzigen Sperre für alle Elemente eines Containers, sodass mehrere Threads in demselben kritischen Abschnitt häufig nicht kollidieren ) oder um Code zu schreiben, der sich direkt mit Transaktionen auskennt.


7.5 Riesenseiten : Anonyme transparente Riesenseiten funktionieren unter Linux gut, ohne hugetlbfs manuell verwenden zu müssen. Machen Sie Zuweisungen> = 2MiB mit 2MiB-Ausrichtung (z. B. posix_memalignoder einealigned_alloc , die die dumme ISO C ++ 17-Anforderung nicht erzwingt, wenn sie fehlschlägt size % alignment != 0).

Bei einer anonymen Zuweisung mit 2 MB ausgerichtet werden standardmäßig große Seiten verwendet. Einige Workloads (z. B. die nach dem Erstellen noch eine Weile große Zuordnungen verwenden) können davon profitieren
echo always >/sys/kernel/mm/transparent_hugepage/defrag, dass der Kernel den physischen Speicher bei Bedarf defragmentiert, anstatt auf 4 KB-Seiten zurückzugreifen. (Siehe die Kernel-Dokumente ). Alternativ können Sie madvise(MADV_HUGEPAGE)nach großen Zuweisungen verwenden (vorzugsweise noch mit 2 MB Ausrichtung).


Anhang B: Oprofil : Linux perfhat größtenteils abgelöst oprofile. Verwenden Sie den ocperf.pyWrapper für detaillierte Ereignisse, die für bestimmte Mikroarchitekturen spezifisch sind . z.B

ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out

Einige Beispiele für die Verwendung finden Sie unter Kann der MOV von x86 wirklich "kostenlos" sein? Warum kann ich das überhaupt nicht reproduzieren? .


3
Sehr lehrreiche Antwort und Hinweise! Dies verdient eindeutig mehr Stimmen!
Claf

@Peter Cordes Gibt es andere Anleitungen / Papiere, die Sie zum Lesen empfehlen würden? Ich bin kein Hochleistungsprogrammierer, aber ich möchte mehr darüber erfahren und hoffentlich Praktiken aufgreifen, die ich in meine tägliche Programmierung integrieren kann.
user3927312

4
@ user3927312: agner.org/optimize ist eine der besten und kohärentesten Anleitungen für Low-Level- Inhalte speziell für x86, aber einige der allgemeinen Ideen gelten auch für andere ISAs. Agner verfügt neben asm-Anleitungen über ein optimierendes C ++ - PDF. Weitere Links zu Leistung und CPU-Architektur finden Sie unter stackoverflow.com/tags/x86/info . Ich habe auch einige Informationen zur Optimierung von C ++ geschrieben, indem ich dem Compiler geholfen habe, einen besseren Asm für kritische Schleifen zu erstellen,
Peter Cordes

74

Von meinem kurzen Blick durch sieht es ziemlich genau aus. Das einzige, was zu beachten ist, ist der Anteil am Unterschied zwischen "integrierten" und "externen" Speichercontrollern. Seit der Veröffentlichung der i7-Reihe sind alle Intel-CPUs integriert, und AMD verwendet seit der ersten Veröffentlichung der AMD64-Chips integrierte Speichercontroller.

Seit dieser Artikel geschrieben wurde, hat sich nicht viel geändert, die Geschwindigkeit ist gestiegen, die Speichercontroller sind viel intelligenter geworden (der i7 verzögert das Schreiben in den Arbeitsspeicher, bis er die Änderungen festschreiben möchte), aber es hat sich nicht viel geändert . Zumindest nicht in einer Weise, die einem Softwareentwickler wichtig wäre.


5
Ich hätte euch beide gerne akzeptiert. Aber ich habe Ihren Beitrag positiv bewertet.
Framester

5
Die wahrscheinlich wichtigste Änderung, die für SW-Entwickler relevant ist, ist, dass Prefetch-Threads eine schlechte Idee sind. CPUs sind leistungsstark genug, um 2 vollständige Threads mit Hyperthreading auszuführen, und haben einen viel besseren HW-Prefetch. SW-Prefetch ist im Allgemeinen viel weniger wichtig, insbesondere für den sequentiellen Zugriff. Siehe meine Antwort.
Peter Cordes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.