int_fast8_t-Größe vs. int_fast16_t-Größe auf der x86-64-Plattform
Ich habe bereits gelernt, dass auf der x86-64-Plattform die Verwendung eines 64-Bit-Registers ein Präfix erfordert REX
und dass für jede Adresse mit weniger als 64 Bit ein Präfix in Adressgröße erforderlich ist.
Auf x86-64 Bit:
E3
rel8 istjrcxz
67 E3
rel8 istjecxz
67
ist der Operationscode für das Präfix zur Überschreibung der Adressgröße.
sizeof(int_fast8_t)
beträgt 8 Bit, während andere sizeof(int_fast16_t)
( sizeof(int_fast32_t)
nur unter Linux) 64 Bit haben.
Warum sind es nur int_fast8_t
8 Bit, wenn andere schnelle Typdefs 64 Bit sind?
Hat es etwas mit der Ausrichtung zu tun?
Antworten
Warum ist nur int_fast8_t 8 Bit, während andere schnelle Typdefs 64 Bit sind?
Weil glibc eine vereinfachende und wohl schlechte Wahl getroffen hat, als x86-64 neu war, als diese C99-Typen neu waren, und die schlechte Entscheidung getroffen hat, es nicht für x86-64 zu spezialisieren.
Alle sind als plattformübergreifend int_fast16/32/64_t
definiert . Das wurde im Mai 1999 gemacht , bevor AMD64 mit einer Papierspezifikation angekündigt wurde (Oktober 1999), die die Entwickler vermutlich erst nach einiger Zeit verstanden haben. (Danke @Homer512 für das Auffinden des Commits und der Historie.)long
long
ist die volle (ganzzahlige) Registerbreite in 32- und 64-Bit-GNU-Systemen. Dies ist auch die Zeigerbreite.
Für die meisten 64-Bit-RISCs ist die volle Breite ziemlich normal, obwohl ich keine Ahnung von Multiplikations- und Divisionsgeschwindigkeiten habe. Es ist offensichtlich schlecht für x86-64, wo 64-Bit-Operandengröße zusätzliche Codegröße erfordert, aber MIPS daddu
und addu
beispielsweise haben dieselbe Codegröße und vermutlich die gleiche Leistung. (Vor x86-64 war es für RISC-ABIs üblich, schmale Typen immer mit Vorzeichen auf 64 Bit zu erweitern, weil MIPS dies zumindest für Nicht-Shift-Anweisungen tatsächlich erforderte. Weitere Informationen zur Geschichte finden Sie unter MOVZX, wo 32-Bit-Register zu 64-Bit-Register fehlt .)
Die Wahl von Glibc macht diese Typen für lokale Variablen größtenteils in Ordnung, zumindest wenn Sie nicht multiplizieren oder dividieren oder __builtin_popcount
andere Operationen durchführen, die mehr Arbeit mit mehr Bits erfordern könnten (insbesondere ohne Hardwareunterstützung popcnt
). Aber nicht überall dort gut, wo Speicherplatz im Speicher eine Rolle spielt.
Wenn Sie auf einen Typ gehofft haben, „wählen Sie nur dann eine größere als die angegebene Größe, wenn dadurch Leistungseinbußen vermieden werden“, dann ist das nicht annähernd das, was glibc Ihnen bietet.
Ich meine mich zu erinnern, dass MUSL unter x86-64 die bessere Wahl getroffen hat, beispielsweise indem jede fast
Größe die Mindestgröße hat, außer vielleicht fast16
32 Bit, und indem Operandengrößenpräfixe und Teilregisterkram vermieden werden.
fast
wirft die Frage auf: „Schnell wofür?“, und die Antwort ist nicht für jeden Anwendungsfall die gleiche Größe . Beispielsweise sind bei etwas, das mit SIMD automatisch vektorisieren kann, die kleinstmöglichen Ganzzahlen normalerweise die besten, um doppelt so viel Arbeit pro 16-Byte-Vektoranweisung zu erledigen. In diesem Fall können 16-Bit-Ganzzahlen gerechtfertigt sein. Oder einfach für den Cache-Footprint in Arrays. Aber erwarten Sie nicht, dass fastxx_t
Typen einen Kompromiss zwischen „nicht zu viel langsamer“ und Platzersparnis in Arrays in Betracht ziehen.
Normalerweise sind schmale Lade-/Speicheranweisungen auf den meisten ISAs in Ordnung, daher sollten Sie lokale Variablen und schmale Array-Elemente verwenden, int
wenn int_fastxx_t
der Cache-Speicherbedarf eine relevante Überlegung ist. Aber die Wahl von glibc ist selbst für lokale Variablen oft schlecht.
Vielleicht haben die Leute in glibc nur die Anweisungen gezählt, nicht die Codegröße (REX-Präfixe) oder die Kosten für Multiplikation und Division (die bei 64-Bit definitiv langsamer waren als bei 32 oder weniger, insbesondere auf den frühen AMD64-CPUs; die Ganzzahldivision war bei 64-Bit auf Intel bis Ice Lake immer noch viel langsamerhttps://uops.info/Undhttps://agner.org/optimize/).
Und ohne die Auswirkungen auf die Strukturgrößen zu betrachten, weder direkt noch aufgrund von alignof(T) == 8
. (Obwohl die Größen der fast
Typen im x86-64 System V ABI nicht festgelegt sind, ist es wahrscheinlich am besten, sie nicht an ABI-Grenzen zu verwenden, wie Strukturen, die in einer Bibliotheks-API enthalten sind.)
Ich weiß nicht wirklich, warum ihnen so ein schwerer Fehler unterlaufen ist, aber er macht int_fastxx_t
Typen für alles außer lokalen Variablen unbrauchbar (nicht für die meisten Strukturen oder Arrays), weil x86-64 GNU/Linux eine wichtige Plattform für den Großteil portablen Codes ist und man nicht möchte, dass der Code dort schlecht ist.
Ähnlich wie die hirnlose Entscheidung von MinGW, std::random_device
Zufallszahlen niedriger Qualität zurückzugeben (anstatt zu scheitern, bis sie endlich etwas Brauchbares implementiert haben), was den portablen Code betrifft, der die Sprachfunktion für den beabsichtigten Zweck nutzen kann, und damit einem Abladen von radioaktivem Müll gleichkam.
Einer der wenigen Vorteile der Verwendung von 64-Bit-Ganzzahlen besteht vielleicht darin, dass man sich nicht mit Müll im oberen Teil der Register an den ABI-Grenzen (Funktionsargumente und Rückgabewerte) herumschlagen muss. Aber normalerweise spielt das keine Rolle, es sei denn, Sie müssen es als Teil eines Adressierungsmodus auf Zeigerbreite erweitern. (In x86-64 müssen alle Register in einem Adressierungsmodus dieselbe Breite haben, wie [rdi + rdx*4]
. AArch64 hat Modi wie [x0, w1 sxt]
diesen, der ein 32-Bit-Register als Index für ein 64-Bit-Register vorzeichenerweitert. Aber das Maschinencodeformat von AArch64 wurde von Grund auf neu entwickelt und kam erst später, als man andere 64-Bit-ISAs in Aktion gesehen hatte.)
Beispielsweise arr[ foo(i) ]
kann eine Anweisung vermieden werden, einen Rückgabewert um Nullen zu erweitern, wenn der Rückgabetyp ein Register füllt. Andernfalls muss er mit einem Vorzeichen oder Nullen auf die Zeigerbreite erweitert werden, bevor er in einem Adressierungsmodus mit einem mov
oder movsxd
(32 auf 64 Bit) oder movzx
oder movsx
(8 oder 16 Bit auf 64 Bit) verwendet werden kann.
Oder bei der Art und Weise, wie das x86-64-System V Strukturen nach Wert in bis zu 2 Registern übergibt und zurückgibt, müssen 64-Bit-Ganzzahlen nicht entpackt werden, da sie sich bereits selbst in einem Register befinden. zB struct ( int32_t a,b; }
hat beide int
s in einem Rückgabewert in RAX gepackt, was Arbeit beim Aufgerufenen zum Packen und beim Aufrufer zum Entpacken erfordert, wenn das Ergebnis tatsächlich verwendet wird und nicht nur die Objektdarstellung in einer Struktur im Speicher gespeichert werden soll. (zB mov ecx, eax
um die untere Hälfte / mit Nullen zu erweitern shr rax, 32
. Oder nur add ebx, eax
um die untere Hälfte zu verwenden und sie dann mit der Verschiebung zu verwerfen; Sie müssen es nicht mit Nullen auf 64 Bit erweitern, um es einfach als 32-Bit-Ganzzahl zu verwenden.)
Innerhalb einer Funktion wissen Compiler, dass ein Wert bereits nach dem Schreiben eines 32-Bit-Registers auf 64 Bit mit Nullen erweitert wurde. Und das Laden aus dem Speicher, sogar die Vorzeichenerweiterung auf 64 Bit, ist kostenlos ( movsxd rax, [rdi]
statt mov eax, [rdi]
). (Oder fast kostenlos auf älteren CPUs, wo die Vorzeichenerweiterung der Speicherquelle noch einen ALU-Uop benötigte, der nicht als Teil eines Lade-Uops durchgeführt wurde.)
Da der Überlauf vorzeichenbehafteter Ganzzahlen UB ist, können Compiler int
( int32_t
) in Schleifen wie auf 64 Bit erweitern for (int i = 0 ; i < n ; i++ ) arr[i] += 1;
oder in ein 64-Bit-Zeigerinkrement konvertieren. (Ich frage mich, ob GCC dies vielleicht schon Anfang der 2000er Jahre konnte, als diese Softwaredesignentscheidungen getroffen wurden? In diesem Fall wären verschwendete movsxd
Anweisungen, um einen Schleifenzähler immer wieder auf 64 Bit zu erweitern, tatsächlich eine interessante Überlegung.)
Aber um fair zu sein, Sie können immer noch Anweisungen zur Vorzeichenerweiterung haben, wenn Sie vorzeichenbehaftete 32-Bit-Ganzzahltypen in Berechnungen verwenden, was zu negativen Ergebnissen führen kann, wenn Sie diese dann zum Indizieren von Arrays verwenden. 64-Bit int_fast32_t
vermeidet also diese movsxd
Anweisungen, auf Kosten der Verschlechterung in anderen Fällen. Vielleicht schließe ich dies aus, weil ich weiß, dass ich es vermeiden muss, z. B. indem ich es verwende, unsigned
wenn es angemessen ist, weil ich weiß, dass es auf x86-64 und AArch64 kostenlos Nullerweiterungen ermöglicht.
Für tatsächliche Berechnungen ist eine Operandengröße von 32 Bit im Allgemeinen mindestens so schnell wie alles andere, einschließlich für imul/div und popcnt , und vermeidet Strafen durch Teilregister oder zusätzliche movzx
Anweisungen, die bei 8 Bit oder 16 Bit auftreten.
- Die Vorteile der Verwendung von 32-Bit-Registern/Anweisungen in x86-64
- Warum beträgt die Standardoperandengröße im 64-Bit-Modus 32 Bit? – 32 Bit benötigt kein REX oder Operandengrößenpräfix.
Aber 8-Bit ist nicht schlecht, und wenn Ihre Zahlen so klein sind, ist es noch schlimmer, sie auf 32 oder 64 Bit aufzublähen; Programmierer erwarten wahrscheinlich eher, dass es int_fast8_t
klein sein wird, es sei denn, es ist viel teurer , es größer zu machen. Auf x86-64 ist es nicht so; Gibt es moderne CPUs, bei denen ein zwischengespeicherter Byte-Speicher tatsächlich langsamer ist als ein Wort-Speicher? – ja, die meisten Nicht-x86-CPUs anscheinend, aber x86 macht Bytes und 16-Bit-Wörter sowohl beim Laden/Speichern als auch bei der Berechnung schnell.
Das Vermeiden von 16 Bit ist wahrscheinlich eine gute Idee und in manchen Fällen die Kosten von 2 zusätzlichen Bytes wert. add ax, 12345
(und andere imm16-Anweisungen) haben LCP-Decodierungsverzögerungen auf Intel-CPUs. Plus falsche Teilregisterabhängigkeiten (oder auf älteren CPUs Zusammenführungsverzögerungen).
jrcxz
vs. jecxz
ist ein seltsames Beispiel, weil es das 67h
Adress -Größenpräfix verwendet, statt 66h
Operandengröße. Und weil Compiler es nie(?) verwenden. Es ist nicht so langsam wie die loopAnweisung , aber es ist überraschenderweise kein Single-Uop, selbst auf Intel-CPUs, die ein Makro zu einem einzigen Uop zusammenführen können test/jz
.