Was ist der beste Weg, um ein Register in einer x86-Assembly auf Null zu setzen: xor, mov oder und?

Antworten:

222

TL; DR-Zusammenfassung : xor same, sameist die beste Wahl für alle CPUs . Keine andere Methode hat einen Vorteil gegenüber dieser, und sie hat zumindest einen Vorteil gegenüber jeder anderen Methode. Es wird offiziell von Intel und AMD empfohlen und was Compiler tun. Verwenden Sie im 64-Bit-Modus weiterhin xor r32, r32, da das Schreiben eines 32-Bit-Registers die oberen 32 auf Null setzt . xor r64, r64ist eine Verschwendung von Bytes, da es ein REX-Präfix benötigt.

Schlimmer noch, Silvermont erkennt nur xor r32,r32als dep-brechend, nicht als 64-Bit-Operandengröße. So , selbst wenn ein Präfix REX ist weiterhin erforderlich , weil Sie Nullstellung r8..r15, verwenden xor r10d,r10d, nichtxor r10,r10 .

GP-Integer-Beispiele:

xor   eax, eax       ; RAX = 0.  Including AL=0 etc.
xor   r10d, r10d     ; R10 = 0
xor   edx, edx       ; RDX = 0

; small code-size alternative:    cdq    ; zero RDX if EAX is already zero

; SUB-OPTIMAL
xor   rax,rax       ; waste of a REX prefix, and extra slow on Silvermont
xor   r10,r10       ; bad on Silvermont (not dep breaking), same as r10d everywhere else because a REX prefix is still needed for r10d or r10.
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes
 and   eax, 0        ; false dependency.  (Microbenchmark experiments might want this)
 sub   eax, eax      ; same as xor on most but not all CPUs; bad on Silvermont for example.

xor   al, al        ; false dep on some CPUs, not a zeroing idiom.  Use xor eax,eax
mov   al, 0         ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified

Das Nullstellen eines Vektorregisters ist normalerweise am besten mit pxor xmm, xmm. Dies ist normalerweise das, was gcc tut (noch vor der Verwendung mit FP-Anweisungen).

xorps xmm, xmmkann Sinn machen. Es ist ein Byte kürzer als pxor, xorpsbenötigt jedoch Ausführungsport 5 unter Intel Nehalem und pxorkann an jedem Port (0/1/5) ausgeführt werden. (Die 2c-Bypass-Verzögerungslatenz von Nehalem zwischen Ganzzahl und FP ist normalerweise nicht relevant, da die Ausführung außerhalb der Reihenfolge sie normalerweise zu Beginn einer neuen Abhängigkeitskette verbergen kann.)

Bei Mikroarchitekturen der SnB-Familie benötigt keine Xor-Zeroing-Variante sogar einen Ausführungsport. Auf AMD und Pre-Nehalem P6 / Core2 Intel, xorpsund pxorwerden auf die gleiche Art und Weise (als Vektor-Integer - Befehle) behandelt.

Wenn Sie die AVX-Version eines 128b-Vektorbefehls verwenden, wird auch der obere Teil der Registrierung auf Null gesetzt. Dies vpxor xmm, xmm, xmmist eine gute Wahl, um YMM (AVX1 / AVX2) oder ZMM (AVX512) oder eine zukünftige Vektorerweiterung auf Null zu setzen. vpxor ymm, ymm, ymmZum Codieren werden jedoch keine zusätzlichen Bytes benötigt, und unter Intel wird dasselbe ausgeführt, unter AMD jedoch langsamer als vor Zen2 (2 Uops). Das ZXM-Nullstellen von AVX512 würde zusätzliche Bytes erfordern (für das EVEX-Präfix), daher sollte das Nullstellen von XMM oder YMM bevorzugt werden.

XMM / YMM / ZMM-Beispiele

    # Good:
 xorps   xmm0, xmm0         ; smallest code size (for non-AVX)
 pxor    xmm0, xmm0         ; costs an extra byte, runs on any port on Nehalem.
 xorps   xmm15, xmm15       ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX.  Code-size is the only penalty.

   # Good with AVX:
 vpxor xmm0, xmm0, xmm0    ; zeros X/Y/ZMM0
 vpxor xmm15, xmm0, xmm0   ; zeros X/Y/ZMM15, still only 2-byte VEX prefix

