Quelle est la meilleure façon de définir un registre à zéro dans un assemblage x86: xor, mov ou et?


120

Toutes les instructions suivantes font la même chose: mettre %eaxà zéro. Quelle voie est optimale (nécessitant le moins de cycles machine)?

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

6
Vous voudrez peut-être lire cet article
Michael Petch

Réponses:


222

TL; Résumé DR : xor same, sameest le meilleur choix pour tous les processeurs . Aucune autre méthode n'a aucun avantage sur elle, et elle a au moins un avantage sur toute autre méthode. Il est officiellement recommandé par Intel et AMD, et ce que font les compilateurs. En mode 64 bits, utilisez toujours xor r32, r32, car l' écriture d'un reg 32 bits remet à zéro les 32 supérieurs . xor r64, r64est un gaspillage d'octet, car il a besoin d'un préfixe REX.

Pire encore, Silvermont ne reconnaît xor r32,r32que la taille d'un opérande de 64 bits, pas de rupture de dép. Ainsi, même si un préfixe REX est toujours nécessaire parce que vous mettez à zéro r8..r15, utilisez xor r10d,r10d, nonxor r10,r10 .

Exemples de GP-integer:

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

Il est généralement préférable de mettre à zéro un registre vectoriel avec pxor xmm, xmm. C'est généralement ce que fait gcc (même avant utilisation avec les instructions FP).

xorps xmm, xmmpeut avoir du sens. C'est un octet plus court que pxor, mais xorpsnécessite le port d'exécution 5 sur Intel Nehalem, alors qu'il pxorpeut fonctionner sur n'importe quel port (0/1/5). (La latence du délai de contournement 2c de Nehalem entre entier et FP n'est généralement pas pertinente, car une exécution dans le désordre peut généralement la masquer au début d'une nouvelle chaîne de dépendances).

Sur les microarchitectures de la famille SnB, aucune des versions de xor-zeroing n'a même besoin d'un port d'exécution. Sur AMD et pré-Nehalem P6 / Core2 Intel, xorpset pxorsont gérées de la même manière (comme des instructions vecteur-entier).

L'utilisation de la version AVX d'une instruction vectorielle 128b met également vpxor xmm, xmm, xmmà zéro la partie supérieure du reg, c'est donc un bon choix pour la remise à zéro de YMM (AVX1 / AVX2) ou ZMM (AVX512), ou de toute future extension vectorielle. vpxor ymm, ymm, ymmne prend pas d'octets supplémentaires à encoder, cependant, et fonctionne de la même manière sur Intel, mais plus lentement sur AMD avant Zen2 (2 uops). La mise à zéro AVX512 ZMM nécessiterait des octets supplémentaires (pour le préfixe EVEX), donc la mise à zéro XMM ou YMM devrait être préférée.

Exemples XMM / YMM / ZMM

    # 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.

Voir La mise à zéro de vxorps sur AMD Jaguar / Bulldozer / Zen est-elle plus rapide avec des registres xmm que ymm? et
Quelle est la manière la plus efficace d'effacer un ou quelques registres ZMM sur Knights Landing?

Semi-lié: Le moyen le plus rapide de définir la valeur __m256 sur tous les bits ONE et de
définir tous les bits du registre du processeur sur 1 couvre également efficacement lesk0..7 registres de masque AVX512 . SSE / AVX vpcmpeqdest dépendant de beaucoup (bien qu'il ait encore besoin d'un uop pour écrire les 1), mais AVX512 vpternlogdpour les regs ZMM n'est même pas dépendant. À l'intérieur d'une boucle, envisagez de copier à partir d'un autre registre au lieu de recréer ceux avec un uop ALU, en particulier avec AVX512.

Mais la remise à zéro n'est pas chère: la mise à zéro d'un reg xmm dans une boucle est généralement aussi bonne que la copie, sauf sur certains processeurs AMD (Bulldozer et Zen) qui ont une élimination de mov pour les regs vectoriels mais qui ont toujours besoin d'un uop ALU pour écrire des zéros pour xor -zéro.


Quelle est la particularité de la remise à zéro des idiomes comme xor sur divers uarches

Certains processeurs reconnaissent sub same,samecomme un idiome de remise à zéro xor, mais tous les processeurs qui reconnaissent les idiomes de remise à zéro le reconnaissentxor . Utilisez simplement xorpour ne pas avoir à vous soucier de quel processeur reconnaît quel idiome de remise à zéro.

