Warum setzen x86-64-Befehle in 32-Bit-Registern den oberen Teil des vollständigen 64-Bit-Registers auf Null?

118

In der x86-64-Tour der Intel-Handbücher habe ich gelesen

Die vielleicht überraschendste Tatsache ist, dass ein Befehl wie beispielsweise MOV EAX, EBXautomatisch die oberen 32 RAXRegisterbits auf Null setzt .

In der Intel-Dokumentation (3.4.1.1 Allzweckregister im 64-Bit-Modus in der manuellen Basisarchitektur), die an derselben Quelle zitiert wird, heißt es:

  • 64-Bit-Operanden erzeugen ein 64-Bit-Ergebnis im Ziel-Allzweckregister.
  • 32-Bit-Operanden erzeugen ein 32-Bit-Ergebnis, das im Ziel-Allzweckregister auf ein 64-Bit-Ergebnis erweitert wird.
  • 8-Bit- und 16-Bit-Operanden erzeugen ein 8-Bit- oder 16-Bit-Ergebnis. Die oberen 56 Bits bzw. 48 Bits des Ziel-Allzweckregisters werden durch die Operation nicht modifiziert. Wenn das Ergebnis einer 8-Bit- oder 16-Bit-Operation für die 64-Bit-Adressberechnung vorgesehen ist, erweitern Sie das Register explizit auf die vollen 64-Bit-Zeichen.

In der x86-32- und x86-64-Assembly werden 16-Bit-Anweisungen wie z

mov ax, bx

Zeigen Sie nicht diese Art von "seltsamem" Verhalten, dass das obere Wort von eax auf Null gesetzt ist.

Also: Was ist der Grund, warum dieses Verhalten eingeführt wurde? Auf den ersten Blick scheint es unlogisch (aber der Grund könnte sein, dass ich an die Macken der x86-32-Assembly gewöhnt bin).

Nubok
quelle
16
Wenn Sie bei Google nach "Partial Register Stall" suchen, finden Sie eine ganze Reihe von Informationen zu dem Problem, das sie (mit ziemlicher Sicherheit) zu vermeiden versuchten.
Jerry Coffin
4
Nicht nur "die meisten". AFAIK, alle Anweisungen mit einem r32Zieloperanden setzen den High 32 auf Null, anstatt zusammenzuführen. Zum Beispiel wird ersetzen einige Montierer pmovmskb r64, xmmmit pmovmskb r32, xmmeiner REX zu speichern, da die 64 - Bit - Zielversion identisch verhält. Obwohl im Abschnitt "Operation" des Handbuchs alle 6 Kombinationen von 32/64-Bit-Dest und 64/128 / 256b-Quelle separat aufgeführt sind, dupliziert die implizite Null-Erweiterung des r32-Formulars die explizite Null-Erweiterung des r64-Formulars. Ich bin gespannt auf die HW-Implementierung ...
Peter Cordes
2
@HansPassant, der Zirkelverweis beginnt.
Kchoi
1
Verwandte Themen: xor eax,eaxoder xor r8d,r8dist der beste Weg, um RAX oder R8 auf Null zu setzen (das Speichern eines REX-Präfixes für RAX und 64-Bit-XOR wird in Silvermont nicht einmal speziell behandelt). Verwandte: Wie genau funktionieren Teilregister auf Haswell / Skylake? Das Schreiben von AL scheint eine falsche Abhängigkeit von RAX zu haben, und AH ist inkonsistent
Peter Cordes

Antworten:

97

Ich bin nicht AMD oder spreche für sie, aber ich hätte es genauso gemacht. Da das Nullstellen der oberen Hälfte keine Abhängigkeit vom vorherigen Wert erzeugt, müsste die CPU warten. Der Mechanismus zur Umbenennung von Registern würde im Wesentlichen zunichte gemacht, wenn dies nicht auf diese Weise geschehen würde.

Auf diese Weise können Sie schnellen Code mit 32-Bit-Werten im 64-Bit-Modus schreiben, ohne die Abhängigkeiten ständig explizit aufheben zu müssen. Ohne dieses Verhalten müsste jeder einzelne 32-Bit-Befehl im 64-Bit-Modus auf etwas warten, das zuvor passiert ist, obwohl dieser hohe Teil fast nie verwendet würde. (Das Erstellen von int64-Bit würde den Cache-Platzbedarf und die Speicherbandbreite verschwenden. X86-64 unterstützt am effizientesten 32- und 64-Bit-Operandengrößen. )

Das Verhalten für 8- und 16-Bit-Operandengrößen ist seltsam. Der Abhängigkeitswahnsinn ist einer der Gründe, warum 16-Bit-Befehle jetzt vermieden werden. x86-64 erbte dies von 8086 für 8-Bit und 386 für 16-Bit und entschied, dass 8- und 16-Bit-Register im 64-Bit-Modus genauso funktionieren wie im 32-Bit-Modus.


