Wie wichtig ist die Ausrichtung des Speichers? Ist es immer noch wichtig?

15

Seit einiger Zeit habe ich viel über Speicherausrichtung gesucht und gelesen, wie es funktioniert und wie man es benutzt. Der relevanteste Artikel, den ich derzeit gefunden habe, ist dieser .

Aber auch damit habe ich noch ein paar Fragen dazu:

  1. Außerhalb des eingebetteten Systems haben wir oft einen riesigen Speicherbereich in unserem Computer, der die Speicherverwaltung viel weniger kritisiert. Ich bin vollkommen in der Optimierung, aber jetzt ist es wirklich etwas, das den Unterschied ausmachen kann, wenn wir dasselbe Programm mit oder vergleichen ohne dass der Speicher neu angeordnet und ausgerichtet ist?
  2. Hat die Speicherausrichtung noch andere Vorteile? Ich habe irgendwo gelesen, dass die CPU mit ausgerichtetem Speicher besser / schneller arbeitet, weil für die Verarbeitung weniger Anweisungen erforderlich sind (wenn einer von Ihnen einen Link für einen Artikel / Benchmark dazu hat?). Ist in diesem Fall der Unterschied wirklich signifikant? Gibt es mehr Vorteile als diese beiden?
  3. In dem Artikel-Link in Kapitel 5 sagt der Autor:

    Achtung: In C ++ können Klassen, die wie Strukturen aussehen, gegen diese Regel verstoßen! (Ob dies der Fall ist oder nicht, hängt davon ab, wie Basisklassen und Funktionen für virtuelle Member implementiert sind und variiert je nach Compiler.)

  4. In dem Artikel geht es hauptsächlich um Strukturen. Ist die Deklaration lokaler Variablen auch von dieser Notwendigkeit betroffen?

    Haben Sie eine Vorstellung davon, wie die Speicherausrichtung in C ++ genau funktioniert, da es einige Unterschiede zu geben scheint?

Diese frühere Frage enthält das Wort "Ausrichtung", enthält jedoch keine Antworten auf die obigen Fragen.

Kane
quelle
C ++ - Compiler sind eher geneigt, dies zu tun (Polster einfügen, wo es benötigt wird oder von Vorteil ist). Suchen Sie unter dem von Ihnen erwähnten Link unter Abschnitt 12 "Tools" nach den Dingen, die Sie verwenden können.
Rwong

Antworten:

11

Ja, sowohl die Ausrichtung als auch die Anordnung Ihrer Daten können einen großen Unterschied in der Leistung bewirken, nicht nur einige Prozent, sondern einige bis viele Hundertstel Prozent.

Nehmen Sie diese Schleife, zwei Anweisungen sind wichtig, wenn Sie genügend Schleifen ausführen.

.globl ASMDELAY
ASMDELAY:
    subs r0,r0,#1
    bne ASMDELAY
    bx lr

Mit und ohne Cache und mit Ausrichtung mit und ohne Cache-Wurf in der Verzweigungsvorhersage können Sie die Leistung dieser beiden Befehle erheblich variieren (Timer-Ticks):

min      max      difference
00016DDE 003E025D 003C947F

Einen Leistungstest können Sie ganz einfach selbst durchführen. Fügen Sie Nops um den zu testenden Code hinzu oder entfernen Sie sie, und führen Sie die zu testenden Anweisungen in einem ausreichend großen Adressbereich aus, um die Ränder der Cache-Zeilen usw. zu berühren.

Ähnliches gilt für Datenzugriffe. Einige Architekturen beschweren sich über nicht ausgerichtete Zugriffe (z. B. 32-Bit-Lesevorgänge an Adresse 0x1001), indem sie Ihnen einen Datenfehler melden. In einigen Fällen können Sie den Fehler deaktivieren und den Leistungseinbruch verkraften. Bei anderen, die nicht ausgerichtete Zugriffe zulassen, wird die Leistung nur beeinträchtigt.

