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:
- 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?
- 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?
- 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.)
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.
quelle
Antworten:
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.
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):
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.
quelle
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.
quelle
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:
Wenn Sie nicht an leistungskritischen Algorithmen arbeiten, vergessen Sie einfach die Speicherausrichtung. Es wird für die normale Programmierung nicht wirklich benötigt.
quelle
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.
quelle
Viele gute Punkte sind bereits in den obigen Antworten erwähnt. Nur um auch in nicht eingebetteten Systemen, die sich mit Datensuche / Mining befassen, die Leistung von Speicherangelegenheiten und Zugriffszeiten hinzuzufügen, sind sie so wichtig, dass außer Alignment Assembly Code für dieselben geschrieben wird.
Ich empfehle auch eine lohnende Lektüre: http://dewaele.org/~robbe/thesis/writing/references/what-every-programmer-should-know-about-memory.2007.pdf
quelle
Ja. Nein, es kommt darauf an.
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.
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).
Ich habe darüber gelesen, als das Alignof-Zeug herauskam (C ++ 11?). Ich habe mich seitdem nicht mehr darum gekümmert.
quelle