Früher waren ARM-Prozessoren nicht in der Lage, nicht ausgerichtete Speicherzugriffe (ARMv5 und niedriger) richtig zu verarbeiten. Etwas wie u32 var32 = *(u32*)ptr;
würde einfach fehlschlagen (Ausnahme auslösen), wenn ptr
war nicht richtig auf 4-Bytes ausgerichtet.
Das Schreiben einer solchen Anweisung würde jedoch für x86/x64 gut funktionieren, da diese CPUs solche Situationen immer sehr effizient gehandhabt haben. Aber nach dem C-Standard ist dies keine “richtige” Schreibweise. u32
ist anscheinend gleichbedeutend mit einer Struktur von 4 Bytes, die muss auf 4 Bytes ausgerichtet werden.
Ein richtiger Weg, um das gleiche Ergebnis zu erzielen und gleichzeitig die Korrektheit der Orthodoxie beizubehalten und Gewährleistung der vollen Kompatibilität mit jeder CPU ist:
u32 read32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
Dieser ist korrekt und generiert den richtigen Code für jede CPU, die an nicht ausgerichteten Positionen lesen kann oder nicht. Noch besser, auf x86/x64 ist es richtig für einen einzelnen Lesevorgang optimiert und hat daher die gleiche Leistung wie die erste Anweisung. Es ist tragbar, sicher und schnell. Wer kann mehr fragen?
Nun, das Problem ist, dass wir bei ARM nicht so viel Glück haben.
Schreiben der memcpy
Version ist in der Tat sicher, scheint aber zu systematischen vorsichtigen Operationen zu führen, die für ARMv6 und ARMv7 (im Grunde jedes Smartphone) sehr langsam sind.
In einer leistungsorientierten Anwendung, die stark auf Leseoperationen angewiesen ist, konnte der Unterschied zwischen der 1. und 2. Version gemessen werden: er liegt bei > 5x bei gcc -O2
die Einstellungen. Das ist viel zu viel, um es zu ignorieren.
Bei dem Versuch, einen Weg zu finden, ARMv6/v7-Funktionen zu nutzen, habe ich nach Anleitungen zu einigen Beispielcodes gesucht. Unglücklicherweise scheinen sie die erste Aussage auszuwählen (direct u32
Zugriff), was nicht korrekt sein soll.
Das ist noch nicht alles: Neue GCC-Versionen versuchen jetzt, Auto-Vektorisierung zu implementieren. Auf x64 bedeutet das SSE/AVX, auf ARMv7 bedeutet das NEON. ARMv7 unterstützt auch einige neue „Load Multiple“ (LDM) und „Store Multiple“ (STM) Opcodes, die benötigen Zeiger ausgerichtet werden.
Was bedeutet das ? Nun, dem Compiler steht es frei, diese erweiterten Anweisungen zu verwenden, auch wenn sie nicht ausdrücklich aus dem C-Code aufgerufen wurden (keine intrinsischen). Um eine solche Entscheidung zu treffen, nutzt es die Tatsache, dass an u32* pointer
soll auf 4 Bytes ausgerichtet werden. Wenn dies nicht der Fall ist, sind alle Wetten ungültig: undefiniertes Verhalten, Abstürze.
Das bedeutet, dass selbst auf CPUs, die nicht ausgerichteten Speicherzugriff unterstützen, es jetzt gefährlich ist, direkt zu verwenden u32
zugreifen, da dies bei hohen Optimierungseinstellungen zu fehlerhafter Codegenerierung führen kann (-O3
).
Dies ist also ein Dilemma: Wie kann auf die native Leistung von ARMv6/v7 bei nicht ausgerichtetem Speicherzugriff zugegriffen werden? ohne falsche Version schreiben u32
Zugang ?
PS: Habe ich auch probiert __packed()
Anweisungen, und aus Performance-Sicht scheinen sie genauso zu funktionieren wie die memcpy
Methode.
[Edit] : Vielen Dank für die bisher erhaltenen hervorragenden Elemente.
Wenn ich mir die generierte Assembly ansehe, könnte ich bestätigen, dass @Notlikethat das gefunden hat memcpy
Version generiert tatsächlich richtig ldr
Opcode (nicht ausgerichtetes Laden). Ich habe jedoch auch festgestellt, dass die generierte Assembly nutzlos aufruft str
(Befehl). Der vollständige Vorgang ist also jetzt ein nicht ausgerichtetes Laden, ein ausgerichtetes Speichern und dann ein endgültig ausgerichtetes Laden. Das ist viel mehr Arbeit als nötig.
Antwort auf @haneefmubarak, ja, der Code ist richtig eingebettet. Und nein, memcpy
ist weit davon entfernt, die bestmögliche Geschwindigkeit bereitzustellen, da der Code gezwungen wird, direkt zu akzeptieren u32
access führt zu enormen Leistungssteigerungen. Es muss also eine bessere Möglichkeit geben.
Ein großes Dankeschön an @artless_noise. Der Link zum Godbolt-Service ist von unschätzbarem Wert. Ich war noch nie in der Lage, die Äquivalenz zwischen einem C-Quellcode und seiner Assembly-Darstellung so deutlich zu sehen. Das ist sehr inspirierend.
Ich habe eines der @artless-Beispiele fertiggestellt und es gibt Folgendes:
#include <stdlib.h>
#include <memory.h>
typedef unsigned int u32;
u32 reada32(const void* ptr) { return *(const u32*) ptr; }
u32 readu32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
einmal kompiliert mit ARM GCC 4.8.2 bei -O3 oder -O2 :
reada32(void const*):
ldr r0, [r0]
bx lr
readu32(void const*):
ldr r0, [r0] @ unaligned
sub sp, sp, #8
str r0, [sp, #4] @ unaligned
ldr r0, [sp, #4]
add sp, sp, #8
bx lr
Ziemlich vielsagend….
OK, die Situation ist verwirrender als man möchte. Um dies zu verdeutlichen, hier die Ergebnisse dieser Reise:
Zugriff auf nicht ausgerichteten Speicher
- Die einzige portable C-Standardlösung für den Zugriff auf nicht ausgerichteten Speicher ist die
memcpy
eines. Ich hatte gehofft, durch diese Frage einen weiteren zu bekommen, aber anscheinend ist es der einzige, der bisher gefunden wurde.
Beispielcode:
u32 read32(const void* ptr) {
u32 value;
memcpy(&value, ptr, sizeof(value));
return value; }
Diese Lösung ist unter allen Umständen sicher. Es kompiliert auch in ein triviales load register
Betrieb auf x86-Ziel mit GCC.
Auf einem ARM-Ziel mit GCC führt dies jedoch zu einer viel zu großen und nutzlosen Montagesequenz, die die Leistung beeinträchtigt.
Verwenden von Clang auf dem ARM-Ziel, memcpy
funktioniert gut (siehe @notlikethat Kommentar unten). Es wäre einfach, GCC insgesamt die Schuld zu geben, aber es ist nicht so einfach: die memcpy
Die Lösung funktioniert gut auf GCC mit x86/x64-, PPC- und ARM64-Zielen. Wenn Sie schließlich einen anderen Compiler, icc13, ausprobieren, ist die memcpy-Version auf x86/x64 überraschend schwerer (4 Anweisungen, obwohl eine ausreichen sollte). Und das sind nur die Kombinationen, die ich bisher testen konnte.
Ich muss Godbolts Projekt danken, um solche Aussagen zu machen leicht zu beobachten.
- Die zweite Lösung ist zu verwenden
__packed
Strukturen. Diese Lösung ist kein C-Standard und hängt vollständig von der Erweiterung des Compilers ab. Folglich hängt die Art und Weise, wie es geschrieben wird, vom Compiler und manchmal von seiner Version ab. Dies ist ein Chaos für die Wartung von portablem Code.
Davon abgesehen führt dies in den meisten Fällen zu einer besseren Codegenerierung als memcpy
. Nur in den meisten Fällen …
Zum Beispiel in Bezug auf die oben genannten Fälle, in denen memcpy
Lösung funktioniert nicht, hier sind die Ergebnisse:
Leider ist es in mindestens einem Fall die einzige Lösung, die in der Lage ist, Leistung aus dem Ziel zu extrahieren. Nämlich für GCC auf ARMv6.
Verwenden Sie diese Lösung jedoch nicht für ARMv7: GCC kann Anweisungen generieren, die für ausgerichtete Speicherzugriffe reserviert sind, nämlich LDM
(Load Multiple), was zum Absturz führt.
Selbst auf x86/x64 wird es heutzutage gefährlich, Ihren Code auf diese Weise zu schreiben, da die Compiler der neuen Generation möglicherweise versuchen, einige kompatible Schleifen automatisch zu vektorisieren und SSE/AVX-Code zu generieren basierend auf der Annahme, dass diese Speicherpositionen richtig ausgerichtet sindAbsturz des Programms.
Als Zusammenfassung sind hier die Ergebnisse in einer Tabelle zusammengefasst, wobei die Konvention verwendet wird: memcpy > gepackt > direkt.
| compiler | x86/x64 | ARMv7 | ARMv6 | ARM64 | PPC |
|-----------|---------|--------|--------|--------|--------|
| GCC 4.8 | memcpy | packed | direct | memcpy | memcpy |
| clang 3.6 | memcpy | memcpy | memcpy | memcpy | ? |
| icc 13 | packed | N/A | N/A | N/A | N/A |
Ein Teil des Problems ist wahrscheinlich, dass Sie keine einfache Inlinierbarkeit und weitere Optimierung zulassen. Das Vorhandensein einer spezialisierten Funktion für das Laden bedeutet, dass bei jedem Aufruf ein Funktionsaufruf ausgegeben werden kann, was die Leistung verringern könnte.
Eine Sache, die Sie tun könnten, ist zu verwenden static inline
wodurch der Compiler die Funktion einbetten kann load32()
, wodurch die Leistung gesteigert wird. Bei höheren Optimierungsstufen sollte der Compiler dies jedoch bereits für Sie einbetten.
Wenn der Compiler einen 4-Byte-Memcpy einbettet, wird er ihn wahrscheinlich in die effizienteste Reihe von Lade- oder Speichervorgängen umwandeln, die immer noch an nicht ausgerichteten Grenzen funktionieren. Wenn Sie auch bei aktivierten Compiler-Optimierungen immer noch eine geringe Leistung sehen, kann dies daher der Fall sein das ist die maximale Leistung für nicht ausgerichtete Lese- und Schreibvorgänge auf den von Ihnen verwendeten Prozessoren. Da du gesagt hast “__packed
Anweisungen” liefern identische Leistung zu memcpy()
das scheint der Fall zu sein.
An diesem Punkt können Sie nur sehr wenig tun, außer Ihre Daten abzugleichen. Wenn Sie es jedoch mit einem zusammenhängenden Array von nicht ausgerichteten zu tun haben u32
‘s, es gibt eine Sache, die Sie tun könnten:
#include <stdint.h>
#include <stdlib.h>
// get array of aligned u32
uint32_t *align32 (const void *p, size_t n) {
uint32_t *r = malloc (n * sizeof (uint32_t));
if (r)
memcpy (r, p, n);
return r;
}
Dies verwendet nur, um ein neues Array mit zuzuweisen malloc()
Weil malloc()
und Freunde weisen Speicher mit korrekter Ausrichtung für alles zu:
Die Funktionen malloc() und calloc() geben einen Zeiger auf den zugewiesenen Speicher zurück, der für jede Art von Variable geeignet ausgerichtet ist.
– malloc(3)
Handbuch für Linux-Programmierer
Dies sollte relativ schnell gehen, da Sie dies nur einmal pro Datensatz tun müssen. Auch beim Kopieren memcpy()
nur für den anfänglichen Mangel an Ausrichtung anpassen und dann die schnellsten verfügbaren ausgerichteten Lade- und Speicheranweisungen verwenden können, wonach Sie in der Lage sein werden, Ihre Daten mit den normalen ausgerichteten Lese- und Schreibvorgängen bei voller Leistung zu verarbeiten.
Ich bezweifle, dass Sie leider etwas schneller als memcpy finden können.
– Adrian
18. August 2015 um 3:31 Uhr
Es ist nicht gefährlich, u32 zu verwenden. Es ist gefährlich, dem Compiler zu sagen, dass Sie besser wissen, worauf er zugreift (explizites Casting), wenn dies tatsächlich nicht der Fall ist.
– Unixschlumpf
18. August 2015 um 7:44 Uhr
Keine Repro. Unter Verwendung eines Linaro GCC 4.8.3 mit -march=armv6 und -O1 kompiliert die obige Funktion im Wesentlichen
ldr r0, [r0]; str r0, [sp, #4]; ldr r0, [sp, #4]
. Schade, dass die Verwendung der lokalen Variablen nicht vollständig vermieden werden kann, aber genau dort ist Ihre unausgerichtete Wortlast; keine mehrfachen Byte-Ladevorgänge oder Out-of-Line-Aufrufe an memcpy.– Nicht so
18. August 2015 um 8:40 Uhr
Zum Beispiel Gottriegel gibt echte Ausgabe und ein Beispiel mit main.
– ungekünstelter Lärm
18. August 2015 um 15:43 Uhr
Danke für diese aufschlussreichen Elemente. Ich habe die Frage dank Godbolt mit neuen Informationen aktualisiert.
– Cyan
18. August 2015 um 19:05 Uhr