Es sind manchmal "Anweisungen", aber die meiste Zeit sind es Takt- / Buszyklen.

Schauen Sie sich die memcpy-Implementierungen in gcc für verschiedene Ziele an. Angenommen, Sie kopieren eine Struktur mit 0x43 Bytes. Möglicherweise finden Sie eine Implementierung, die ein Byte kopiert und dabei 0x42 belässt. Anschließend werden 0x40 Bytes in großen, effizienten Blöcken kopiert. Anschließend wird das letzte 0x2 als zwei einzelne Bytes oder als 16-Bit-Übertragung ausgeführt. Ausrichtung und Ziel spielen eine Rolle, wenn sich Quell- und Zieladresse auf derselben Ausrichtung befinden, z. B. 0x1003 und 0x2003. Dann könnten Sie das eine Byte, dann 0x40 in großen Blöcken, dann 0x2, aber wenn eines 0x1002 und das andere 0x1003 ist, dann wird es sehr hässlich und sehr langsam.

Meistens sind es Buszyklen. Oder schlimmer die Anzahl der Überweisungen. Nehmen Sie einen Prozessor mit einem 64-Bit-breiten Datenbus wie ARM und führen Sie eine 4-Wort-Übertragung (Lesen oder Schreiben, LDM oder STM) an der Adresse 0x1004 durch, das ist eine wortausgerichtete Adresse und vollkommen legal, aber wenn der Bus 64 ist Bit breit ist es wahrscheinlich, dass der einzelne Befehl in drei Übertragungen umgewandelt wird, in diesem Fall ein 32-Bit bei 0x1004, ein 64-Bit bei 0x1008 und ein 32-Bit bei 0x100A. Wenn Sie jedoch den gleichen Befehl unter der Adresse 0x1008 hätten, könnte eine Übertragung von vier Wörtern unter der Adresse 0x1008 durchgeführt werden. Jeder Übertragung ist eine Einrichtungszeit zugeordnet. Die Adressunterschiede von 0x1004 zu 0x1008 können also für sich genommen um ein Vielfaches schneller sein, auch wenn / esp einen Cache verwendet und alle Cache-Treffer sind.

Apropos: Selbst wenn Sie zwei Wörter an der Adresse 0x1000 vs 0x0FFC lesen, führt 0x0FFC mit Cache-Fehlern zu zwei Cache-Zeilen-Lesevorgängen, wobei 0x1000 eine Cache-Zeile ist, und Sie haben die Strafe einer ohnehin gelesenen Cache-Zeile für einen Zufall Zugriff (Lesen von mehr Daten als Verwenden), aber dann verdoppelt sich das. Wie Ihre Strukturen oder Ihre Daten im Allgemeinen ausgerichtet sind und wie häufig Sie auf diese Daten zugreifen, usw. kann zu einer Überlastung des Caches führen.

Sie können Ihre Daten so streifen, dass Sie beim Verarbeiten der Daten, die Sie räumen können, möglicherweise echtes Pech haben und nur einen Bruchteil Ihres Caches verwenden. Wenn Sie durch den nächsten Blob springen, kollidiert der nächste Blob von Daten mit einem vorherigen Blob . Indem Sie Ihre Daten vermischen oder Funktionen im Quellcode usw. neu anordnen, können Sie Kollisionen erstellen oder entfernen, da nicht alle Caches gleich erstellt werden. Der Compiler wird Ihnen hier nicht weiterhelfen. Sogar das Erkennen von Leistungseinbußen oder -verbesserungen liegt bei Ihnen.

