Pourquoi les instructions x86-64 sur les registres 32 bits mettent à zéro la partie supérieure du registre 64 bits complet?

119

Dans la visite x86-64 des manuels Intel , j'ai lu

Le fait le plus surprenant est peut-être qu'une instruction telle que met MOV EAX, EBXautomatiquement à zéro les 32 bits supérieurs du RAXregistre.

La documentation Intel (3.4.1.1 Registres à usage général en mode 64 bits en architecture de base manuelle) citée à la même source nous dit:

  • Les opérandes 64 bits génèrent un résultat 64 bits dans le registre à usage général de destination.
  • Les opérandes 32 bits génèrent un résultat 32 bits, étendu de zéro à un résultat 64 bits dans le registre général de destination.
  • Les opérandes 8 bits et 16 bits génèrent un résultat 8 bits ou 16 bits. Les 56 bits ou 48 bits supérieurs (respectivement) du registre universel de destination ne sont pas modifiés par l'opération. Si le résultat d'une opération 8 bits ou 16 bits est destiné au calcul d'adresse 64 bits, étendez explicitement le registre à 64 bits.

Dans les assemblages x86-32 et x86-64, des instructions 16 bits telles que

mov ax, bx

ne montrez pas ce genre de comportement "étrange" que le mot supérieur de eax est mis à zéro.

Ainsi: quelle est la raison pour laquelle ce comportement a été introduit? À première vue, cela semble illogique (mais la raison pourrait être que je suis habitué aux bizarreries de l'assemblage x86-32).

Nubok
la source
16
Si vous recherchez sur Google "Blocage du registre partiel", vous trouverez pas mal d'informations sur le problème qu'ils essayaient (presque certainement) d'éviter.
Jerry Coffin
4
Pas seulement "la plupart". AFAIK, toutes les instructions avec un r32opérande de destination zéro le 32 haut, plutôt que de fusionner. Par exemple, certains assembleurs remplaceront pmovmskb r64, xmmpar pmovmskb r32, xmm, en enregistrant un REX, car la version de destination 64 bits se comporte de la même manière. Même si la section Operation du manuel répertorie les 6 combinaisons de 32 / 64bit dest et 64/128 / 256b source séparément, l'extension zéro implicite du formulaire r32 duplique l'extension zéro explicite du formulaire r64. Je suis curieux de connaître la mise en œuvre de HW ...
Peter Cordes
2
@HansPassant, la référence circulaire commence.
kchoi

Réponses:

98

Je ne suis pas AMD ou ne parle pas pour eux, mais j'aurais fait de la même manière. Parce que la remise à zéro de la moitié supérieure ne crée pas une dépendance à la valeur précédente, que le processeur devrait attendre. Le mécanisme de renommage de registre serait essentiellement vaincu s'il n'était pas fait de cette façon.

De cette façon, vous pouvez écrire du code rapide en utilisant des valeurs 32 bits en mode 64 bits sans avoir à rompre explicitement les dépendances tout le temps. Sans ce comportement, chaque instruction 32 bits en mode 64 bits devrait attendre quelque chose qui s'est passé auparavant, même si cette partie haute ne serait presque jamais utilisée. (La création de int64 bits gaspillerait l'encombrement du cache et la bande passante mémoire; x86-64 prend en charge le plus efficacement les tailles d'opérande 32 et 64 bits )

Le comportement des tailles d'opérande 8 et 16 bits est étrange. La folie des dépendances est l'une des raisons pour lesquelles les instructions 16 bits sont désormais évitées. x86-64 a hérité de 8086 pour 8 bits et 386 pour 16 bits, et a décidé de faire fonctionner les registres 8 et 16 bits de la même manière en mode 64 bits qu'en mode 32 bits.


Voir aussi Pourquoi GCC n'utilise-t-il pas de registres partiels? pour des détails pratiques sur la manière dont les écritures dans les registres partiels 8 et 16 bits (et les lectures ultérieures du registre complet) sont gérées par des processeurs réels.

Harold
la source
8
Je ne pense pas que ce soit étrange, je pense qu'ils ne voulaient pas trop casser et ont conservé l'ancien comportement là-bas.
Alexey Frunze
5
@Alex quand ils ont introduit le mode 32 bits, il n'y avait pas d'ancien comportement pour la partie haute. Il n'y avait pas de partie haute avant .. Bien sûr, après cela, cela ne pouvait plus être changé.
harold
1
Je parlais d'opérandes 16 bits, pourquoi les bits supérieurs ne sont pas remis à zéro dans ce cas. Ils ne le font pas dans les modes non 64 bits. Et cela est également conservé en mode 64 bits.
Alexey Frunze
3
J'ai interprété votre "Le comportement des instructions 16 bits est étrange" comme "il est étrange que l'extension zéro ne se produise pas avec des opérandes 16 bits en mode 64 bits". D'où mes commentaires sur le fait de le garder de la même manière en mode 64 bits pour une meilleure compatibilité.
Alexey Frunze
8
@Alex oh je vois. D'accord. Je ne pense pas que ce soit étrange de ce point de vue. Juste à partir d'une perspective «avec le recul, peut-être que ce n'était pas une si bonne idée». Je suppose que j'aurais dû être plus clair :)
harold
9

