Ich glaube, ich suche nach einer Antwort auf eine Trivia-Frage. Ich versuche zu verstehen, warum die MIPS-Architektur einen expliziten "Null" -Wert in einem Register verwendet, wenn Sie dasselbe erreichen können, indem Sie einfach jedes Register mit sich selbst XOR-verknüpfen. Man könnte sagen, dass die Operation bereits für Sie erledigt ist; Ich kann mir jedoch keine Situation vorstellen, in der Sie viele "Null" -Werte verwenden würden. Ich habe Hennesseys Originalpapiere gelesen, und es wird tatsächlich ohne wirkliche Begründung eine Null zugewiesen.
Gibt es einen logischen Grund für eine fest codierte binäre Zuweisung von Null?
Update: In 8k einer ausführbaren Datei von xc32-gcc für den MIPS-Kern im PIC32MZ habe ich eine einzelne Instanz von "Null".
add t3,t1,zero
Die eigentliche Antwort: Ich habe die Prämie an die Person vergeben, die die Informationen über MIPS und Bedingungscodes hatte. Die Antwort liegt tatsächlich in der MIPS-Architektur für Bedingungen. Obwohl ich dem zunächst keine Zeit widmen wollte, habe ich die Architektur für opensparc , RISC-V , MIPS-IV und OpenPOWER überprüft (dieses Dokument war intern) und hier sind die zusammenfassenden Ergebnisse. Das R0-Register, das aufgrund der Architektur der Pipeline zum Vergleich auf Verzweigungen erforderlich ist.
Es kommt nur darauf an, wie die Hardware in der Umsetzung aussieht. Aus dem RISC-V-Handbuch gibt es auf Seite 68 ein nicht referenziertes Zitat:
Die bedingten Verzweigungen wurden entwickelt, um arithmetische Vergleichsoperationen zwischen zwei Registern (wie auch in PA-RISC und Xtensa ISA) einzuschließen, anstatt Bedingungscodes (x86, ARM, SPARC, PowerPC) zu verwenden oder nur ein Register mit Null zu vergleichen ( Alpha, MIPS) oder zwei Register nur für Gleichheit (MIPS). Dieses Design wurde durch die Beobachtung motiviert, dass ein kombinierter Vergleichs- und Verzweigungsbefehl in eine reguläre Pipeline passt, einen zusätzlichen Bedingungscodezustand oder die Verwendung eines temporären Registers vermeidet und die statische Codegröße und den dynamischen Befehlsabruf-Trac reduziert. Ein weiterer Punkt ist, dass Vergleiche mit Null eine nicht triviale Schaltungsverzögerung erfordern (insbesondere nach der Umstellung auf statische Logik in fortgeschrittenen Prozessen) und daher fast so teuer sind wie arithmetische Größenvergleiche. Ein weiterer Vorteil eines verschmolzenen Vergleichs- und Verzweigungsbefehls besteht darin, dass Verzweigungen früher im Front-End-Befehlsstrom beobachtet werden und somit früher vorhergesagt werden können. Ein Design mit Bedingungscodes hat vielleicht einen Vorteil, wenn mehrere Verzweigungen auf der Grundlage derselben Bedingungscodes genommen werden können, aber wir glauben, dass dieser Fall relativ selten ist.
Das RISC-V-Dokument trifft den Autor des zitierten Abschnitts nicht. Ich danke allen für ihre Zeit und Überlegung.
Das Nullregister auf RISC-CPUs ist aus zwei Gründen nützlich:
Abhängig von Einschränkungen der ISA können Sie in einigen Befehlscodierungen kein Literal verwenden, aber Sie können sicher sein, dass Sie r0
damit 0 erhalten.
Das ist vielleicht der wichtigste Punkt. Als ISA-Designer können Sie ein Allzweckregister gegen ein Nullregister austauschen, um andere nützliche Anweisungen zu synthetisieren. Das Synthetisieren von Anweisungen ist gut, da Sie durch weniger tatsächliche Anweisungen weniger Bits zum Codieren einer Operation in einem Opcode benötigen, wodurch Speicherplatz im Anweisungscodierungsraum freigegeben wird. Sie können diesen Platz verwenden, um zB größere Adress-Offsets und/oder Literale zu haben.
Die Semantik des Nullregisters ist wie /dev/zero
auf *nix-Systemen: Alles, was darauf geschrieben wird, wird verworfen, und Sie lesen immer 0 zurück.
Sehen wir uns einige Beispiele an, wie wir mit Hilfe des Nullregisters Pseudobefehle erstellen können r0
:
; ### Hypothetical CPU ###
; Assembler with syntax:
; op rd, rm, rn
; => rd: destination, rm: 1st operand, rn: 2nd operand
; literal as #lit
; On an CPU architecture with a status register (which contains arithmetic status
; flags), `sub` can be used, with r0 as destination to discard result.
cmp rn, rm ; => sub r0, rn, rm
; `add` instruction can be used as a `mov` instruction:
mov rd, rm ; => add rd, rm, r0
mov rd, #lit ; => add rd, r0, #lit
; Negate:
neg rd, rm ; => sub rd, r0, rm
; On CPU without status flags,
nop ; => add r0, r0, r0
; RISC-V's `jal` instruction -- Jump and Link: Jump to PC-relative instruction,
; save return address into rd; we can synthesize a `jmp` instruction out of it.
jmp dest ; => jal r0, dest
; You can even load from an absolute (direct) address, for a usually small range
; of addresses by using a literal offset as an address.
ld rd, addr ; => ld rd, [r0, #addr]
Ich habe mir den MIPS-Befehlssatz genauer angeschaut. Es gibt eine Handvoll Pseudoanweisungen, die $zero
; Sie werden hauptsächlich für Zweige verwendet. Hier sind einige Beispiele von dem, was ich gefunden habe:
move $rt, $rs => add $rt, $rs, $zero
not $rt, $rs => nor $rt, $rs, $zero
b Label => beq $zero, $zero, Label ; a small relative branch
bgt $rs, $rt, Label => slt $at, $rt, $rs
bne $at, $zero, Label
blt $rs, $rt, Label => slt $at, $rs, $rt
bne $at, $zero, Label
bge $rs, $rt, Label => slt $at, $rs, $rt
beq $at, $zero, Label
ble $rs, $rt, Label => slt $at, $rt, $rs
beq $at, $zero, Label
Warum Sie in Ihrer Disassemblierung nur eine Instanz des $zero
Registers gefunden haben, ist vielleicht Ihr Disassembler schlau genug, um bekannte Befehlssequenzen in ihre äquivalente Pseudo-Anweisung umzuwandeln.
Nun, anscheinend findet ARM ein Nullregister so nützlich, dass es in ihrem (etwas) neuen ARMv8-A-Kern, der AArch64 implementiert, jetzt ein Nullregister im 64-Bit-Modus gibt; vorher gab es kein nullregister. (Das Register ist jedoch etwas speziell, in einigen Codierungskontexten ist es ein Nullregister, in anderen bezeichnet es stattdessen den Stapelzeiger. )
slt
, slti
, sltu
) zu emulieren.Sie könnten denken, dass ARM32, SPARC usw. kein 0-Register haben, aber tatsächlich haben sie es! Auf der Ebene der Mikroarchitektur fügen die meisten CPU-Entwickler ein 0-Register hinzu, das für die Software möglicherweise unsichtbar ist (das Nullregister von ARM ist unsichtbar), und verwenden dieses Nullregister, um die Befehlsdecodierung zu optimieren.
Stellen Sie sich ein typisches modernes ARM32-Design vor, das über ein unsichtbares Softwareregister verfügt, z. B. R16, das mit 0 verbunden ist. Betrachten Sie die ARM32-Last, viele Fälle von ARM32-Ladeanweisungen fallen in eine dieser Formen (Ignorieren Sie die Prä-Post-Indizierung für eine Weile, um die Diskussion einfach zu halten )...
LDR ra, [rb] // NOTE:The ! is optional and represents address writeback.
LDR ra, [rb, rc](!)
LDR ra, [rb, #k](!)
Innerhalb des Prozessors wird dies zu einem General dekodiert
ldr.uop ra, rb, rx, rc, #c // Internal decoded instruction format.
vor Eintritt in die Ausgabephase, in der die Register gelesen werden. Beachten Sie, dass rx das Register darstellt, um die aktualisierte Adresse zurückzuschreiben. Hier sind einige Dekodierungsbeispiele:
LDR R0, [R1] ==> ldr.uop R0, R1, R16, R16, #0 // Writeback to NULL.
LDR R0, [R1, R2]! ==> ldr.uop R0, R1, R1, R2, #0 // Writeback to R1.
LDR R0, [R1, #2] ==> ldr.uop R0, R1, R16, R16, #2 // Writeback to NULL.
Auf Schaltungsebene sind alle drei Lasten tatsächlich die gleichen internen Befehle, und eine einfache Möglichkeit, diese Art von Orthogonalität zu erreichen, besteht darin, ein Grundregister R16 zu erstellen. Da R16 immer geerdet ist, decodieren diese Befehle natürlich korrekt ohne zusätzliche Logik. Das Abbilden einer Klasse von Anweisungen auf ein einziges internes Format hilft bei superskalaren Implementierungen sehr, da es die logische Komplexität reduziert.
Ein weiterer Grund ist eine optimierte Methode zum Wegwerfen von Schreibvorgängen. Anweisungen können deaktiviert werden, indem einfach das Zielregister und die Flags auf R16 gesetzt werden. Es besteht keine Notwendigkeit, ein anderes Steuersignal zu erzeugen, um das Zurückschreiben usw. zu deaktivieren.
Die meisten Prozessorimplementierungen, unabhängig von der Architektur, enden früh in der Pipeline mit einem RAZ-Registermodell. Die MIPS-Pipeline beginnt im Wesentlichen an einem Punkt, der in anderen Architekturen ein paar Stufen entfernt wäre.
Daher ist ein Null-Lese-Register in jeder modernen Prozessorimplementierung fast obligatorisch, und MIPS, das es für Software sichtbar macht, ist definitiv ein Pluspunkt, wenn man bedenkt, wie es die interne Dekodierungslogik rationalisiert. Designer von MIPS-Prozessoren müssen kein zusätzliches RAZ-Register hinzufügen, da $0 bereits auf Masse liegt. Da RAZ dem Assembler zur Verfügung steht, stehen MIPS viele Pseudobefehle zur Verfügung, und man kann sich das so vorstellen, als ob ein Teil der Decodierungslogik an den Assembler selbst weitergegeben wird, anstatt dedizierte Formate für jeden Befehlstyp zu erstellen, um das RAZ-Register vor der Software zu verbergen wie bei anderen Architekturen. Das RAZ-Register ist eine gute Idee und deshalb hat ARMv8 es kopiert.
Wenn ARM32 ein $0-Register hätte, wäre die Dekodierungslogik einfacher geworden und die Architektur wäre in Bezug auf Geschwindigkeit, Fläche und Leistung viel besser gewesen. Beispielsweise würden von den drei oben vorgestellten LDR-Versionen nur 2 Formate benötigt. Ebenso besteht keine Notwendigkeit, Dekodierlogik für die MOV- und MVN-Befehle zu reservieren. Außerdem würden CMP/CMN/TST/TEQ überflüssig werden. Es wäre auch nicht notwendig, zwischen kurzer (MUL) und langer Multiplikation (UMULL/SMULL) zu unterscheiden, da eine kurze Multiplikation als lange Multiplikation betrachtet werden könnte, wenn das obere Register auf $0 gesetzt ist usw.
Da MIPS ursprünglich von einem kleinen Team entworfen wurde, war die Einfachheit des Designs wichtig und daher wurde $0 explizit im Sinne von RISC gewählt. ARM32 behält viele traditionelle CISC-Funktionen auf Architekturebene bei.
Haftungsausschluss: Ich kenne den MIPS-Assembler nicht wirklich, aber das 0-Wert-Register ist nicht einzigartig für diese Architektur, und ich denke, es wird auf die gleiche Weise verwendet wie in anderen mir bekannten RISC-Architekturen.
Das XOR-Verknüpfen eines Registers zum Erhalten von 0 kostet Sie eine Anweisung, während die Verwendung eines vordefinierten 0-Wert-Registers dies nicht tut.
Anweisungen werden beispielsweise mov RX, RY
oft als implementiert add RX, RY, R0
. Ohne ein 0-Wert-Register müssten Sie xor RZ, RZ
jedes Mal, wenn Sie verwenden möchten mov
.
Ein weiteres Beispiel ist die cmp
Anweisung und ihre Varianten (wie "vergleichen und springen", "vergleichen und verschieben" usw.), cmp RX, R0
die zum Testen auf negative Zahlen verwendet werden.
MOV Rx,Ry
als geben AND Rx,Ry,Ry
?mov RX, Imm
oder mov RX, mem[RY]
wenn Ihr Befehlssatz nur einen einzigen unmittelbaren Wert und einen einzigen Speicherzugriff pro Befehl unterstützt.mov
ist ein schlechtes Beispiel; Sie könnten es mit einer sofortigen 0 anstelle eines Nullregisters implementieren. zB ori dst, src, 0
. Aber ja, Sie bräuchten einen Opcode für mov-immediate, um sich zu registrieren, wenn Sie kein addiu $dst, $zero, 1234
, wie lui
aber für die unteren 16 Bits anstelle der oberen 16 hätten. Und Sie könnten nor
oder nicht verwenden sub
, um einen Operanden not / neg zu erstellen .add r1,r2,r3
gibt, warum nicht mov r1,[r2+r3]
? Würden nicht beide Operationen zwei Lese- und eine Schreiboperation erfordern?move A0, (0, A1, Xn.SIZE*SCALE)
es ist gültig (wobei Xn ein A- oder D-Register ist). nxp.com/files-static/archives/doc/ref_manual/M68000PRM.pdf . Der Quelloperand kann An oder Dn sein, da es zwei getrennte Modi für Register-Direct gibt, einen für D-Regs (Modus=000) und einen für A-Regs (Modus=001). MOVE < ea > , < ea >
hat Moduscodes für beide Operationenmove
könnte mikrocodiert werden müssen, weil ich denke, dass es Mem-to-Mem-Bewegungen unterstützt, einschließlich der beiden Operanden, die speicherindirekte Adressierungsmodi sind. Das sind also potenziell 2 Ladevorgänge für src und 1 Ladevorgang + 1 Speichervorgang für dst und mehrere Adressberechnungen.Es ist billig, ein paar Kabel am Ende Ihrer Registerbank zu erden (billiger, als daraus ein vollwertiges Register zu machen).
Das Ausführen des eigentlichen xor erfordert ein wenig Kraft und Zeit, um die Gates umzuschalten und es dann im Register zu speichern. Warum diese Kosten zahlen, wenn ein vorhandener 0-Wert leicht verfügbar sein kann.
Moderne CPUs haben auch ein (verstecktes) 0-Wert-Register, das sie als Ergebnis einer xor eax eax
Anweisung durch Registerumbenennung verwenden können.
R0
liegen nicht in der Erdung einiger Drähte, sondern in der Tatsache, dass Sie in jeder Anweisung, die sich mit Registern befasst, einen Code dafür reservieren müssen.std::memory_order_consume
) erfordern XOR, um die Abhängigkeit weiterzugeben.lui
, aber nicht um 16 nach links verschoben. Sie können also immer noch mit einer Anweisung eine kleine Zahl in ein Register schreiben. Nur Null mit einer falschen Abhängigkeit zuzulassen, wäre verrückt. (Normales MIPS erstellt Werte ungleich Null mit addiu $dst, $zero, 1234
oder ori
, sodass Ihr "Stromkosten" -Argument zusammenbricht. Wenn Sie vermeiden wollten, dass eine ALU gestartet wird, würden Sie einen Opcode für mov-immediate zum Registrieren hinzufügen, anstatt Software ADD oder OR zu haben ein Direktwert mit Null.)
JimmyB
JimmyB
JimmyB
Benutzer3528438
JAB
Markus Plotnik
Anonym
b degnan
Jarhmander
b degnan
maxschlepzig