wake-up-neo.com

Was sollte jeder Programmierer über Speicher wissen?

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

133
Framester

Soweit ich mich erinnere, beschreibt Dreppers Inhalt grundlegende Konzepte zum Thema Arbeitsspeicher: Wie der CPU-Cache funktioniert, was physischer und virtueller Arbeitsspeicher ist und wie der Linux-Kernel mit diesem Zoo umgeht. Wahrscheinlich gibt es in einigen Beispielen veraltete API-Referenzen, aber das spielt keine Rolle. das wird die Relevanz der grundlegenden Konzepte nicht beeinträchtigen.

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

93
Dan Kruchinin

Der Leitfaden in PDF in Form von https://www.akkadia.org/drepper/cpumemory.pdf .

Es ist im Allgemeinen immer noch exzellent und sehr zu empfehlen (von mir und ich denke von anderen Performance-Tuning-Experten). 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 Optimierung der x86-Leistung und zur Optimierung von SSE/asm (und C/C++) im x86Tag-Wiki . (Ulrichs Artikel ist nicht x86-spezifisch, aber die meisten seiner Benchmarks beziehen sich auf x86-Hardware.)

Die Hardware-Details auf niedriger Ebene zur Funktionsweise von DRAM und Caches gelten weiterhin . DDR4 verwendet dieselben Befehle wie für DDR1/DDR2 (Lese-/Schreib-Burst) beschrieben. Die Verbesserungen von DDR3/4 sind keine grundlegenden Änderungen. AFAIK, all das Arch-unabhängige Zeug gilt immer noch allgemein, z. zu AArch64/ARM32.

Siehe auch der Abschnitt Latenzzeitgebundene Plattformen in dieser Antwort für wichtige Details zu den Auswirkungen der Speicher-/L3-Latenzzeit auf Single-Threaded Bandbreite: bandwidth <= max_concurrency / latency, und dies ist tatsächlich der primäre Engpass für Single-Threaded-Bandbreite auf einer modernen Mehrkern-CPU wie einem Xeon. Ein Quad-Core-Skylake-Desktop kann jedoch fast die gesamte DRAM-Bandbreite mit einem einzigen Thread nutzen. Dieser Link enthält einige sehr gute Informationen zu NT-Stores im Vergleich zu normalen Stores auf x86. Warum ist Skylake für 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 des Remote-Speichers auf anderen NUMA-Knoten als auch auf Ihren eigenen zu nutzen, auf moderner Hardware kontraproduktiv Speichercontroller haben mehr Bandbreite, als ein einzelner Kern verwenden kann. Möglicherweise können Sie sich eine Situation vorstellen, in der mehrere speicherhungrige Threads auf demselben NUMA-Knoten für die Inter-Thread-Kommunikation mit geringer Latenz ausgeführt werden, der Remotespeicher jedoch für nicht latenzempfindliche Inhalte mit hoher Bandbreite verwendet wird. Dies ist jedoch ziemlich undurchsichtig. Normalerweise werden Threads nur zwischen NUMA-Knoten aufgeteilt und lokal gespeichert. Die Bandbreite pro Kern ist aufgrund der maximalen Parallelität (siehe unten) latenzempfindlich, aber alle Kerne in einem Sockel können in der Regel die Speichercontroller in diesem Sockel mehr als auslasten.


(in der Regel) Verwenden Sie keinen Software-Prefetch

Eine wichtige Änderung ist, dass der Hardware-Prefetch viel besser ist als beim Pentium 4 und schrittweise Zugriffsmuster erkennen kann bis zu einem ziemlich großen Schritt und mehreren Streams gleichzeitig (z. B. ein vorwärts/rückwärts pro 4k-Seite). Intels Optimierungshandbuch beschreibt einige Details der HW-Prefetchers in verschiedenen Cache-Ebenen für die Mikroarchitektur ihrer Sandybridge-Familie. Ivybridge und höher verfügen über Hardware-Prefetch für die nächste Seite, anstatt auf einen Cache-Fehler auf der neuen Seite zu warten, um einen Schnellstart auszulösen. Ich nehme an, AMD hat ähnliche Dinge in seinem Optimierungshandbuch. Beachten Sie, dass das Intel-Handbuch auch viele alte Ratschläge enthält, von denen einige nur für P4 gelten. Die Sandybridge-spezifischen Abschnitte sind natürlich für SnB genau, aber z. Nichtlaminierung von mikrogeschmolzenen Uops in HSW geändert und im Handbuch nicht erwähnt .

