it-swarm.com.de

Wie kann ein Register in x86-Assembly am besten auf Null gesetzt werden: xor, mov oder and?

Alle folgenden Anweisungen machen dasselbe: set %eax bis Null. Welcher Weg ist optimal (erfordert nur wenige Maschinenzyklen)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax
107
balajimc55

TL; DR Zusammenfassung: xor same, same Ist die beste Wahl für alle CPUs. Keine andere Methode hat einen Vorteil gegenüber der anderen, und sie hat zumindest einen gewissen Vorteil gegenüber jeder anderen Methode. Es wird offiziell von Intel und AMD empfohlen. Verwenden Sie im 64-Bit-Modus immer noch xor r32, r32, Da Schreiben einer 32-Bit-Reg-Null die oberen 32 . xor r64, r64 Ist eine Verschwendung von Byte, da es ein REX-Präfix benötigt.

Schlimmer noch, Silvermont erkennt xor r32,r32 Nur als abbruchkräftig und nicht als 64-Bit-Operandengröße. Also auch wenn ein REX-Präfix noch erforderlich ist, weil Sie r8..r15 auf Null setzen, verwenden Sie xor r10d,r10d, Nicht xor r10,r10.

Beispiele:

xor   eax, eax       ; RAX = 0
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
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes

Das Nullsetzen eines Vektorregisters erfolgt normalerweise am besten mit pxor xmm, xmm. Das macht normalerweise gcc (sogar vor der Verwendung mit FP Anweisungen)).

xorps xmm, xmm Kann sinnvoll sein. Es ist ein Byte kürzer als pxor, aber xorps benötigt Ausführungsport 5 auf Intel Nehalem, während pxor auf jedem Port (0/1/5) ausgeführt werden kann. (Nehalems 2c-Bypass-Verzögerungszeit zwischen Integer und FP ist normalerweise nicht relevant, da die Ausführung außerhalb der Reihenfolge sie normalerweise am Anfang einer neuen Abhängigkeitskette ausblenden kann).

Auf Mikroarchitekturen der SnB-Familie benötigt weder die Xor-Zero-Variante noch eine Ausführungsschnittstelle. Bei AMD und Intel vor Nehalem P6/Core2 werden xorps und pxor auf die gleiche Weise behandelt (wie bei Vektor-Integer-Anweisungen).

Bei Verwendung der AVX-Version eines 128b-Vektorbefehls wird auch der obere Teil des Registers auf Null gesetzt. Daher ist vpxor xmm, xmm, xmm Eine gute Wahl, um YMM (AVX1/AVX2) oder ZMM (AVX512) oder eine zukünftige Vektorerweiterung auf Null zu setzen. vpxor ymm, ymm, ymm Benötigt jedoch keine zusätzlichen Bytes für die Codierung und wird genauso ausgeführt. Das Nullsetzen des AVX512-ZMM erfordert zusätzliche Bytes (für das EVEX-Präfix), daher sollte das Nullsetzen von XMM oder YMM bevorzugt werden.


Einige CPUs erkennen sub same,same Als Nullstellungs-IDiom wie xor, aber alle CPUs, die Nullstellungs-IDiome erkennen, erkennen xor. Verwenden Sie einfach xor, damit Sie sich keine Gedanken darüber machen müssen, welche CPU welche Nullstellungssprache erkennt.

xor (im Gegensatz zu mov reg, 0 eine anerkannte Nullstellungssprache) hat einige offensichtliche und subtile Vorteile (Zusammenfassungsliste, dann werde ich diese erweitern):

  • kleinere Codegröße als mov reg,0. (Alle CPUs)
  • vermeidet Teilregister-Strafen 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 die Intel SnB-Familie (und P4), möglicherweise auch AMD, da sie ein ähnliches PRF-Design verwenden, anstatt den Registerstatus im ROB beizubehalten, wie bei den Mikroarchitekturen der Intel P6-Familie.)

Kleinere Maschinencode-Größe (2 Bytes statt 5) ist immer ein Vorteil: Eine höhere Codedichte führt zu weniger Fehlschlägen im Befehls-Cache und zu einem besseren Abrufen und potenziellen Dekodieren der Bandbreite von Befehlen.