#sub-optimal AVX
 vpxor xmm15, xmm15, xmm15  ; 3-byte VEX prefix because of high source reg
 vpxor ymm0, ymm0, ymm0     ; decodes to 2 uops on AMD before Zen2


    # Good with AVX512
 vpxor  xmm15,  xmm0, xmm0     ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
 vpxord xmm30, xmm30, xmm30    ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD.  May be worth using only high regs to avoid needing vzeroupper in short functions.
    # Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
 vpxord zmm30, zmm30, zmm30    ; Without AVX512VL you have to use a 512-bit instruction.

# sub-optimal with AVX512 (even without AVX512VL)
 vpxord  zmm0, zmm0, zmm0      ; EVEX prefix (4 bytes), and a 512-bit uop.  Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.

Siehe Ist vxorps-zeroing bei AMD Jaguar / Bulldozer / Zen mit xmm-Registern schneller als mit ymm? und
Was ist der effizienteste Weg, um ein oder mehrere ZMM-Register auf Knights Landing zu löschen?

Semi-related: Der schnellste Weg, den __m256-Wert auf alle ONE-Bits zu setzen und
alle Bits im CPU-Register effizient auf 1 zu setzen, deckt auch AVX512-Maskenregister ab k0..7. SSE / AVX vpcmpeqdist bei vielen abhängig (obwohl zum Schreiben der 1 noch ein UOP erforderlich ist), aber AVX512 vpternlogdfür ZMM-Regs ist nicht einmal abhängig . Innerhalb einer Schleife sollten Sie in Betracht ziehen, aus einem anderen Register zu kopieren, anstatt diese mit einem ALU-UOP neu zu erstellen, insbesondere mit AVX512.

Das Nullstellen ist jedoch billig: Das xor-Nullstellen eines xmm-Regs innerhalb einer Schleife ist normalerweise so gut wie das Kopieren, außer bei einigen AMD-CPUs (Bulldozer und Zen), die eine Bewegungseliminierung für Vektorregs aufweisen, aber dennoch ein ALU-Uop benötigen, um Nullen für xor zu schreiben Null.


Was ist das Besondere daran, Redewendungen wie xor auf verschiedenen Uarchen auf Null zu setzen?

Einige CPUs erkennen sub same,sameals Null-Idiom xor, aber alle CPUs, die Null-Idiome erkennen, erkennenxor . Verwenden xorSie es einfach, damit Sie sich keine Gedanken darüber machen müssen, welche CPU welches Null-Idiom erkennt.

xor(im Gegensatz zu einer anerkannten Null-Sprache mov reg, 0) hat einige offensichtliche und einige subtile Vorteile (zusammenfassende Liste, dann werde ich diese erweitern):

  • kleinere Codegröße als mov reg,0. (Alle CPUs)
  • vermeidet Teilregistrierungsstrafen für späteren Code. (Intel P6-Familie und SnB-Familie).
  • verwendet keine Ausführungseinheit, spart Strom und setzt Ausführungsressourcen frei. (Intel SnB-Familie)
  • Ein kleineres UOP (keine unmittelbaren Daten) lässt Platz in der UOP-Cache-Zeile, damit Anweisungen in der Nähe bei Bedarf ausgeliehen werden können. (Intel SnB-Familie).
  • verbraucht keine Einträge in der physischen Registerdatei . (Zumindest Intel SnB-Familie (und P4), möglicherweise auch AMD, da sie ein ähnliches PRF-Design verwenden, anstatt den Registerstatus im ROB beizubehalten, wie dies bei Mikroarchitekturen der Intel P6-Familie der Fall ist.)

Eine kleinere Maschinencodegröße (2 Bytes statt 5) ist immer von Vorteil: Eine höhere Codedichte führt zu weniger Befehls-Cache-Fehlern und einem besseren Befehlsabruf und einer möglichen Dekodierung der Bandbreite.


Der Vorteil, keine Ausführungseinheit für xor in Mikroarchitekturen der Intel SnB-Familie zu verwenden, ist gering, spart jedoch Strom. Es ist wahrscheinlicher, dass SnB oder IvB eine Rolle spielen, die nur 3 ALU-Ausführungsports haben. Haswell und höher verfügen über 4 Ausführungsports, die ganzzahlige ALU-Anweisungen verarbeiten können. mov r32, imm32Mit einer perfekten Entscheidungsfindung durch den Scheduler (was in der Praxis nicht immer der Fall ist) kann HSW auch dann noch 4 Uops pro Takt aufrechterhalten, wenn alle ALU benötigen Ausführungsports.

Weitere Einzelheiten finden Sie in meiner Antwort auf eine andere Frage zum Nullstellen von Registern .