Heutzutage ist es üblich, alle SW-Vorablesezugriffe aus dem alten Code zu entfernen und sie nur dann wieder einzufügen, wenn bei der Profilerstellung Cache-Fehler auftreten (und Sie sind) Speicherbandbreite nicht auslasten). Das Vorabrufen beider Seiten des next Schritts einer binären Suche kann immer noch helfen. z.B. Sobald Sie sich entschieden haben, welches Element als nächstes angezeigt werden soll, rufen Sie die 1/4 und 3/4 Elemente vorab ab, damit sie parallel zum Laden/Prüfen der Mitte geladen werden können.

Der Vorschlag, einen separaten Prefetch-Thread (6.3.4) zu verwenden, ist meines Erachtens völlig veraltet und hat sich nur auf Pentium 4 bewährt. P4 hatte Hyperthreading (2 logische Kerne teilen sich einen physischen Kern), aber nicht genügend Trace-Cache (und/oder Ausführungsressourcen außerhalb der Reihenfolge), um Durchsatz zu erzielen, wenn zwei vollständige Berechnungsthreads auf demselben Kern ausgeführt werden. Moderne CPUs (Sandybridge-Familie und Ryzen) sind jedoch much bulliger 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 Partitionierung des ROB).

Der Software-Prefetch war schon immer "brüchig" : Die richtigen Magic-Tuning-Nummern für eine Beschleunigung hängen von den Details der Hardware und möglicherweise der Systemlast ab. Zu früh und es ist vor der Nachfrage Last geräumt. Zu spät und es hilft nicht. Dieser Blog-Artikel zeigt Code + Diagramme für ein interessantes Experiment zur Verwendung von SW-Prefetch in Haswell zum Prefetch des nicht-sequentiellen Teils eines Problems. Siehe auch Wie verwende ich Prefetch-Anweisungen richtig? . Der NT-Prefetch ist interessant, aber noch spröder, da bei einer frühen Räumung von L1 nicht nur L2, sondern L3 oder DRAM erreicht werden muss. Wenn Sie den letzten Tropfen der Leistung benötigen, und Sie können für eine bestimmte Maschine optimieren, SW-Prefetch lohnt sich für den sequentiellen Zugriff, aber es kann immer noch eine Verlangsamung sein, wenn Sie genug ALU-Arbeit zu tun haben, während Sie sich einem Engpass im Speicher nähern.


Die Cache-Zeilengröße beträgt immer noch 64 Byte. (L1D-Lese-/Schreibbandbreite ist sehr hoch, und moderne CPUs können 2 Vektorladevorgänge pro Takt + 1 Vektorspeicher ausführen, wenn alles in L1D auftritt. Siehe Wie kann Cache so schnell sein? .) Bei AVX512 ist Zeilengröße = Vektorbreite, sodass Sie eine gesamte Cache-Zeile in einem Befehl laden/speichern können. Somit überschreitet jedes falsch ausgerichtete Laden/Speichern bei 256b AVX1/AVX2 eine Cache-Zeilengrenze, anstatt jede andere, was das Schleifen über ein Array, das sich nicht in L1D befand, häufig nicht verlangsamt.

Anweisungen zum nicht ausgerichteten Laden haben keine Strafe, wenn die Adresse zur Laufzeit ausgerichtet wird, aber Compiler (insbesondere gcc) verbessern den Code beim automatischen Sektorisieren, wenn sie Ausrichtungsgarantien kennen. Tatsächlich sind unausgerichtete Operationen im Allgemeinen schnell, aber Seitenaufteilungen schaden immer noch (viel weniger bei Skylake; nur ~ 11 zusätzliche Zyklen Latenz gegenüber 100, aber immer noch eine Durchsatzstrafe).


Wie Ulrich vorausgesagt hat, ist jedes Multi-Socket System heutzutage NUMA: Integrierte Speichercontroller sind Standard, d. H. 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 den Kernen verwendet. AMD-CPUs sind unterschiedlich, aber die Details sind mir nicht so klar.

Skylake-X (AVX512) hat kein integriertes 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 dass Snoops tatsächlich an alle Kerne gesendet werden. SKX verwendet ein Mesh anstelle eines Ringbusses , mit im Allgemeinen noch schlechterer Latenz als bei früheren Xeons mit vielen Kernen, leider.

Grundsätzlich gelten weiterhin alle Ratschläge zur Optimierung der Speicherplatzierung. Es gibt jedoch Unterschiede in den Details, was genau passiert, wenn Sie Cache-Ausfälle oder Konflikte nicht vermeiden können.