xor(étant un idiome de réduction à zéro reconnu, contrairement à mov reg, 0) présente des avantages évidents et subtils (liste récapitulative, je vais les développer):

  • taille de code inférieure à mov reg,0. (Tous les processeurs)
  • évite les pénalités de registre partiel pour le code ultérieur. (Famille Intel P6 et famille SnB).
  • n'utilise pas d'unité d'exécution, économisant de l'énergie et libérant des ressources d'exécution. (Famille Intel SnB)
  • un uop plus petit (pas de données immédiates) laisse de la place dans la ligne de cache uop pour les instructions à proximité à emprunter si nécessaire. (Famille Intel SnB).
  • n'utilise pas les entrées du fichier de registre physique . (Au moins la famille Intel SnB (et P4), peut-être aussi AMD puisqu'ils utilisent une conception PRF similaire au lieu de conserver l'état du registre dans le ROB comme les microarchitectures de la famille Intel P6.)

Une plus petite taille de code machine (2 octets au lieu de 5) est toujours un avantage: une densité de code plus élevée conduit à moins d'erreurs de cache d'instructions, et une meilleure extraction des instructions et potentiellement décodage de la bande passante.


L'avantage de ne pas utiliser d'unité d'exécution pour xor sur les microarchitectures de la famille Intel SnB est mineur, mais économise de l'énergie. Il est plus probable que cela soit important sur SnB ou IvB, qui n'ont que 3 ports d'exécution ALU. Haswell et les versions ultérieures ont 4 ports d'exécution qui peuvent gérer des instructions ALU entières, y compris mov r32, imm32, donc avec une prise de décision parfaite par le planificateur (ce qui ne se produit pas toujours dans la pratique), HSW pourrait toujours supporter 4 uops par horloge même quand ils ont tous besoin d'ALU ports d'exécution.

Voir ma réponse à une autre question sur la remise à zéro des registres pour plus de détails.

Le billet de blog de Bruce Dawson que Michael Petch a lié (dans un commentaire sur la question) souligne qu'il xorest traité à l'étape du changement de nom du registre sans avoir besoin d'une unité d'exécution (zéro uops dans le domaine non fusionné), mais a manqué le fait qu'il reste un uop dans le domaine fusionné. Les processeurs Intel modernes peuvent émettre et retirer 4 uops de domaine fusionné par horloge. C'est de là que vient la limite de 4 zéros par horloge. La complexité accrue du matériel de renommage des registres n'est qu'une des raisons pour lesquelles la largeur de la conception est limitée à 4. (Bruce a écrit d'excellents articles de blog, comme sa série sur les problèmes de mathématiques FP et x87 / SSE / arrondi , ce que je fais recommande fortement).


Sur les processeurs de la famille AMD Bulldozer , mov immediates'exécute sur les mêmes ports d'exécution d'entiers EX0 / EX1 que xor. mov reg,regpeut également fonctionner sur AGU0 / 1, mais ce n'est que pour la copie de registre, pas pour la configuration à partir de l'immédiat. Donc , autant que je sache, sur AMD le seul avantage de xorplus movest l'encodage plus court. Cela pourrait également économiser des ressources de registre physiques, mais je n'ai vu aucun test.


Les idiomes de remise à zéro reconnus évitent les pénalités de registre partiel sur les processeurs Intel qui renomment les registres partiels séparément des registres complets (familles P6 et SnB).

xormarquera le registre comme ayant les parties supérieures mises à zéro , donc xor eax, eax/ inc al/ inc eaxévite la pénalité habituelle de registre partiel que les CPU pré-IvB ont. Même sans xor, IvB n'a besoin d'un uop de fusion que lorsque les 8 bits élevés ( AH) sont modifiés et que tout le registre est lu, et Haswell supprime même cela.

Extrait du guide microarch d'Agner Fog, p. 98 (section Pentium M, référencée par les sections suivantes, y compris SnB):

Le processeur reconnaît le XOR d'un registre avec lui-même comme le mettant à zéro. Une balise spéciale dans le registre se souvient que la partie haute du registre est zéro de sorte que EAX = AL. Cette balise est mémorisée même en boucle:

    ; 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

(à partir de pg82): Le processeur se souvient que les 24 bits supérieurs d'EAX sont à zéro tant que vous n'obtenez pas d'interruption, d'erreur de prédiction ou autre événement de sérialisation.

pg82 de ce guide confirme également que ce mov reg, 0n'est pas reconnu comme un idiome de réduction à zéro, du moins sur les premières conceptions P6 comme PIII ou PM. Je serais très surpris s'ils passaient des transistors à le détecter sur les processeurs ultérieurs.


xordéfinit des indicateurs , ce qui signifie que vous devez faire attention lorsque vous testez les conditions. Comme il setccn'est malheureusement disponible qu'avec une destination 8 bits , vous devez généralement prendre soin d'éviter les pénalités de registre partiel.

Cela aurait été bien si x86-64 avait réutilisé l'un des opcodes supprimés (comme AAM) pour un 16/32/64 bits setcc r/m, avec le prédicat codé dans le champ 3 bits du registre source du champ r / m (la manière certaines autres instructions à un seul opérande les utilisent comme bits d'opcode). Mais ils ne l'ont pas fait, et cela n'aiderait pas de toute façon pour x86-32.

