web-dev-qa-db-fra.com

Quel est le meilleur moyen de définir un registre à zéro dans x86 Assembly: xor, mov ou et?

Toutes les instructions suivantes font la même chose: set %eax à zéro. De quelle manière est optimal (nécessitant le moins de cycles de la machine)?

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

TL; résumé de DR : xor same, same est le meilleur choix pour tous les processeurs . Aucune autre méthode n’a un avantage sur elle, et elle a au moins un avantage sur toute autre méthode. C'est officiellement recommandé par Intel et AMD. En mode 64 bits, utilisez toujours xor r32, r32, parce que en écrivant dans la partie supérieure 32 les zéros réglés sur 32 bits . xor r64, r64 est une perte d'octet, car il nécessite un préfixe REX.

Pire encore, Silvermont ne reconnaît que xor r32,r32 comme dep-break, pas une taille d'opérande de 64 bits. Ainsi même lorsqu'un préfixe REX est toujours requis parce que vous mettez à zéro r8..r15, utilisez xor r10d,r10d, ne pas xor r10,r10 _ .

Exemples:

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

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

xorps xmm, xmm peut avoir un sens. Son octet est plus court que pxor, mais xorps a besoin du port d’exécution 5 sur Intel Nehalem, tandis que pxor peut s’exécuter sur n’importe quel port (0/1/5). (La latence de 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 le masquer au début d'une nouvelle chaîne de dépendance).

Sur les microarchitectures de la famille SnB, aucune version de xor-zeroing n’a besoin d’un port d’exécution. Sous AMD et Intel pré-Nehalem P6/Core2, xorps et pxor sont traités de la même manière (en tant qu’instructions de vecteur entier).

En utilisant la version AVX d’une instruction vectorielle 128b, la partie supérieure du registre est également mise à zéro, donc vpxor xmm, xmm, xmm est un bon choix pour la remise à zéro de YMM (AVX1/AVX2) ou ZMM (AVX512), ou de toute extension de vecteur future. vpxor ymm, ymm, ymm ne nécessite toutefois pas d'octets supplémentaires à encoder et fonctionne de la même manière. La mise à zéro de l’AVX512 ZMM nécessiterait des octets supplémentaires (pour le préfixe EVEX), la mise à zéro XMM ou YMM devrait donc être préférée.


Certains processeurs reconnaissent sub same,same comme idiome de remise à zéro comme xor, mais tous les processeurs qui reconnaissent un idiome de remise à zéro reconnaissent xor. Utilisez simplement xor pour ne pas avoir à vous soucier de savoir 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 certains avantages subtils (liste récapitulative, je développerai ensuite ceux-ci):

  • 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).
  • ne pas utiliser une unité d'exécution, économiser de l'énergie et libérer des ressources d'exécution. (Famille Intel SnB)
  • uop plus petit (pas de données immédiates) laisse de la place dans la ligne de cache uop pour les instructions à emprunter à proximité si nécessaire. (Famille Intel SnB).
  • n'utilise pas d'entrées dans le fichier de registre physique . (Intel SnB-family (et P4) au moins, peut-être aussi AMD, car 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 taille de code machine plus petite (2 octets au lieu de 5) est toujours un avantage: une densité de code plus élevée entraîne moins d'erreurs dans le cache des instructions et une meilleure extraction des instructions et potentiellement décoder la bande passante.


L'avantage de ne pas utiliser une unité d'exécution pour xor sur des microarchitectures de la famille Intel SnB est mineur, mais permet d'économiser de l'énergie. Cela est plus susceptible d’importer sur SnB ou IvB, qui ne dispose que de 3 ports d’exécution d’ALU. Haswell et ses versions ultérieures disposent de 4 ports d’exécution capables de gérer des instructions ALU entières, dont mov r32, imm32, donc avec une prise de décision parfaite de la part du planificateur (ce qui n’arrive pas dans la pratique), HSW peut toujours supporter 4 uops par horloge même quand ils ont tous besoin de ports d’exécution.

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

blog de Bruce Dawson que Michael Petch a lié (dans un commentaire sur la question) indique que xor est traité à l'étape de changement de nom du registre sans avoir besoin d'une unité d'exécution (zéro uops dans domaine non fusionné), mais a manqué le fait qu’il reste encore un uop dans le domaine fusionné. Les processeurs Intel modernes peuvent émettre et retirer 4 Uops de domaine fondu par horloge. C'est de là que vient la limite de 4 zéros par horloge. L’augmentation de la complexité du matériel de changement de nom de registre n’est qu’une des raisons pour lesquelles la largeur de la conception a été réduite à 4. (Bruce a écrit d’excellents billets de blog, comme sa série sur FP math et x87/SSE/problèmes d'arrondi , ce que je recommande vivement).


Sur les processeurs de la famille des bulldozers AMD , mov immediate s'exécute sur les mêmes ports d’exécution de nombres entiers EX0/EX1 que xor. mov reg,reg peut également fonctionner sur AGU0/1, mais cela ne sert que pour la copie des registres, pas pour la configuration immédiate. Donc, autant que je sache, sur AMD, le seul avantage de xor par rapport à mov est le codage le plus court. Cela pourrait également économiser des ressources de registre physique, 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).

xor va marquer le registre comme ayant les parties supérieures mises à zéro , donc xor eax, eax/inc al/inc eax évite la pénalité de registre partiel habituelle des processeurs antérieurs à IvB. 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 le supprime même.

Extrait du guide sur la microarchie d'Agner Fog, page 98 (section Pentium M, référencée par des sections ultérieures, notamment SnB):

Le processeur reconnaît le XOR d'un registre avec lui-même le mettant à zéro. Une balise spéciale dans le registre rappelle que la partie haute du registre est zéro, de sorte que EAX = AL. Cette balise est rappelés même dans une 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

(de la page 82): Le processeur se souvient que les 24 bits supérieurs d'EAX sont nuls tant que vous ne recevez pas d'interruption, de prédiction erronée ou autre événement de sérialisation.

la page 82 de ce guide confirme également que mov reg, 0 est pas reconnu comme un idiome de la réduction à zéro, du moins sur les conceptions P6 antérieures telles que PIII ou PM. Je serais très surpris s'ils utilisaient des transistors pour le détecter sur des processeurs ultérieurs.


xor définit des indicateurs , ce qui signifie que vous devez faire attention lorsque vous testez des conditions. Puisque setcc est malheureusement disponible uniquement avec une destination 8 bits , vous devez généralement veiller à é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 bit setcc r/m, avec le prédicat codé dans le champ de 3 bits du champ r/m du registre source (façon dont certaines autres instructions à un seul opérande les utilisent comme bits de code d'opération). Mais ils ne l'ont pas fait, et cela n'aiderait pas pour x86-32 de toute façon.

Idéalement, vous devriez utiliser xor/set flags/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 blocage, pas de fusion ou de fausses dépendances).

Les choses sont plus compliquées lorsque vous ne voulez pas modifier avant une instruction de mise en drapeau . par exemple. vous souhaitez créer une branche à une condition, puis définir une autre condition à partir des mêmes indicateurs. par exemple. cmp/jle, sete et vous n’avez pas de registre de secours ou vous voulez garder le xor en dehors du chemin de code non utilisé.

Il n'y a pas d'idiome de réduction à zéro reconnu qui n'affecte pas les indicateurs. Le meilleur choix dépend donc de la microarchitecture cible. Sur Core2, l’insertion d’un uop en 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. En utilisant mov reg, 0/setcc aurait une pénalité significative sur les anciens processeurs Intel, et serait encore un peu pire sur les nouveaux Intel.

Utiliser setcc/movzx r32, r8 est probablement la meilleure alternative pour les familles Intel P6 et SnB, si vous ne pouvez pas xor-zéro avant l’instruction de mise en drapeau. Cela devrait être mieux que de répéter le test après une réduction à zéro. (Ne considérez même pas sahf/lahf ou pushf/popf.) IvB peut éliminer movzx r32, r8 (c'est-à-dire le gérer avec un changement de nom de registre sans unité d'exécution ni latence, comme avec xor-zeroing). Haswell et les versions ultérieures n'éliminent que les instructions mov habituelles, de sorte que movzx prend une unité d'exécution et a une latence non nulle, rendant test/setcc/movzx pire que xor/test/setcc, mais toujours au moins aussi bon que test/mov r,0/setcc (et beaucoup mieux sur les anciens processeurs).

Utiliser setcc/movzx sans remise à zéro en premier est mauvais sur AMD/P4/Silvermont, car ils ne suivent pas les dépôts séparément pour les sous-registres. Il y aurait un faux dep sur l'ancienne valeur du registre. En utilisant mov reg, 0/setcc pour la remise à zéro/la suppression des dépendances est probablement la meilleure alternative lorsque xor/test/setcc n'est pas une option.

Bien sûr, si vous n'avez pas besoin que la sortie de setcc soit supérieure à 8 bits, vous n'avez pas besoin de mettre à zéro quoi que ce soit. Cependant, méfiez-vous des fausses dépendances de processeurs autres que P6/SnB si vous choisissez un registre faisant récemment partie d'une longue chaîne de dépendances. (Et méfiez-vous des registres partiels ou supplémentaires si vous appelez une fonction susceptible de sauvegarder/restaurer le registre dont vous faites partie.)


and avec un zéro immédiat n'est pas casé comme indépendant de l'ancienne valeur sur tous les processeurs que je sache, donc cela ne casse pas les chaînes de dépendance. Il n'a aucun avantage sur xor et de nombreux inconvénients.

Voir http://agner.org/optimize/ pour la documentation sur microarch, y compris les idiomes de réduction à zéro qui sont reconnus comme une rupture de dépendance (par exemple, sub same,same est sur certains processeurs mais pas sur tous, alors que xor same,same est reconnu sur tous.) mov rompt la chaîne de dépendances avec l'ancienne valeur du registre (quelle que soit la valeur source, nulle ou non, car c'est ainsi que mov fonctionne). xor ne rompt que les chaînes de dépendance dans le cas spécial où src et dest sont le même registre, raison pour laquelle mov est exclu de la liste de spécialement briseur de dépendance reconnu. (En outre, parce que ce n'est pas reconnu comme un idiome de réduction à zéro, avec les autres avantages que cela comporte.)

Fait intéressant, la conception la plus ancienne de P6 (PPro à Pentium III) non reconnaît xor- la réduction à zéro en tant que disjoncteur de dépendance, uniquement comme idiome de la réduction à zéro dans le but d'éviter -register stalls, donc dans certains cas cela valait la peine d'utiliser les deux. (Voir l'exemple 6.17 d'Agner Fog dans son microarch pdf. Il dit que cela s'applique également à P2, à P3 et même à (tôt?) PM. n commentaire sur l'article de blog lié dit que c'est seulement PPro que eu cet oubli, mais j’ai testé sur Katmai PIII et @Fanael sur un Pentium M, et nous avons tous deux constaté qu’il ne rompait pas la dépendance pour une chaîne liée à la latence imul.)


Si cela rend vraiment votre code plus agréable ou enregistre des instructions, alors bien sûr, mettez à zéro avec mov pour éviter de toucher les drapeaux, tant que vous ne présentez pas de problème de performance autre que la taille du code. Éviter les drapeaux clobeurs est la seule raison raisonnable de ne pas utiliser xor.

195
Peter Cordes