Siehe auch Warum verwendet GCC keine Teilregister? Für praktische Details, wie Schreibvorgänge in 8- und 16-Bit-Teilregister (und nachfolgende Lesevorgänge des vollständigen Registers) von echten CPUs behandelt werden.

Harold
quelle
8
Ich finde es nicht seltsam, ich denke, sie wollten nicht zu viel brechen und haben das alte Verhalten dort beibehalten.
Alexey Frunze
5
@Alex Als sie den 32-Bit-Modus einführten, gab es kein altes Verhalten für den hohen Teil. Vorher gab es keinen hohen Teil. Danach konnte er natürlich nicht mehr geändert werden.
Harold
1
Ich habe über 16-Bit-Operanden gesprochen, warum die oberen Bits in diesem Fall nicht auf Null gesetzt werden. Sie sind nicht im Nicht-64-Bit-Modus. Und das bleibt auch im 64-Bit-Modus.
Alexey Frunze
3
Ich habe Ihr "Das Verhalten für 16-Bit-Anweisungen ist seltsam" als "es ist seltsam, dass bei 16-Bit-Operanden im 64-Bit-Modus keine Null-Erweiterung auftritt". Daher meine Kommentare zur Beibehaltung des 64-Bit-Modus zur besseren Kompatibilität.
Alexey Frunze
8
@ Alex oh ich verstehe. OK. Ich finde es aus dieser Perspektive nicht seltsam. Nur aus der Perspektive "Rückblick, vielleicht war es keine so gute Idee". Ich denke, ich hätte klarer sein sollen :)
Harold
9

Es spart einfach Platz in den Anweisungen und im Befehlssatz. Sie können kleine Sofortwerte mithilfe vorhandener (32-Bit-) Anweisungen in ein 64-Bit-Register verschieben.

Außerdem müssen Sie keine 8-Byte-Werte codieren MOV RAX, 42, wenn MOV EAX, 42diese wiederverwendet werden können.

Diese Optimierung ist für 8- und 16-Bit-Operationen nicht so wichtig (weil sie kleiner sind), und eine Änderung der Regeln dort würde auch alten Code beschädigen.

Bo Persson
quelle
7
Wenn das richtig ist, wäre es nicht sinnvoller gewesen, das Zeichen zu erweitern, als es zu verlängern?
Damien_The_Unbeliever
16
Die Zeichenerweiterung ist auch bei Hardware langsamer. Die Nullverlängerung kann parallel zu jeder Berechnung durchgeführt werden, die die untere Hälfte erzeugt, aber die Vorzeichenerweiterung kann nicht durchgeführt werden, bis (zumindest das Vorzeichen von) die untere Hälfte berechnet wurde.
Jerry Coffin
13
Ein weiterer verwandter Trick ist die Verwendung, XOR EAX, EAXda XOR RAX, RAXein REX-Präfix erforderlich wäre.
Neil
3
@Nubok: Sicher, sie hätten eine Kodierung von movzx / movsx hinzufügen können, die ein sofortiges Argument erfordert. Die meiste Zeit ist es mehr bequem die oberen Bits auf Null gesetzt zu haben, so dass Sie einen Wert als Array - Index verwenden können (weil alle regs die gleiche Größe in einer effektiven Adresse sein müssen: [rsi + edx]ist nicht erlaubt). Ein weiterer wichtiger Grund ist natürlich die Vermeidung falscher Abhängigkeiten / Teilregisterstillstände (die andere Antwort).
Peter Cordes
4
und das Ändern der Regeln dort würde auch alten Code brechen. Alter Code kann ohnehin nicht im 64-Bit-Modus ausgeführt werden (z. B. 1-Byte-Inc / Dec sind REX-Präfixe). das ist irrelevant. Der Grund dafür , dass die Warzen von x86 nicht bereinigt werden, sind weniger Unterschiede zwischen dem Langmodus und dem Kompatibilitäts- / Legacy-Modus, sodass weniger Anweisungen je nach Modus unterschiedlich dekodiert werden müssen. AMD wusste nicht, dass sich AMD64 durchsetzen würde, und war leider sehr konservativ, so dass weniger Transistoren zur Unterstützung benötigt würden. Langfristig wäre es in Ordnung gewesen, wenn Compiler und Menschen sich daran erinnern müssten, welche Dinge im 64-Bit-Modus anders funktionieren.
Peter Cordes
1