All die Dinge, die wir hinzugefügt haben, um die Leistung zu verbessern, breitere Datenbusse, Pipelines, Caches, Verzweigungsvorhersage, mehrere Ausführungseinheiten / -pfade usw. Werden am häufigsten helfen, aber alle haben Schwachstellen, die absichtlich oder versehentlich ausgenutzt werden können. Der Compiler oder die Bibliotheken können nur sehr wenig dagegen tun. Wenn Sie an der Leistung interessiert sind, müssen Sie diese optimieren, und einer der größten Optimierungsfaktoren ist die Ausrichtung des Codes und der Daten, nicht nur auf 32, 64, 128, 256 Bitgrenzen, aber auch wenn die Dinge relativ zueinander sind, möchten Sie, dass stark genutzte Schleifen oder wiederverwendete Daten nicht im selben Cache landen, sondern jeweils eigene. Compiler können zum Beispiel bei der Bestellung von Anweisungen für eine superskalare Architektur helfen, indem sie Anweisungen neu anordnen, die relativ zueinander keine Rolle spielen.

Das größte Versehen ist die Annahme, dass der Prozessor der Engpass ist. Seit einem Jahrzehnt oder länger nicht mehr wahr, ist das Füttern des Prozessors das Problem, und hier kommen Probleme wie Leistungseinbußen bei der Ausrichtung, Cache-Thrashing usw. ins Spiel. Mit ein wenig Arbeit selbst auf der Ebene des Quellcodes kann das Neuanordnen von Daten in einer Struktur, das Anordnen von Variablen- / Strukturdeklarationen, das Anordnen von Funktionen im Quellcode und ein wenig zusätzlicher Code zum Ausrichten von Daten die Leistung um ein Vielfaches verbessern Mehr.

Oldtimer
quelle
+1, wenn nur für Ihren letzten Absatz. Die Speicherbandbreite ist das kritischste Problem für alle, die heute versuchen, schnellen Code zu schreiben, nicht die Anzahl der Befehle. Dies bedeutet, dass die Optimierung von Dingen zur Reduzierung von Cache-Fehlern, die durch Ändern der Ausrichtung unter vielen Umständen durchgeführt werden kann, von enormer Bedeutung ist.
Jules
Wenn Ihr Code und Ihre Daten zwischengespeichert werden und Sie genügend Schleifen / Zyklen für diese Daten ausführen, ist die Befehlsanzahl und die Position der Befehle in einer Abrufzeile, in der die Verzweigungen im Verhältnis zu der Position, auf die sie angewiesen sind, in der Pipe landen, von Bedeutung. Aber in Dram- und / oder Flash-basierten Systemen muss man sich ja zuerst Gedanken über die Speisung des Prozessors machen.
old_timer
15

Ja, die Speicherausrichtung ist immer noch wichtig.

Einige Prozessoren können tatsächlich keine Lesevorgänge für nicht ausgerichtete Adressen ausführen. Wenn Sie auf einer solchen Hardware arbeiten und Ihre Ganzzahlen nicht ausgerichtet speichern, müssen Sie sie wahrscheinlich mit zwei Anweisungen lesen, gefolgt von weiteren Anweisungen, um die verschiedenen Bytes an die richtigen Stellen zu bringen, damit Sie sie tatsächlich verwenden können . Ausgerichtete Daten sind also leistungskritisch.

Die gute Nachricht ist, dass Sie sich größtenteils nicht wirklich darum kümmern müssen. Nahezu jeder Compiler für nahezu jede Sprache wird Maschinencode erstellen, der die Ausrichtungsanforderungen des Zielsystems erfüllt. Sie müssen sich nur Gedanken darüber machen, wenn Sie die direkte Kontrolle über die speicherinterne Darstellung Ihrer Daten übernehmen, was bei weitem nicht mehr so ​​häufig erforderlich ist wie früher. Es ist eine interessante Sache zu wissen, und es ist absolut wichtig zu wissen, ob Sie die Speichernutzung aus verschiedenen Strukturen verstehen wollen, die Sie erstellen, und wie Sie die Dinge möglicherweise neu organisieren können, um sie effizienter zu gestalten (ohne Auffüllen). Aber wenn Sie diese Art der Steuerung nicht benötigen (und für die meisten Systeme einfach nicht), können Sie eine ganze Karriere ohne Wissen oder ohne Rücksicht darauf glücklich durchlaufen.