Der Vorteil von keine Ausführungseinheit verwenden für xor auf Mikroarchitekturen der Intel SnB-Familie ist gering, spart jedoch Strom. Es ist wahrscheinlicher, dass es auf SnB oder IvB ankommt, die nur 3 ALU-Ausführungsports haben. Haswell und neuere Versionen verfügen über 4 Ausführungsports, die ganzzahlige ALU-Anweisungen verarbeiten können, einschließlich mov r32, imm32, Sodass HSW bei perfekter Entscheidungsfindung durch den Scheduler (was in der Praxis nicht der Fall ist) immer noch 4 Uops pro Takt aufrecht erhalten kann wenn sie alle Ausführungsports benötigen.

Siehe meine Antwort auf eine andere Frage zum Nullstellen von Registern für weitere Einzelheiten.

Bruce Dawsons Blogpost dass Michael Petch (in einem Kommentar zu der Frage) darauf hinweist, dass xor beim Umbenennen des Registers behandelt wird, ohne dass eine Ausführungseinheit erforderlich ist (null Ups in der nicht verschmolzene Domain), aber es wurde übersehen, dass es immer noch ein UOP in der verschmolzenen Domain ist. Moderne Intel-CPUs können 4 Fused-Domain-Ups pro Takt ausgeben und stilllegen. Das ist, wo die 4 Nullen pro Taktbegrenzung herkommen. Die zunehmende Komplexität der Hardware zum Umbenennen von Registern ist nur einer der Gründe für die Beschränkung der Breite des Designs auf 4. (Bruce hat einige sehr gute Blog-Beiträge verfasst, z. B. seine Serie über FP math und x87/SSE/Rundungsprobleme , die ich sehr empfehlen kann).


Auf CPUs der AMD Bulldozer-Familie, mov immediate Wird auf denselben EX0/EX1-Integer-Ausführungsports ausgeführt wie xor. mov reg,reg Kann auch auf AGU0/1 ausgeführt werden, jedoch nur zum Kopieren von Registern, nicht zum Setzen von Sofort-Codes. Also AFAIK, bei AMD ist der einzige Vorteil von xor gegenüber mov die kürzere Codierung. Es könnte auch physische Registerressourcen sparen, aber ich habe keine Tests gesehen.


Erkannte Nullstellungs-Idiome Strafen für Teilregister vermeiden auf Intel-CPUs, die Teilregister getrennt von Vollregistern umbenennen (P6- und SnB-Familien).

xor wird das Register als mit den oberen Teilen auf Null gesetzt markieren, so dass xor eax, eax/inc al/inc eax die üblichen Teil- Register Strafe, die Pre-IvB-CPUs haben. Selbst ohne xor muss IvB nur dann zusammengeführt werden, wenn die hohen 8-Bit-Werte (AH) geändert wurden und dann das gesamte Register gelesen wird, und Haswell entfernt dies sogar.

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

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

    ; 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 kein anderes Serialisierungsereignis erhalten.

seite 82 dieses Handbuchs bestätigt auch, dass mov reg, 0 nicht als Nullstellungssprache erkannt wird, zumindest bei 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.


xor setzt Flags, was bedeutet, dass Sie beim Testen der Bedingungen vorsichtig sein müssen. Da setcc leider nur mit einem 8-Bit-Ziel verfügbar ist, müssen Sie in der Regel darauf achten, dass Sie keine Teilregister-Strafen erleiden.

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

Im Idealfall 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 Unterbrechungen, Zusammenführen von Ups oder falsche Abhängigkeiten).

Die Dinge sind komplizierter, wenn Sie nicht vor einer Flag-Setz-Anweisung xor wollen. z.B. Sie möchten unter einer Bedingung verzweigen und dann unter einer anderen Bedingung von denselben Flags aus setzen. z.B. cmp/jle, sete, und Sie haben entweder kein Ersatzregister, oder Sie möchten den xor ganz aus dem nicht verwendeten Codepfad heraushalten.

Es gibt keine erkannten Nullstellen-Idiome, die sich nicht auf Flags auswirken. Die beste Wahl hängt also von der Ziel-Mikroarchitektur ab. Auf Core2 kann das Einfügen eines Merge-Ups zu einem Stillstand von 2 oder 3 Zyklen führen. Es scheint auf SnB billiger zu sein, aber ich habe nicht viel Zeit damit verbracht, zu messen. Die Verwendung von mov reg, 0/setcc hätte einen erheblichen Nachteil für ältere Intel-CPUs und wäre bei neueren Intel-Prozessoren noch etwas schlechter.