Ohne eine Ausdehnung von Null auf 64 Bit würde dies bedeuten, dass ein Befehl, aus dem gelesen wird rax, zwei Abhängigkeiten für seinen raxOperanden hat (den Befehl, in den geschrieben wird, eaxund den Befehl, in den raxdavor geschrieben wird). Dies bedeutet, dass 1) der ROB Einträge für haben müsste mehrere Abhängigkeiten für einen einzelnen Operanden, was bedeutet, dass der ROB mehr Logik und Transistoren benötigt und mehr Platz beansprucht, und die Ausführung langsamer auf eine unnötige zweite Abhängigkeit wartet, deren Ausführung möglicherweise ewig dauert; oder alternativ 2), was meiner Meinung nach mit den 16-Bit-Befehlen geschieht, bleibt die Zuweisungsstufe wahrscheinlich stehen (dh wenn die RAT eine aktive Zuordnung für einen axSchreibvorgang hat und ein eaxLesevorgang angezeigt wird, bleibt sie stehen, bis der axSchreibvorgang beendet wird).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

Der einzige Vorteil der Erweiterung nicht Null ist die Sicherstellung der Bits höherer Ordnung von rax enthalten sind. Wenn sie beispielsweise ursprünglich 0xffffffffffffffff enthalten, wäre das Ergebnis 0xffffffff00000007, aber es gibt kaum einen Grund für die ISA, diese Garantie auf eine solche Kosten zu übernehmen Es ist wahrscheinlicher, dass der Vorteil einer Null-Erweiterung tatsächlich mehr erforderlich ist, sodass die zusätzliche Codezeile gespart wird mov rax, 0. Durch die Gewährleistung wird es immer verlängert werden null bis 64 Bits können die Compiler mit diesem Axiom daran arbeiten , während in mov rdx, rax, raxnur für seine einzige Abhängigkeit warten, dh es ist die Ausführung schneller beginnen kann und sich zurückziehen, Ausführungseinheiten frei. Darüber hinaus ermöglicht es auch effizientere Null-Idiome wie xor eax, eaxNull, raxohne dass ein REX-Byte erforderlich ist.

Lewis Kelsey
quelle
Partial-Flags auf Skylake funktionieren zumindest, wenn separate Eingaben für CF im Vergleich zu SPAZO vorhanden sind. (So cmovbeist 2 Uops, aber cmovbist 1). Aber keine CPU, die eine Umbenennung von Teilregistern durchführt, macht es so, wie Sie es vorschlagen. Stattdessen fügen sie ein Zusammenführungs-UOP ein, wenn eine Teilregistrierung getrennt von der Vollregistrierung umbenannt wird (dh "schmutzig" ist). Siehe Warum verwendet GCC keine Teilregister? und wie genau funktionieren Teilregister auf Haswell / Skylake? Das Schreiben von AL scheint eine falsche Abhängigkeit von RAX zu haben, und AH ist inkonsistent
Peter Cordes
CPUs der P6-Familie blieben entweder für ~ 3 Zyklen stehen, um ein zusammenführendes UOP (Core2 / Nehalem) einzufügen, oder frühere P6-Familien (PM, PIII, PII, PPro) blockierten nur für (mindestens?) ~ 6 Zyklen. Vielleicht ist das so, wie Sie es in 2 vorgeschlagen haben, und wartet darauf, dass der vollständige Registrierungswert per Rückschreiben in die permanente / Architekturregisterdatei verfügbar ist.
Peter Cordes
@PeterCordes oh, ich wusste über das Zusammenführen von Uops zumindest für teilweise Flaggenstände Bescheid. Sinnvoll, aber ich habe für eine Minute vergessen, wie es funktioniert; es hat einmal geklickt, aber ich habe vergessen, mir Notizen zu machen
Lewis Kelsey
@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXIch kann kein Beispiel für das 'Zusammenführen von UOP' finden, das verwendet werden würde, um dies zu lösen, genau wie bei einem partiellen Flaggenstillstand
Lewis Kelsey,
Richtig, der frühe P6 bleibt nur bis zum Rückschreiben stehen. Core2 und Nehalem fügen nach / vor ein Zusammenführungs-UOP ein? nur das Frontend für eine kürzere Zeit blockieren. Sandybridge fügt Ups zusammen, ohne zu blockieren. (Die AH-Zusammenführung muss jedoch in einem eigenen Zyklus erfolgen, während die AL-Zusammenführung Teil einer vollständigen Gruppe sein kann.) Haswell / SKL benennt AL überhaupt nicht getrennt von RAX um, ebenso mov al, [mem]wie eine mikrofusionierte Last + ALU- Zusammenführen, nur Umbenennen von AH, und ein AH-Zusammenführen von UOP wird immer noch alleine ausgegeben. Die Mechanismen zum Zusammenführen von Teilflags in diesen CPUs variieren, z. B. bleibt Core2 / Nehalem im Gegensatz zu Teilregistern nur für Teilflags stehen.
Peter Cordes