Cela économise simplement de l'espace dans les instructions et le jeu d'instructions. Vous pouvez déplacer de petites valeurs immédiates vers un registre 64 bits à l'aide d'instructions existantes (32 bits).

Cela vous évite également d'avoir à encoder des valeurs de 8 octets pour MOV RAX, 42, quand MOV EAX, 42peut être réutilisé.

Cette optimisation n'est pas aussi importante pour les opérations 8 et 16 bits (car elles sont plus petites), et changer les règles à cet endroit casserait également l'ancien code.

Bo Persson
la source
7
Si c'est correct, cela n'aurait-il pas plus de sens qu'il s'étende par signe plutôt que 0?
Damien_The_Unbeliever
16
L'extension de signe est plus lente, même dans le matériel. L'extension zéro peut être effectuée en parallèle avec tout calcul qui produit la moitié inférieure, mais l'extension du signe ne peut pas être effectuée tant que (au moins le signe de) la moitié inférieure n'a pas été calculée.
Jerry Coffin
13
Une autre astuce connexe consiste à utiliser XOR EAX, EAXcar XOR RAX, RAXil faudrait un préfixe REX.
Neil
3
@Nubok: Bien sûr, ils auraient pu ajouter un encodage de movzx / movsx qui prend un argument immédiat. La plupart du temps, il est plus pratique de remettre à zéro les bits supérieurs, vous pouvez donc utiliser une valeur comme index de tableau (car tous les regs doivent avoir la même taille dans une adresse effective: [rsi + edx]n'est pas autorisé). Bien sûr, éviter les fausses dépendances / les blocages de registres partiels (l'autre réponse) est une autre raison majeure.
Peter Cordes
4
et changer les règles là-bas casserait également l'ancien code. De toute façon, l'ancien code ne peut pas fonctionner en mode 64 bits (par exemple, inc / dec 1 octet sont des préfixes REX); ce n'est pas pertinent. La raison de ne pas nettoyer les verrues de x86 est moins de différences entre le mode long et les modes compat / legacy, donc moins d'instructions doivent décoder différemment selon le mode. AMD ne savait pas qu'AMD64 allait faire son chemin, et était malheureusement très conservateur, il faudrait donc moins de transistors pour le prendre en charge. À long terme, cela aurait été bien si les compilateurs et les humains devaient se rappeler quelles choses fonctionnent différemment en mode 64 bits.
Peter Cordes
1