Die Verwendung von setcc/movzx r32, r8 Ist wahrscheinlich die beste Alternative für Intel P6- und SnB-Familien, wenn Sie vor dem Befehl zum Setzen des Flags keine Nullen setzen können. Das sollte besser sein, als den Test nach einem Nullabgleich zu wiederholen. (Berücksichtigen Sie nicht einmal sahf/lahf oder pushf/popf). IvB kann movzx r32, r8 Eliminieren (d. H. Mit Registerumbenennung ohne Ausführungseinheit oder Latenz, wie xor-zeroing, umgehen). Haswell und später eliminieren nur reguläre mov Anweisungen, also nimmt movzx eine Ausführungseinheit und hat eine Latenz ungleich Null, was test/setcc/movzx schlechter macht als xor/test/setcc, aber immer noch mindestens so gut wie test/mov r,0/setcc (und viel besser auf älteren CPUs).

Die Verwendung von setcc/movzx ohne vorherige Nullsetzung ist bei AMD/P4/Silvermont schlecht, da die Abhängigkeiten nicht separat für Unterregister nachverfolgt werden. Es würde eine falsche Abhängigkeit vom alten Wert des Registers geben. Die Verwendung von mov reg, 0/setcc zum Nullsetzen/Auflösen von Abhängigkeiten ist wahrscheinlich die beste Alternative, wenn xor/test/setcc keine Option ist.

Wenn Sie nicht möchten, dass die Ausgabe von setcc breiter als 8 Bit ist, 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 hüten Sie sich vor einem teilweisen Reg-Stall oder einem zusätzlichen UOP, wenn Sie eine Funktion aufrufen, die das Register, von dem Sie einen Teil verwenden, speichern/wiederherstellen könnte.)


and mit einer unmittelbaren Null ist kein Sonderfall, da er auf allen mir bekannten CPUs vom alten Wert unabhängig ist, sodass die Abhängigkeitsketten nicht unterbrochen werden. Es hat keine Vorteile gegenüber xor und viele Nachteile.

Unter http://agner.org/optimize/ finden Sie eine Dokumentation zu den Mikroarchiven, einschließlich der Angaben, welche Nullstellensprachen als Abhängigkeitsbruch erkannt werden (z. B. sub same,same Ist auf einigen, aber nicht allen CPUs vorhanden, während xor same,same Wird auf allen erkannt.) mov unterbricht die Abhängigkeitskette vom alten Wert des Registers (unabhängig vom Quellwert, null oder nicht, da mov so funktioniert) . xor unterbricht Abhängigkeitsketten nur in dem speziellen Fall, in dem src und dest dasselbe Register sind, weshalb mov aus der Liste der besonders anerkannte Abhängigkeitsbrecher. (Auch, weil es nicht als Nullstellungssprache erkannt wird, mit den anderen Vorteilen, die es mit sich bringt.)

Interessanterweise hat das älteste P6-Design (PPro bis Pentium III) xor- Nullstellen nicht als Abhängigkeitsunterbrecher erkannt , sondern nur als Nullstellungssprache für diese Zwecke Es hat sich in einigen Fällen gelohnt, beide zu verwenden. (Siehe Beispiel 6.17 von Agner Fog in seinem Mikroarchitektur-PDF. Er sagt, dass dies auch für P2, P3 und sogar (frühe?) PM gilt. Ein Kommentar zum verlinkten Blog-Beitrag sagt, dass es nur PPro war hatte dieses Versehen, aber ich habe es auf Katmai PIII und @Fanael auf einem Pentium M getestet, und wir fanden beide, dass es keine Abhängigkeit für eine latenzgebundene imul Kette aufbrach.)


Wenn es Ihren Code wirklich netter macht oder Anweisungen speichert, dann setzen Sie mit mov eine Null, um das Berühren der Flags zu vermeiden, solange Sie kein anderes Leistungsproblem als die Codegröße einführen. Das Vermeiden von Clobber-Flags ist jedoch der einzig vernünftige Grund, warum xor nicht verwendet wird.

195
Peter Cordes