Matthew Walton
quelle
1
Insbesondere unterstützt ARM keinen nicht ausgerichteten Zugriff. Und das ist die CPU, die fast alles mobile nutzt.
Jan Hudec
Beachten Sie auch, dass Linux einen nicht ausgerichteten Zugriff zu bestimmten Laufzeitkosten emuliert, Windows (CE und Phone) dies jedoch nicht tut und der Versuch eines nicht ausgerichteten Zugriffs die Anwendung einfach zum Absturz bringt.
Jan Hudec
2
Beachten Sie, dass einige Plattformen (einschließlich x86) unterschiedliche Ausrichtungsanforderungen haben, je nachdem, welche Anweisungen verwendet werden sollen. Dies ist für den Compiler nicht einfach, sodass Sie manchmal einen Pad ausführen müssen, um dies sicherzustellen Bestimmte Operationen (z. B. die SSE-Anweisungen, von denen viele eine 16-Byte-Ausrichtung erfordern) können für einige Operationen verwendet werden. Das Hinzufügen zusätzlicher Füllungen, sodass zwei Elemente, die häufig zusammen verwendet werden, in derselben Cache-Zeile (ebenfalls 16 Byte) vorkommen, kann in einigen Fällen eine enorme Auswirkung auf die Leistung haben und ist auch nicht automatisiert.
Jules
3

Ja, es ist immer noch wichtig, und bei einigen leistungskritischen Algorithmen können Sie sich nicht auf den Compiler verlassen.

Ich werde nur einige Beispiele auflisten:

  1. Aus dieser Antwort :

Normalerweise holt der Mikrocode die richtige 4-Byte-Menge aus dem Speicher, aber wenn er nicht ausgerichtet ist, muss er zwei 4-Byte-Stellen aus dem Speicher holen und die gewünschte 4-Byte-Menge aus den entsprechenden Bytes der beiden Stellen rekonstruieren

  1. Der SSE-Befehlssatz erfordert eine spezielle Ausrichtung. Wenn dies nicht der Fall ist, müssen Sie spezielle Funktionen verwenden, um Daten in den nicht ausgerichteten Speicher zu laden und zu speichern. Das bedeutet zwei zusätzliche Anweisungen.

Wenn Sie nicht an leistungskritischen Algorithmen arbeiten, vergessen Sie einfach die Speicherausrichtung. Es wird für die normale Programmierung nicht wirklich benötigt.

BЈовић
quelle
1

Wir neigen dazu, Situationen zu vermeiden, in denen es darauf ankommt. Wenn es darauf ankommt, ist es wichtig. Nicht ausgerichtete Daten wurden zum Beispiel bei der Verarbeitung von Binärdaten verwendet, was heutzutage vermieden zu werden scheint (Menschen verwenden häufig XML oder JSON).

WENN es Ihnen irgendwie gelingt, ein nicht ausgerichtetes Array von Ganzzahlen zu erstellen, wird auf einem typischen Intel-Prozessor der Code, der dieses Array verarbeitet, etwas langsamer ausgeführt als bei ausgerichteten Daten. Auf einem ARM-Prozessor läuft es etwas langsamer, wenn Sie dem Compiler mitteilen, dass die Daten nicht ausgerichtet sind. Je nach Prozessormodell und Betriebssystem kann die Ausführung sehr viel langsamer sein oder zu falschen Ergebnissen führen, wenn Sie nicht ausgerichtete Daten verwenden, ohne dies dem Compiler mitzuteilen.

Erklären des Verweises auf C ++: In C müssen alle Felder in einer Struktur in aufsteigender Speicherreihenfolge gespeichert werden. Wenn Sie also die Felder char / double / char haben und alles ausrichten möchten, haben Sie ein Byte char, sieben Byte unbenutzt, acht Byte double, ein Byte char, sieben Byte unbenutzt. In C ++ - Strukturen ist es aus Kompatibilitätsgründen dasselbe. Bei Strukturen kann der Compiler Felder neu anordnen, sodass Sie möglicherweise ein Bytezeichen, ein anderes Bytezeichen, sechs unbenutzte Byte und ein 8-Byte-Doppelbyte haben. Verwenden von 16 anstelle von 24 Bytes. In C-Strukturen vermeiden Entwickler normalerweise diese Situation und haben die Felder zunächst in einer anderen Reihenfolge.