Sans zéro s'étendant à 64 bits, cela signifierait qu'une instruction lisant à partir de raxaurait 2 dépendances pour son raxopérande (l'instruction qui écrit eaxet l'instruction qui écrit raxavant), cela signifie que 1) le ROB devrait avoir des entrées pour plusieurs dépendances pour un seul opérande, ce qui signifie que le ROB exigerait plus de logique et de transistors et prendrait plus d'espace, et que l'exécution serait plus lente en attendant une deuxième dépendance inutile qui pourrait prendre des siècles à s'exécuter; ou bien 2), ce qui, je suppose, arrive avec les instructions 16 bits, l'étape d'allocation s'arrête probablement (c'est-à-dire que si le RAT a une allocation active pour une axécriture et qu'une eaxlecture apparaît, elle se bloque jusqu'à ce que l' axécriture se retire).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

Le seul avantage d'une extension non nulle est de s'assurer que les bits d'ordre supérieur de raxsont inclus, par exemple, s'il contient à l'origine 0xffffffffffffffff, le résultat serait 0xffffffff00000007, mais il y a très peu de raisons pour que l'ISA fasse cette garantie à un tel coût, et il est plus probable que l'avantage de l'extension zéro soit en fait plus nécessaire, ce qui permet d'économiser la ligne de code supplémentaire mov rax, 0. En garantissant qu'il sera toujours zéro étendu à 64 bits, les compilateurs peuvent travailler avec cet axiome à l'esprit tandis que dedans mov rdx, rax, raxn'a qu'à attendre sa seule dépendance, ce qui signifie qu'il peut commencer l'exécution plus rapidement et se retirer, libérant des unités d'exécution. En outre, il permet également des idiomes zéro plus efficaces comme xor eax, eaxzéro raxsans nécessiter un octet REX.

Lewis Kelsey
la source
Les indicateurs partiels sur Skylake fonctionnent au moins en ayant des entrées séparées pour CF par rapport à tout SPAZO. (Donc cmovbec'est 2 uops mais cmovbc'est 1). Mais aucun processeur qui renomme un registre partiel ne le fait comme vous le suggérez. Au lieu de cela, ils insèrent un uop de fusion si un reg partiel est renommé séparément du reg complet (c'est-à-dire qu'il est "sale"). Voir Pourquoi GCC n'utilise-t-il pas de registres partiels? et Comment fonctionnent exactement les registres partiels sur Haswell / Skylake? L'écriture d'AL semble avoir une fausse dépendance à RAX, et AH est incohérente
Peter Cordes
Les processeurs de la famille P6 sont soit bloqués pendant ~ 3 cycles pour insérer un uop de fusion (Core2 / Nehalem), soit les anciens processeurs de la famille P6 (PM, PIII, PII, PPro) stagnent juste pendant (au moins?) ~ 6 cycles. C'est peut-être comme vous l'avez suggéré dans 2, en attendant que la valeur reg complète soit disponible via l'écriture différée dans le fichier de registre permanent / architectural.
Peter Cordes
@PeterCordes oh, je savais qu'il fallait fusionner des uops au moins pour les calages partiels des drapeaux. Cela a du sens, mais j'ai oublié comment cela fonctionne pendant une minute; il a cliqué une fois mais j'ai oublié de prendre des notes
Lewis Kelsey
@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXJe ne trouve pas d'exemple de la `` fusion uop '' qui serait utilisée pour résoudre ce problème, même chose pour un décrochage partiel du drapeau
Lewis Kelsey
Bon, le début de P6 se ​​bloque jusqu'à l'écriture différée. Core2 et Nehalem insèrent un uop fusionnant après / avant? ne bloque le front-end que pendant un temps plus court. Les inserts Sandybridge fusionnent sans caler. (Mais la fusion AH doit se produire dans un cycle en soi, tandis que la fusion AL peut faire partie d'un groupe complet.) Haswell / SKL ne renomme pas du tout AL séparément de RAX, il en mov al, [mem]va de même pour une charge micro-fusionnée + ALU- merge, renommer uniquement AH, et un uop de fusion AH pose toujours problème seul. Les mécanismes de fusion d'indicateurs partiels dans ces processeurs varient, par exemple Core2 / Nehalem se bloque toujours pour les indicateurs partiels, contrairement à partial-reg.
Peter Cordes le