6.4.2 Atomic Ops : Der Benchmark, der eine CAS-Wiederholungsschleife als 4-mal schlechter als durch Hardware arbitrierte lock add Anzeigt, spiegelt wahrscheinlich immer noch a wider Maximaler Konflikt . In echten Multithread-Programmen wird die Synchronisation jedoch auf ein Minimum beschränkt (weil sie teuer ist), sodass Konflikte gering sind und eine CAS-Wiederholungsschleife normalerweise erfolgreich ist, ohne dass erneut versucht werden muss.

C++ 11 std::atomicfetch_add Wird zu einem lock add Kompiliert (oder lock xadd, Wenn der Rückgabewert verwendet wird), aber ein Algorithmus, der CAS verwendet, um etwas zu tun Das kann mit einer Anweisung locked nicht gemacht werden, ist normalerweise keine Katastrophe. Verwenden Sie C++ 11 std::atomic oder C11 stdatomic anstelle der integrierten GCC-Legacy __sync) oder die neueren __atomic - eingebauten es sei denn, Sie möchten atomaren und nichtatomaren Zugriff auf denselben Ort mischen ...

8.1 DWCAS (cmpxchg16b) : Sie können gcc zum Aussenden überreden, aber wenn Sie nur eine Hälfte des Objekts effizient belasten möchten , du brauchst hässliche union Hacks: Wie kann ich ABA counter mit c ++ 11 CAS implementieren? . (Verwechseln Sie DWCAS nicht mit DCAS von 2 getrennt Speicherplätzen . Die sperrenfreie atomare Emulation von DCAS ist mit DWCAS nicht möglich, aber der Transaktionsspeicher (wie x86 TSX) macht es möglich.)

8.2.4 Transaktionsspeicher : Nach ein paar Fehlstarts (freigegeben und dann durch ein Mikrocode-Update aufgrund eines selten ausgelösten Fehlers deaktiviert) hat Intel Transaktionsarbeit geleistet Arbeitsspeicher in Broadwell-Modellen und allen Skylake-CPUs. Das Design ist noch was David Kanter für Haswell beschrieben hat . Es gibt eine Lock-Ellision-Methode, um den 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 im selben kritischen Abschnitt häufig nicht kollidieren ) oder um Code zu schreiben, der sich direkt mit Transaktionen auskennt.


7.5 Riesenseiten : Anonyme transparente Riesenseiten funktionieren gut unter Linux, ohne dass hugetlbfs manuell verwendet werden muss. Machen Sie Zuweisungen> = 2MiB mit 2MiB Ausrichtung (zB posix_memalign Oder ein aligned_alloc , das die dumme ISO C++ 17 Anforderung nicht erzwingt, wenn size % alignment != 0).

Bei einer anonymen Zuordnung mit einer Größe von 2 MB werden standardmäßig riesige Seiten verwendet. Einige Workloads (z. B., die nach ihrer Erstellung noch einige Zeit lang große Zuordnungen verwenden) können davon profitieren
echo always >/sys/kernel/mm/transparent_hugepage/defrag, damit der Kernel bei Bedarf den physischen Speicher defragmentiert, anstatt auf 4.000 Seiten zurückzugreifen. (Siehe die Kerneldokumentation ). Alternativ können Sie madvise(MADV_HUGEPAGE) verwenden, nachdem Sie umfangreiche Zuweisungen vorgenommen haben (vorzugsweise immer noch mit 2MiB-Ausrichtung).


Anhang B: Oprofile : Linux perf hat oprofile größtenteils abgelöst. Für detaillierte Ereignisse, die für bestimmte Mikroarchitekturen spezifisch sind, verwenden Sie den Wrapper ocperf.py . 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 "frei" sein? Warum kann ich das überhaupt nicht reproduzieren? .

93
Peter Cordes

Von meinem kurzen Blick durch sieht es ziemlich genau aus. Das Einzige, was zu bemerken ist, ist der Unterschied zwischen "integrierten" und "externen" Speichercontrollern. Seit dem Release der i7-Reihe sind alle Intel-CPUs integriert, und AMD verwendet seit der Einführung der AMD64-Chips integrierte Speichercontroller.

Seit dieser Artikel geschrieben wurde, hat sich nicht viel geändert, die Geschwindigkeiten sind höher geworden, die Speichercontroller sind viel intelligenter geworden (das i7 wird Schreibvorgänge auf RAM verzögern, bis es sich anfühlt, als würde es die Änderungen übernehmen) ), aber nicht viel hat sich geändert. Zumindest nicht in irgendeiner Weise, die ein Softwareentwickler interessieren würde.

70