Bruce Dawsons Blog-Beitrag , den Michael Petch (in einem Kommentar zur Frage) verlinkt hat, weist darauf hin, dass er xorin der Phase des Umbenennens des Registers behandelt wird, ohne dass eine Ausführungseinheit erforderlich ist (null Uops in der nicht fusionierten Domäne), aber die Tatsache übersehen hat, dass es immer noch ein UOP ist in der fusionierten Domäne. Moderne Intel-CPUs können 4 UPs mit fusionierter Domäne pro Takt ausgeben und außer Betrieb setzen. Daher kommt die Grenze von 4 Nullen pro Takt. Die erhöhte Komplexität der Hardware zum Umbenennen von Registern ist nur einer der Gründe, die Breite des Designs auf 4 zu beschränken. (Bruce hat einige sehr gute Blog-Beiträge verfasst, wie seine Serie zu FP-Mathematik und x87 / SSE / Rundungsproblemen , die ich mache sehr empfehlenswert).


Auf AMD Bulldozer-Familie CPUs , mov immediateläuft auf demselben EX0 / EX1 Integer - Ausführungs Ports xor. mov reg,regkann auch auf AGU0 / 1 ausgeführt werden, dies gilt jedoch nur zum Kopieren von Registern, nicht zum Festlegen von Sofortnachrichten. AFAIK, bei AMD ist der einzige Vorteil xorgegenüber movder kürzeren Codierung. Es könnte auch physische Registerressourcen sparen, aber ich habe keine Tests gesehen.


Anerkannte Nullstellungssprachen vermeiden Teilregisterstrafen auf Intel-CPUs, die Teilregister getrennt von Vollregistern (P6- und SnB-Familien) umbenennen.

xorwird das Register Tag , das die oberen Teile auf Null gesetzt haben , so xor eax, eax/ inc al/ inc eaxvermeidet die üblichen Teilregister Strafe , die pre-IVB CPUs haben. Auch ohne xorbenötigt IvB nur dann ein Zusammenführen, wenn die hohen 8 Bit ( AH) geändert werden und dann das gesamte Register gelesen wird, und Haswell entfernt dies sogar.

Aus dem Microarch-Handbuch von Agner Fog, S. 98 (Abschnitt Pentium M, auf den in späteren Abschnitten einschließlich SnB verwiesen wird):

Der Prozessor erkennt das XOR eines Registers bei sich selbst als auf Null gesetzt. Ein spezielles Tag im Register merkt sich, dass der obere Teil des Registers Null ist, so dass EAX = AL ist. Dieses Tag wird sogar in einer Schleife gespeichert:

    ; Example    7.9. Partial register problem avoided in loop
    xor    eax, eax
    mov    ecx, 100
LL:
    mov    al, [esi]
    mov    [edi], eax    ; No extra uop
    inc    esi
    add    edi, 4
    dec    ecx
    jnz    LL

(ab S. 82): Der Prozessor merkt sich, dass die oberen 24 Bits von EAX Null sind, solange Sie keinen Interrupt, keine falsche Vorhersage oder ein anderes Serialisierungsereignis erhalten.

pg82 diese Führung auch bestätigt , dass mov reg, 0ist nicht als Nullstellung Idiom anerkannt, zumindest auf frühen P6 - Designs wie PIII oder PM. Ich wäre sehr überrascht, wenn sie Transistoren für die Erkennung auf späteren CPUs ausgeben würden.


xorsetzt Flags , was bedeutet, dass Sie beim Testen der Bedingungen vorsichtig sein müssen. Da dies setccleider nur mit einem 8-Bit-Ziel verfügbar ist , müssen Sie normalerweise darauf achten, dass Teilregistrierungsstrafen vermieden werden.

Es wäre schön gewesen, wenn x86-64 einen der entfernten Opcodes (wie AAM) für ein 16/32 setcc r/m/ 64-Bit umfunktioniert hätte , wobei das Prädikat im 3-Bit-Feld des Quellregisters des R / M-Felds (übrigens) codiert wäre Einige andere Einzeloperandenbefehle verwenden sie als Opcode-Bits. Aber das haben sie nicht getan, und das würde für x86-32 sowieso nicht helfen.

Idealerweise sollten Sie xor/ set flags / setcc/ read full register verwenden:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

Dies hat eine optimale Leistung auf allen CPUs (keine Blockierungen, Zusammenführen von Uops oder falsche Abhängigkeiten).