gnasher729
quelle
1
Nicht ausgerichtete Daten befinden sich im Speicher. Programme, die nicht über ordnungsgemäß gepackte Datenstrukturen verfügen, können massive Leistungseinbußen erleiden, selbst wenn die Reihenfolge der Werte scheinbar nicht von Belang ist. In Thread-Code führen beispielsweise zwei Werte in einer einzelnen Cache-Zeile zu massiven Pipeline-Stillständen, wenn zwei Threads gleichzeitig auf sie zugreifen (wobei die Thread-Sicherheitsprobleme natürlich ignoriert werden).
Greyfade
Ein C ++ - Compiler kann Felder nur unter bestimmten Bedingungen neu anordnen, die wahrscheinlich nicht erfüllt werden, wenn Sie diese Regeln nicht kennen. Außerdem ist mir kein C ++ - Compiler bekannt, der diese Freiheit tatsächlich nutzt.
Sjoerd
1
Ich habe noch nie einen C-Compiler gesehen, der Felder neu anordnet. Ich habe zum Beispiel viele Einfügepolster und Ausrichtungen zwischen Zeichen / Inches gesehen.
PaulHK
1

Wie wichtig ist die Ausrichtung des Speichers? Ist es immer noch wichtig?

Ja. Nein, es kommt darauf an.

Außerhalb des eingebetteten Systems haben wir oft einen riesigen Speicherbereich in unserem Computer, der die Speicherverwaltung viel weniger kritisiert. Ich bin vollkommen in der Optimierung, aber jetzt ist es wirklich etwas, das den Unterschied ausmachen kann, wenn wir dasselbe Programm mit oder vergleichen ohne dass der Speicher neu angeordnet und ausgerichtet ist?

Ihre Anwendung hat einen geringeren Speicherbedarf und arbeitet schneller, wenn sie richtig ausgerichtet ist. In der typischen Desktop-Anwendung spielt es außerhalb seltener / atypischer Fälle keine Rolle (z. B. wenn Ihre Anwendung immer mit dem gleichen Leistungsengpass endet und Optimierungen erforderlich sind). Das heißt, die App wird kleiner und schneller, wenn sie richtig ausgerichtet ist. In den meisten praktischen Fällen sollte sie den Benutzer jedoch nicht auf die eine oder andere Weise beeinträchtigen.

Hat die Speicherausrichtung noch andere Vorteile? Ich habe irgendwo gelesen, dass die CPU mit ausgerichtetem Speicher besser / schneller arbeitet, weil für die Verarbeitung weniger Anweisungen erforderlich sind (wenn einer von Ihnen einen Link zu einem Artikel / Benchmark darüber hat?). Ist in diesem Fall der Unterschied wirklich signifikant? Gibt es mehr Vorteile als diese beiden?

Es kann sein. Es ist (möglicherweise) etwas zu beachten, wenn Sie Code schreiben, aber in den meisten Fällen sollte es keine Rolle spielen (das heißt, ich ordne meine Mitgliedsvariablen immer noch nach Speicherbedarf und Zugriffshäufigkeit an - was das Zwischenspeichern erleichtern sollte -, aber ich tue dies für Benutzerfreundlichkeit / Lesen und Refactoring des Codes, nicht für Caching-Zwecke).

Haben Sie eine Vorstellung davon, wie die Speicherausrichtung in C ++ genau funktioniert, da es einige Unterschiede zu geben scheint?

Ich habe darüber gelesen, als das Alignof-Zeug herauskam (C ++ 11?). Ich habe mich seitdem nicht mehr darum gekümmert.

utnapistim
quelle