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.