Die Dinge sind komplizierter, wenn Sie nicht vor einer Anweisung zum Setzen von Flags xor möchten . Beispiel: Sie möchten unter denselben Bedingungen auf eine Bedingung verzweigen und dann auf eine andere Bedingung setzen. zB cmp/jle, seteund Sie haben entweder kein Ersatzregister oder Sie möchten xorden nicht genommenen Codepfad ganz ausschließen.

Es gibt keine erkannten Nullpunkt-Idiome, die keine Auswirkungen auf Flags haben. Die beste Wahl hängt daher von der Ziel-Mikroarchitektur ab. Auf Core2 kann das Einfügen eines zusammengeführten Uops zu einem Stillstand von 2 oder 3 Zyklen führen. Bei SnB scheint es billiger zu sein, aber ich habe nicht viel Zeit damit verbracht, zu messen. Die Verwendung von mov reg, 0/ setcchätte bei älteren Intel-CPUs erhebliche Nachteile und wäre bei neueren Intel-Geräten noch etwas schlechter.

Die Verwendung von setcc/ movzx r32, r8ist wahrscheinlich die beste Alternative für Intel P6- und SnB-Familien, wenn Sie vor dem Flag-Setting-Befehl nicht xor-zero können. Das sollte besser sein, als den Test nach einem Xor-Zeroing zu wiederholen. (Nicht einmal überlegen sahf/ lahfoder pushf/ popf). IvB kann eliminieren movzx r32, r8(dh mit Registerumbenennung ohne Ausführungseinheit oder Latenz wie xor- zeroing umgehen). Haswell und später eliminieren nur reguläre movAnweisungen, nehmen also movzxeine Ausführungseinheit und haben eine Latenz ungleich Null, was test / setcc/ movzxschlechter als xor/ test / macht setcc, aber immer noch mindestens so gut wie test / mov r,0/ setcc(und viel besser auf älteren CPUs).

Die Verwendung von setcc/ movzxohne Nullung ist bei AMD / P4 / Silvermont schlecht, da Deps für Unterregister nicht separat verfolgt werden. Es würde eine falsche Abhängigkeit vom alten Wert des Registers geben. Die Verwendung von mov reg, 0/ setcczum Nullstellen / Unterbrechen von Abhängigkeiten ist wahrscheinlich die beste Alternative, wenn xor/ test / setcckeine Option ist.

Wenn die setccAusgabe nicht breiter als 8 Bit sein soll, müssen Sie natürlich nichts auf Null setzen. Achten Sie jedoch auf falsche Abhängigkeiten von anderen CPUs als P6 / SnB, wenn Sie ein Register auswählen, das kürzlich Teil einer langen Abhängigkeitskette war. (Und seien Sie vorsichtig, wenn Sie eine Funktion aufrufen, mit der das Register, in dem Sie einen Teil verwenden, gespeichert / wiederhergestellt werden kann.)


andmit einer unmittelbaren Null ist nicht speziell unabhängig von dem alten Wert auf allen mir bekannten CPUs, sodass die Abhängigkeitsketten nicht unterbrochen werden. Es hat keine Vor- xorund Nachteile.

Es ist nur sinnvoll für Microbenchmarks zu schreiben , wenn Sie wollen eine Abhängigkeit als Teil einer Latenztest, sondern wollen durch Nullsetzen und das Hinzufügen eines bekannten Wert schaffen.


Unter http://agner.org/optimize/ finden Sie Details zu Mikroarch , einschließlich der Frage, welche Nullpunkt-Idiome als Abhängigkeitsunterbrechung erkannt werden (z. B. sub same,samebei einigen, aber nicht bei allen CPUs, während xor same,samesie bei allen erkannt werden). movDadurch wird die Abhängigkeitskette für den alten Wert unterbrochen des Registers (unabhängig vom Quellwert, Null oder nicht, denn so movfunktioniert es). xorUnterbricht Abhängigkeitsketten nur in dem Sonderfall, in dem src und dest dasselbe Register sind, weshalb movsie in der Liste der speziell erkannten Abhängigkeitsunterbrecher nicht aufgeführt sind. (Auch, weil es nicht als Null-Redewendung erkannt wird, mit den anderen Vorteilen, die sich daraus ergeben.)

Interessanterweise ist die älteste P6 - Design (PPro durch Pentium III) nicht erkennen xorals Abhängigkeit Brecher -zeroing, nur als Nullstellung Idiom für die Zwecke der Vermeidung von Teilregisterstände , so dass in einigen Fällen war es lohnt sich der Einsatz beide mov und dann xor- Null in dieser Reihenfolge, um die Dep zu brechen und dann wieder Null zu setzen + Setzen Sie das interne Tag-Bit so, dass die High-Bits Null sind, also EAX = AX = AL.