Idéalement, vous devriez utiliser xor/ définir des indicateurs / setcc/ lire le registre complet:

...
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

Cela a des performances optimales sur tous les processeurs (pas de décrochage, de fusion d'ups ou de fausses dépendances).

Les choses sont plus compliquées lorsque vous ne voulez pas xor avant une instruction de réglage de drapeau . par exemple, vous voulez créer une branche sur une condition, puis setcc sur une autre condition à partir des mêmes indicateurs. par exemple cmp/jle, seteet soit vous n'avez pas de registre de rechange, soit vous voulez garder xorcomplètement le chemin de code non pris.

Il n'y a pas d'idiomes de remise à zéro reconnus qui n'affectent pas les indicateurs, donc le meilleur choix dépend de la microarchitecture cible. Sur Core2, l'insertion d'un uop de fusion peut provoquer un blocage de 2 ou 3 cycles. Cela semble être moins cher sur SnB, mais je n'ai pas passé beaucoup de temps à essayer de mesurer. L'utilisation de mov reg, 0/ setccaurait une pénalité significative sur les anciens processeurs Intel, et serait encore un peu pire sur les nouveaux Intel.

L'utilisation de setcc/ movzx r32, r8est probablement la meilleure alternative pour les familles Intel P6 et SnB, si vous ne pouvez pas xor-zero avant l'instruction de réglage du drapeau. Cela devrait être mieux que de répéter le test après un xor-zeroing. (Ne considérez même pas sahf/ lahfou pushf/ popf). IvB peut éliminer movzx r32, r8(c'est-à-dire le gérer avec un renommage de registre sans unité d'exécution ni latence, comme xor-zeroing). Haswell et les versions ultérieures n'éliminent que les movinstructions régulières , donc movzxprend une unité d'exécution et a une latence non nulle, ce qui rend test / setcc/ movzxpire que xor/ test / setcc, mais toujours au moins aussi bon que test / mov r,0/ setcc(et bien meilleur sur les anciens processeurs).

Utiliser setcc/ movzxsans remise à zéro en premier est mauvais sur AMD / P4 / Silvermont, car ils ne suivent pas les déps séparément pour les sous-registres. Il y aurait un faux dépendant de l'ancienne valeur du registre. Utiliser mov reg, 0/ setccpour la remise à zéro / la rupture de dépendance est probablement la meilleure alternative lorsque xor/ test / setccn'est pas une option.

Bien sûr, si vous n'avez pas besoin setccd'une sortie de plus de 8 bits, vous n'avez rien à mettre à zéro. Cependant, méfiez-vous des fausses dépendances sur les processeurs autres que P6 / SnB si vous choisissez un registre qui faisait récemment partie d'une longue chaîne de dépendances. (Et méfiez-vous de provoquer un décrochage partiel du reg ou un uop supplémentaire si vous appelez une fonction qui pourrait sauvegarder / restaurer le registre dont vous utilisez une partie.)


andavec un zéro immédiat n'est pas une casse spéciale comme indépendante de l'ancienne valeur sur les processeurs que je connais, donc cela ne rompt pas les chaînes de dépendance. Il n'a pas d'avantages xoret de nombreux inconvénients.

Cela n'est utile que pour écrire des microbenchmarks lorsque vous voulez une dépendance dans le cadre d'un test de latence, mais que vous voulez créer une valeur connue en mettant à zéro et en ajoutant.


Voir http://agner.org/optimize/ pour plus de détails sur les microarchives , y compris les idiomes de remise à zéro qui sont reconnus comme brisant les dépendances (par exemple, sub same,samesur certains processeurs mais pas sur tous, alors qu'ils xor same,samesont reconnus sur tous.) movRompt la chaîne de dépendances sur l'ancienne valeur du registre (quelle que soit la valeur source, zéro ou non, car c'est ainsi que ça movmarche). xorne casse les chaînes de dépendances que dans le cas spécial où src et dest sont le même registre, c'est pourquoi il movest exclu de la liste des disjoncteurs de dépendances spécialement reconnus. (De plus, parce que ce n'est pas reconnu comme un idiome de réduction à zéro, avec les autres avantages que cela comporte.)

Il est intéressant de noter que la conception la plus ancienne de P6 (PPro à Pentium III) ne reconnaissait pas la mise àxor zéro comme un disjoncteur de dépendance, uniquement comme un idiome de remise à zéro dans le but d'éviter les blocages de registres partiels , donc dans certains cas, il valait la peine d'utiliser les deux mov , puis xor-zéro dans cet ordre pour casser le dep, puis à nouveau à zéro + définir le bit de balise interne que les bits hauts sont à zéro donc EAX = AX = AL.

Voir l'exemple 6.17 d'Agner Fog. dans son pdf microarch. Il dit que cela s'applique également à P2, P3 et même (tôt?) PM. Un commentaire sur l'article de blog lié dit que seul PPro avait cette oubli, mais j'ai testé sur Katmai PIII et @Fanael testé sur un Pentium M, et nous avons tous deux constaté qu'il ne cassait pas une dépendance pour une latence imulchaîne liée . Cela confirme les résultats d'Agner Fog, malheureusement.


TL: DR:

Si cela rend vraiment votre code plus agréable ou enregistre des instructions, alors bien sûr, zéro avec movpour éviter de toucher les indicateurs, tant que vous n'introduisez pas de problème de performances autre que la taille du code. Éviter les drapeaux écrasants est la seule raison raisonnable de ne pas utiliser xor, mais parfois vous pouvez xor-zéro avant la chose qui définit les drapeaux si vous avez un registre de rechange.

mov-zéro avant setccest meilleur pour la latence movzx reg32, reg8qu'après (sauf sur Intel lorsque vous pouvez choisir différents registres), mais la taille du code est pire.


7
La plupart des instructions arithmétiques OP R, S sont forcées par une CPU dans le désordre d'attendre que le contenu du registre R soit rempli par des instructions précédentes avec le registre R comme cible; c'est une dépendance de données. Le point clé est que les puces Intel / AMD ont un matériel spécial pour briser les dépendances à attendre les données sur le registre R lorsque XOR R, R est rencontré, et ne le fait pas nécessairement pour les autres instructions de remise à zéro du registre. Cela signifie que l'instruction XOR peut être planifiée pour une exécution immédiate, et c'est pourquoi Intel / AMD recommande de l' utiliser.
Ira Baxter

3
@IraBaxter: Oui, et juste pour éviter toute confusion (parce que j'ai vu cette idée fausse sur SO), mov reg, srcbrise également les chaînes dep pour les processeurs OO (indépendamment du fait que src soit imm32 [mem], ou d'un autre registre). Cette rupture de dépendance n'est pas mentionnée dans les manuels d'optimisation car ce n'est pas un cas spécial qui se produit uniquement lorsque src et dest sont le même registre. Cela arrive toujours pour des instructions qui ne dépendent pas de leur destination. (sauf pour l'implémentation d'Intel d' popcnt/lzcnt/tzcntavoir un faux dép sur la destination.)
Peter Cordes

2
@Zboson: La "latence" d'une instruction sans dépendances n'a d'importance que s'il y avait une bulle dans le pipeline. C'est bien pour l'élimination des mouvements, mais pour la remise à zéro des instructions, l'avantage de la latence zéro n'entre en jeu qu'après quelque chose comme une erreur de branche ou I $ miss, où l'exécution attend les instructions décodées, plutôt que les données soient prêtes. Mais oui, l'élimination des mouvements ne rend pas movgratuit, seulement zéro latence. La partie "ne pas prendre de port d'exécution" n'est généralement pas importante. Le débit du domaine fusionné peut facilement être le goulot d'étranglement, en particulier. avec des charges ou des magasins dans le mélange.
Peter Cordes

2
Selon Agner, KNL ne reconnaît pas l'indépendance des registres 64 bits. Donc xor r64, r64ne gaspille pas seulement un octet. Comme vous le dites, xor r32, r32c'est le meilleur choix, surtout avec KNL. Voir la section 15.7 «Cas particuliers d'indépendance» dans ce manuel de micrarch si vous souhaitez en savoir plus.
boson Z

3
ah, où est le bon vieux MIPS, avec son "zéro registre" quand vous en avez besoin.
hayalci
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.