Siehe Beispiel 6.17 von Agner Fog. in seinem Mikroarch pdf. Er sagt, dass dies auch für P2, P3 und sogar (früh?) PM gilt. Ein Kommentar zu dem verlinkten Blog-Beitrag besagt, dass nur PPro dieses Versehen hatte, aber ich habe auf Katmai PIII und @Fanael auf einem Pentium M getestet, und wir haben beide festgestellt, dass es keine Abhängigkeit für eine Latenz aufhebt -gebundene imulKette. Dies bestätigt leider die Ergebnisse von Agner Fog.


TL: DR:

Wenn es Ihren Code wirklich schöner macht oder Anweisungen speichert, sollten Sie auf Null setzen mov, um ein Berühren der Flags zu vermeiden, solange Sie kein anderes Leistungsproblem als die Codegröße einführen. Das Vermeiden von Clobbering-Flags ist der einzig vernünftige Grund für die Nichtverwendung xor, aber manchmal können Sie xor-zero vor dem Ding setzen, das Flags setzt, wenn Sie ein Ersatzregister haben.

mov-zero vor setccist für die Latenz besser als movzx reg32, reg8nach (außer bei Intel, wenn Sie verschiedene Register auswählen können), aber schlechtere Codegröße.

Peter Cordes
quelle
7
Die meisten arithmetischen Anweisungen OP R, S werden von einer CPU außerhalb der Reihenfolge gezwungen, darauf zu warten, dass der Inhalt des Registers R durch vorherige Anweisungen mit Register R als Ziel gefüllt wird; Dies ist eine Datenabhängigkeit. Der entscheidende Punkt ist, dass Intel / AMD-Chips über spezielle Hardware verfügen, um die Abhängigkeit von Daten, die auf Daten R warten müssen, von Register R zu lösen, wenn XOR R, R angetroffen wird, und dies nicht unbedingt für andere Registernullierungsanweisungen. Dies bedeutet, dass die XOR-Anweisung für die sofortige Ausführung geplant werden kann. Aus diesem Grund empfiehlt Intel / AMD die Verwendung.
Ira Baxter
3
@IraBaxter: Ja, und um Verwirrung zu vermeiden (weil ich dieses Missverständnis bei SO gesehen habe), werden mov reg, srcauch Dep-Ketten für OO-CPUs unterbrochen (unabhängig davon, ob src imm32 [mem]oder ein anderes Register ist). Dieser Abhängigkeitsbruch wird in Optimierungshandbüchern nicht erwähnt, da es sich nicht um einen Sonderfall handelt, der nur auftritt, wenn src und dest dasselbe Register sind. Es passiert immer für Anweisungen, die nicht von ihrem Ziel abhängen. (Mit Ausnahme von Intels Implementierung popcnt/lzcnt/tzcnteiner falschen Abhängigkeit vom Ziel)
Peter Cordes
2
@Zboson: Die "Latenz" eines Befehls ohne Abhängigkeiten ist nur wichtig, wenn sich eine Blase in der Pipeline befindet. Es ist gut für die Eliminierung von Moves, aber für das Nullstellen von Anweisungen kommt der Vorteil der Nulllatenz erst nach so etwas wie einem Verzweigungsfehler oder I $ miss ins Spiel, bei dem die Ausführung auf die decodierten Anweisungen wartet und nicht darauf, dass die Daten bereit sind. Aber ja, Mov-Elimination macht nicht movfrei, nur keine Latenz. Der Teil "keinen Ausführungsport nehmen" ist normalerweise nicht wichtig. Der Fused-Domain-Durchsatz kann leicht der Engpass sein, insb. mit Ladungen oder Speichern in der Mischung.
Peter Cordes
2
Laut Agner erkennt KNL die Unabhängigkeit von 64-Bit-Registern nicht an. So xor r64, r64vergeudet nicht nur ein Byte. Wie Sie sagen, xor r32, r32ist die beste Wahl vor allem mit KNL. Weitere Informationen finden Sie in Abschnitt 15.7 "Sonderfälle der Unabhängigkeit" in diesem Mikrarch-Handbuch.
Z Boson
3
Ah, wo ist das gute alte MIPS mit seinem "Nullregister", wenn Sie es brauchen?
Hayalci