Comment fonctionnent exactement les registres partiels sur Haswell/Skylake? Écrire AL semble avoir une fausse dépendance sur RAX, et AH est incohérent
Cette boucle s'exécute à une itération par 3 cycles sur Intel Conroe / Merom, goulot d'étranglement sur le débit imul
comme prévu. Mais sur Haswell / Skylake, il s'exécute à une itération par 11 cycles, apparemment parce que {[2] } a une dépendance sur le dernier imul
.
; synthetic micro-benchmark to test partial-register renaming
mov ecx, 1000000000
.loop: ; do{
imul eax, eax ; a dep chain with high latency but also high throughput
imul eax, eax
imul eax, eax
dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4)
setnz al ; ****** Does this depend on RAX as well as ZF?
movzx eax, al
jnz .loop ; }while(ecx);
Si setnz al
dépend de rax
, la séquence 3ximul/setcc/movzx forme une chaîne de dépendance portée en boucle. Si non, chaque setcc
/movzx
/la chaîne 3x imul
est indépendante, bifurquée du dec
qui met à jour le compteur de boucle. Le 11C par l'itération mesurée sur HSW / SKL s'explique parfaitement par un goulot d'étranglement de latence: 3x3c (imul) + 1c (read-modify-write by setcc) + 1c (movzx dans le même registre).
Hors sujet: éviter ces goulets d'étranglement (intentionnels)
J'allais pour un comportement compréhensible / prévisible pour isoler des choses partielles-reg, pas des performances optimales.
Par exemple xor
-zéro / set-drapeaux / setcc
est mieux de toute façon (dans ce cas, xor eax,eax
/ dec ecx
/ setnz al
). Que les pauses le dep sur eax sur tous les processeurs (sauf la famille P6 précoce comme PII et PIII), évite toujours les pénalités de fusion de registre partiel, et économise 1c de movzx
latence. Il utilise également un uop alu de moins sur les processeurs qui gèrent la mise à zéro xor dans l'étape register-rename . Voir ce lien pour plus d'informations sur l'utilisation de XOR-zeroing avec setcc
.
Notez QU'AMD, Intel Silvermont / KNL et P4 ne font pas du tout de renommage de registre partiel. Ce n'est qu'une fonctionnalité dans les processeurs Intel P6-family et son descendant, Intel Sandybridge-famille, mais semble être éliminés.
Gcc a malheureusement tendance à utiliser cmp
/ setcc al
/ movzx eax,al
où il aurait pu utiliser xor
au lieu de movzx
(Godbolt compiler-explorer exemple) , tandis que clang utilise xor-zero / cmp / setcc sauf si vous combinez plusieurs conditions booléennes comme count += (a==b) | (a==~b)
.
La version xor/dec/setnz s'exécute à 3.0 c par itération sur Skylake, Haswell et Core2 (goulot d'étranglement sur le débit imul
). xor
- la réduction à zéro rompt la dépendance sur l'ancienne valeur de eax
sur tous les processeurs hors service autres que PPro / PII / PIII / early-Pentium-M (où elle évite toujours les pénalités de fusion de registre partiel mais ne casse pas le dep). Le Guide de microarchitecture D'Agner Fog décrit ceci. Remplacer le XOR-zeroing par mov eax,0
le ralentit à un par 4,78 cycles sur Core2: décrochage 2-3c (dans le front-end?) pour insérer une UOP de fusion partielle-reg lorsque imul
lit eax
après setnz al
.
Aussi, j'ai utilisé movzx eax, al
qui défait MOV-élimination, tout comme mov rax,rax
fait. (IvB, HSW et SKL peuvent renommer movzx eax, bl
avec une latence 0, mais Core2 ne peut pas). Cela rend tout égal à travers Core2 / SKL, sauf pour le comportement de registre partiel.
Le comportement Core2 est cohérent avec Le guide microarch D'Agner Fog , mais le comportement HSW/SKL ne L'est pas. de la section 11.10 pour Skylake, et même pour les uarches Intel précédentes:
Différentes parties d'un registre général peuvent être stockées dans différents registres temporaires afin de supprimer les fausses dépendances.
Malheureusement, il n'a pas le temps de faire des tests détaillés pour chaque nouveau uarch pour tester à nouveau les hypothèses, donc ce changement de comportement a échappé aux mailles du filet.
Agner décrit un uop fusionnant inséré (sans décrochage) pour les registres high8 (AH/BH/CH/DH) sur Sandybridge via Skylake, et pour low8/low16 sur SnB. (J'ai malheureusement répandu des informations erronées dans le passé, et en disant que Haswell peut fusionner AH gratuitement. J'ai écrémé la section Haswell D'Agner trop rapidement, et je n'ai pas remarqué le paragraphe suivant sur les registres high8. Faites-moi savoir si vous voyez mes mauvais commentaires sur d'autres messages, afin que je puisse les supprimer ou ajouter une correction. Je vais essayer au moins de trouver et d'éditer mes réponses où je l'ai dit.)
Mes questions réelles: comment exactement les registres partiels se comportent-ils vraiment sur Skylake?
Est-ce que tout est pareil de IvyBridge à Skylake, y compris la latence supplémentaire high8?
Le manuel D'optimisation D'Intel n'est pas spécifique sur les processeurs qui ont de fausses dépendances pour quoi (bien qu'il mentionne que certains processeurs les ont), et laisse de côté des choses comme lire AH / BH/CH / DH (registres high8) en ajoutant une latence supplémentaire même s'ils n'ont pas été modifiés.
S'il y a un comportement P6-family (Core2/Nehalem) que le guide de Microarch d'Agner Fog ne décrit pas, ce serait intéressant aussi, mais je devrais probablement limiter la portée de cette question à Skylake ou Sandybridge-family.
Mes données de test Skylake , en mettant des séquences courtes %rep 4
dans une petite boucle dec ebp/jnz
qui exécute des itérations 100M ou 1G. J'ai mesuré les cycles avec Linux perf
de la même manière que dans ma réponse ici, sur le même matériel (bureau Skylake i7 6700k).
Sauf indication contraire, chaque instruction s'exécute en tant que 1 uop de domaine fusionné, en utilisant un ALU l'exécution de port. (Mesurée avec ocperf.py stat -e ...,uops_issued.any,uops_executed.thread
). Cela détecte (absence de) élimination mov et UOPs de fusion supplémentaires.
Les cas "4 par cycle" sont une extrapolation au cas infiniment déroulé. La surcharge de boucle prend une partie de la bande passante frontale, mais quelque chose de mieux que 1 par cycle est une indication que le changement de nom de registre évite la dépendance de sortie écriture après écriture, et que l'uop n'est pas géré en interne comme un lire-modifier-écrire.
Écriture à AH seulement : empêche la boucle de s'exécuter à partir du tampon de bouclage (alias Le détecteur de flux de boucle (LSD)). Les comptes pour lsd.uops
sont exactement 0 sur HSW, et minuscules sur SKL (autour de 1.8 k) et ne sont pas mis à l'échelle avec le nombre d'itérations de boucle. Probablement ces comptes proviennent d'un code du noyau. Lorsque les boucles s'exécutent à partir du LSD, lsd.uops ~= uops_issued
à l'intérieur du bruit de mesure. Certaines boucles alternent entre LSD ou no-LSD (par exemple quand elles pourraient ne pas entrer dans le cache uop si decode commence au mauvais endroit), mais je n'ai pas rencontré cela en testant ceci.
- répété
mov ah, bh
et/oumov ah, bl
exécute à 4 par cycle. Il faut un uop ALU, donc il n'est pas éliminé commemov eax, ebx
est. - répété
mov ah, [rsi]
Fonctionne à 2 par cycle (goulot d'étranglement de débit de charge). - répété
mov ah, 123
Fonctionne à 1 par cycle. (A dep-rupturexor eax,eax
l'intérieur de la boucle supprime le goulot d'étranglement.) -
Répété
setz ah
ousetc ah
exécute à 1 par cycle. (Un dep-breakingxor eax,eax
permet de goulot d'étranglement sur le débit p06 Poursetcc
et la branche loop.)Pourquoi l'écriture de
ah
avec une instruction qui utiliserait normalement une unité D'exécution ALU a-t-elle une fausse dépendance sur l'ancienne valeur, alors quemov r8, r/m8
ne le fait pas (pour reg ou memory src)?{[98] }( et qu'en est-il demov r/m8, r8
? Sûrement, peu importe lequel des deux opcodes que vous utilisez pour les mouvements reg-reg?) Répété
add ah, 123
exécute à 1 par cycle, comme devrait.- répété
add dh, cl
Fonctionne à 1 par cycle. - répété
add dh, dh
Fonctionne à 1 par cycle. - répété
add dh, ch
Fonctionne à 0,5 par cycle. Lire [ABCD]H est spécial quand ils sont "propres" (dans ce cas, RCX n'est pas du tout modifié récemment).
Terminologie : tous ces éléments laissent AH (ou DH)" dirty ", c'est-à-dire nécessitant une fusion (avec une uop de fusion) lorsque le reste du registre est lu (ou dans d'autres cas). c'est à dire que AH est renommé séparément de RAX, si je comprends bien cela. "nettoyer" est le contraire. Il y a plusieurs façons de nettoyer un registre sale, le plus simple étant inc eax
ou mov eax, esi
.
Écrire à AL seulement : ces boucles fonctionnent à partir du LSD: uops_issue.any
~= lsd.uops
.
- répété
mov al, bl
fonctionne à 1 par cycle. Un DEP-breaking occasionnelxor eax,eax
par groupe permet un goulot D'étranglement de L'exécution OOO sur le débit uop, pas de latence. - répété
mov al, [rsi]
fonctionne à 1 par cycle, comme un micro-fusionné alu + charge uop. (uops_issued=4G + frais généraux de boucle, uops_executed = 8G + frais généraux de boucle). Un DEP-breakingxor eax,eax
avant qu'un groupe de 4 le laisse goulot d'étranglement sur 2 charges par horloge. - répété
mov al, 123
fonctionne à 1 par cycle. - répété
mov al, bh
fonctionne à 0,5 par cycle. (1 pour 2 cycles). Lire [ABCD] H est spécial. -
xor eax,eax
+ 6xmov al,bh
+dec ebp/jnz
: 2c par iter, goulot d'étranglement sur 4 uops par horloge pour le front-end. - répété
add dl, ch
fonctionne à 0.5 par cycle. (1 pour 2 cycles). La lecture de [ABCD]h crée apparemment une latence supplémentaire pourdl
. - répété
add dl, cl
fonctionne à 1 par cycle.
Je pense qu'une écriture dans un reg low-8 se comporte comme un mélange RMW dans le reg complet, comme le serait add eax, 123
, mais elle ne déclenche pas de fusion si ah
est sale. Donc (autre que d'ignorer AH
fusion) il se comporte de la même manière que sur les processeurs qui ne font pas du tout de renommage partiel. Il semble que AL
ne soit jamais renommé séparément de RAX
?
-
inc al
/inc ah
les paires peuvent fonctionner en parallèle. -
mov ecx, eax
insère une uop de fusion siah
est "sale", mais lemov
réel est renommé. C'est ce queAgner Fog décrit pour IvyBridge et plus tard. - répété
movzx eax, ah
fonctionne à un par 2 cycles. (La lecture des registres high-8 après l'écriture des regs complets a une latence supplémentaire.) -
movzx ecx, al
a une latence nulle et ne prend pas de port d'exécution sur HSW et SKL. (Comme ce que décrit Agner Fog IvyBridge, mais il dit que HSW ne renomme pas movzx). -
movzx ecx, cl
a une latence 1c et prend un port d'exécution. ( MOV-elimination ne fonctionne jamais pour le cassame,same
, seulement entre différents registres architecturaux.)Une boucle qui insère une uop de fusion chaque itération ne peut pas s'exécuter à partir du LSD (tampon de boucle)?
Je ne pense pas QU'il y ait quelque chose de spécial à propos de AL/AH/RAX vs. B*, C*, DL/DH / RDX. J'en ai testé certains avec des regs partiels dans d'autres registres (même si je montre surtoutAL
/AH
pour la cohérence), et n'ont jamais remarqué de différence.
Comment Pouvons-nous expliquer toutes ces observations avec un modèle sensible de la façon dont le microarch fonctionne en interne?
Connexes: Partielle drapeau questions sont différentes partielle s'inscrire questions. Voir INC instruction vs ADD 1: est-ce important? pour des trucs super-bizarres avec shr r32,cl
(et même shr r32,2
sur Core2 / Nehalem: ne lisez pas les drapeaux d'un décalage autre que par 1).
Voir aussi problèmes avec ADC / SBB et INC / DEC dans les boucles serrées sur certains processeurs pour les éléments de drapeau partiel dans les boucles adc
.
1 réponses
Autres réponses bienvenue pour aborder Sandybridge et IvyBridge plus en détail. Je n'ai pas accès à ce matériel.
Je n'ai trouvé aucune différence de comportement partielle entre HSW et SKL. Sur Haswell et Skylake, tout ce que j'ai testé jusqu'à présent supporte ce modèle:
AL n'est jamais renommé séparément de RAX (ou r15b à partir de r15). Donc, si vous ne touchez jamais les registres high8 (AH/BH / CH / DH), tout se comporte exactement comme sur un processeur sans partial - Reg renommage (par exemple AMD).
L'accès en écriture seule à AL fusionne dans RAX, avec une dépendance sur RAX. Pour les charges dans AL, il s'agit d'un uop alu+load micro-fusionné qui s'exécute sur p0156, ce qui est l'une des preuves les plus solides qu'il fusionne vraiment à chaque Écriture, et pas seulement faire une double comptabilité comme Agner a spéculé.
Agner (et Intel) disent que Sandybridge peut exiger une uop de fusion pour AL, donc il est probablement renommé séparément de RAX. Par La Bns, manuel d'optimisation D'Intel (section 3.5.2.4 stands de Registre partiel) dit
SnB (pas nécessairement plus tard uarches) insère une UOP fusion dans les cas suivants:
Après une écriture à L'un des registres AH, BH, CH ou Dh et avant un après lecture de la forme 2, 4 ou 8 octets du même registre. Dans dans ces cas, une fusion micro-op est insérée. l'insertion consomme un cycle d'allocation complet dans lequel d'autres micro-ops ne peuvent pas être alloué.
Après un micro-op avec un registre de destination de 1 ou 2 octets, qui est pas une source de l'instruction (ou la forme plus grande du registre), et avant une lecture suivante d'une forme de 2, 4 ou 8 octets de la même inscrire. Dans ces cas la fusion micro-op fait partie du flux.
Je pense qu'ils disent que sur SnB, {[5] } va RMW le RAX complet au lieu de le renommer séparément, parce que l'une des sources registres est (partie de) RAX. Je suppose que cela ne s'applique pas à une charge comme mov al, [rbx + rax]
; rax
dans un mode d'adressage ne compte probablement pas comme source.
Je n'ai pas testé si les uops de fusion high8 doivent toujours émettre / renommer eux-mêmes sur HSW / SKL. Cela rendrait l'impact frontal équivalent à 4 uops (puisque c'est le problème/renommer la largeur du pipeline).
- Il N'y a aucun moyen de briser une dépendance impliquant AL sans écrire EAX / RAX.
xor al,al
n'aide pas, et ni faitmov al, 0
. -
movzx ebx, al
a zéro latence (renommé), et a besoin d'aucune unité d'exécution. (i.e. MOV-elimination fonctionne sur HSW et SKL). il déclenche la fusion de AH si c'est sale, ce qui, je suppose, est nécessaire pour que cela fonctionne sans ALU. Ce n'est probablement pas une coïncidence si Intel a abandonné le renommage low8 dans le même uarch qui a introduit l'élimination mov. (Le guide micro-arch d'Agner Fog a une erreur ici, disant que les mouvements étendus à zéro ne sont pas éliminés sur HSW ou SKL, seulement IvB.) -
movzx eax, al
est pas éliminé à renommer. mov-élimination sur Intel ne fonctionne jamais pour même.mov rax,rax
n'est pas éliminé non plus, même s'il n'a rien à étendre à zéro. (Bien qu'il ne servirait à rien de lui donner un support matériel spécial, car c'est juste un no-op, contrairement àmov eax,eax
). Quoi qu'il en soit, préférez se déplacer entre deux registres architecturaux séparés lorsque l'extension zéro, que ce soit avec unmov
32 bits ou un 8 bitsmovzx
. -
movzx eax, bx
est pas éliminé au renommage sur HSW ou SKL. Il a une latence 1c et utilise un uop ALU. Le manuel d'optimisation d'Intel ne mentionne que la latence zéro pour movzx 8 bits (et souligne quemovzx r32, high8
n'est jamais renommé).
Les regs High-8 peuvent être renommés séparément du reste du registre et nécessitent une fusion des uops.
- accès en écriture seule à
ah
avecmov ah, r8
ou {[20] } renommez AH, sans dépendance à l'ancienne valeur. Ce sont deux instructions qui n'auraient normalement pas besoin D'un uop ALU (pour la version 32 bits). - un RMW de AH (comme
inc ah
) le salit. -
setcc ah
cela dépend de l'ancienah
, mais le salit toujours. Je pense quemov ah, imm8
est le même, mais n'a pas testé autant de cas de coin.(inexpliqué: une boucle impliquant
setcc ah
peut parfois s'exécuter à partir du LSD, voir la bouclercr
à la fin de ce post. Peut-être que tant queah
est propre à la fin de la boucle, il peut utiliser le LSD?).Si
ah
est sale,setcc ah
se confond avec le renomméah
, plutôt que de forcer une fusion dansrax
. par exemple%rep 4
(inc al
/test ebx,ebx
/setcc ah
/inc al
/inc ah
) ne génère aucune UOPs de fusion et ne s'exécute qu'à environ 8.7 c (latence de 8inc al
ralentie par les conflits de ressources des uops pourah
. Aussi leinc ah
/setcc ah
chaîne dep).Je pense que ce qui se passe ici est que
setcc r8
est toujours implémenté en lecture-modification-écriture. Intel a probablement décidé qu'il cela ne valait pas la peine d'avoir un uop en écriture seulesetcc
pour optimiser le cassetcc ah
, car il est très rare que le code généré par le compilateur soitsetcc ah
. (Mais voir le lien godbolt dans la question: clang4. 0 avec-m32
le fera.) La lecture de AX, EAX ou RAX déclenche une fusion uop (qui prend en charge le problème frontal / renommer la bande passante). Probablement le RAT (table D'Allocation de Registre) suit l'état high-8-dirty pour l'architecture R [ABCD]X, et même après une écriture à AH se retire, les données AH sont stocké dans un registre physique séparé de RAX. Même avec 256 NOPs entre l'écriture AH et la lecture EAX, il y a une UOP de fusion supplémentaire. (ROB size = 224 sur SKL, donc cela garantit que le
mov ah, 123
a été retiré). Détecté avec les compteurs uops_issued / executed perf, qui montrent clairement la différence.Lire-modifier-écrire de AL (par exemple
inc al
) fusionne gratuitement, dans le cadre de L'uop ALU. (Testé uniquement avec quelques simples uop, commeadd
/inc
, pasdiv r8
oumul r8
). Encore une fois, pas de fusion uop est déclenché même si AH est sale.Ecrire uniquement dans EAX / RAX (comme
lea eax, [rsi + rcx]
ouxor eax,eax
) efface L'état AH-dirty (pas de fusion uop).- Écriture seule à AX (
mov ax, 1
) déclenche une fusion de AH en premier. Je suppose qu'au lieu de spécial-casing cela, il fonctionne comme n'importe quel autre RMW de AX/RAX. (TODO: testmov ax, bx
, bien que cela ne devrait pas être spécial car il n'est pas renommé.) -
xor ah,ah
a une latence 1c, n'est pas Dep-breaking, et a toujours besoin d'une exécution port. - la lecture et / ou l'écriture de AL ne force pas une fusion, donc AH peut rester sale (et être utilisé indépendamment dans une chaîne dep séparée). (par exemple
add ah, cl
/add al, dl
peut fonctionner à 1 par horloge (goulot d'étranglement sur l'ajout de latence).
Rendre AH sale empêche une boucle de s'exécuter à partir du LSD (le tampon de boucle), même s'il n'y a pas de UOPs fusionnant. Le LSD est lorsque le processeur recycle uops dans la file d'attente qui alimente l'étape de problème/renommer. (Appelé le IDQ).
Insérer des uops de fusion est un peu comme insérer des UOPs de synchronisation de pile pour le moteur de pile. Le manuel d'optimisation d'Intel indique que le LSD de SnB ne peut pas exécuter de boucles avec des correspondances push
/pop
, ce qui est logique, mais cela implique qu'il peut exécuter des boucles avec équilibré push
/pop
. Ce n'est pas ce que je vois sur SKL: même équilibré push
/pop
empêche l'exécution du LSD (par exemple push rax
/ pop rdx
/ times 6 imul rax, rdx
. (Il peut y avoir une réelle différence entre le LSD de la BNS et le HSW / SKL: SnB peut simplement "verrouiller" les uops dans L'IDQ au lieu de les répéter plusieurs fois, donc une boucle 5-uop prend 2 cycles à émettre au lieu de 1.25.) Quoi qu'il en soit, il semble que HSW/SKL ne puisse pas utiliser le LSD lorsqu'un registre high-8 est sale ou lorsqu'il contient des uops de stack-engine.
Ce comportement peut être lié à un un erratum dans la touche programmable:
Problème: dans des conditions micro-architecturales complexes, des boucles courtes de moins de 64 instructions qui utilisent des registres AH, BH, CH ou DH ainsi que leurs registres plus larges correspondants (par exemple RAX, EAX ou AX pour AH) peuvent provoquer un comportement imprévisible du système. Cela ne peut se produire que lorsque les deux processeurs logiques sur le même processeur physique sont actifs.
Cela peut également être lié à la déclaration du manuel d'optimisation D'Intel selon laquelle SnB doit au moins problème / renommer un AH-merge uop dans un cycle par lui-même. C'est une différence étrange pour le front-end.
Mon Journal du noyau Linux dit microcode: sig=0x506e3, pf=0x2, revision=0x84
.
Le paquet intel-ucode
d'Arch Linux fournit simplement la mise à jour, vous devez éditer les fichiers de configuration pour les charger. Donc mon test Skylake était sur un i7-6700k avec la révision du microcode 0x84, qui n'inclut pas le correctif pour SKL150. Il correspond au comportement Haswell dans tous les cas que j'ai testés, IIRC. (par exemple Haswell et mon SKL peut exécuter le setne ah
/ add ah,ah
/ rcr ebx,1
/ mov eax,ebx
boucle du LSD). J'ai HT activé (ce qui est une condition préalable pour que SKL150 se manifeste), mais je testais sur un système principalement inactif, donc mon thread avait le noyau pour lui-même.
Avec microcode mis à jour, le LSD est complètement désactivé pour tout tout le temps, pas seulement lorsque les registres partiels sont actifs. lsd.uops
est toujours exactement zéro, y compris pour les programmes réels pas de boucles synthétiques. Bugs matériels (plutôt que microcode bugs) souvent nécessite la désactivation d'une fonctionnalité entière à corriger. C'est pourquoi SKL-avx512 (SKX) est signalé pour ne pas avoir de tampon de bouclage. Heureusement, ce n'est pas un problème de performance: le débit UOP-cache accru de SKL sur Broadwell peut presque toujours suivre le problème/renommer.
Extra ah/BH/CH/DH latence:
- Lire AH quand ce n'est pas sale (renommé séparément) ajoute un cycle de latence supplémentaire pour les deux opérandes. par exemple,
add bl, ah
a une latence de 2c de l'entrée BL à sortie BL, de sorte qu'il peut ajouter de la latence au chemin critique même si RAX et AH ne font pas partie de celui-ci. (J'ai déjà vu ce genre de latence supplémentaire pour l'autre opérande, avec une latence vectorielle sur Skylake, où un retard int/float "pollue" un registre pour toujours. TODO: écrire ça.)
Cela signifie déballer les octets avec movzx ecx, al
/ movzx edx, ah
a une latence supplémentaire vs. movzx
/shr eax,8
/movzx
, mais encore un meilleur débit.
-
Lire AH quand il est sale ne pas ajouter aucune latence. (
add ah,ah
ouadd ah,dh
/add dh,ah
ont 1C latence par ajouter). Je n'ai pas fait beaucoup de tests pour confirmer cela dans de nombreux cas.Hypothèse: une valeur high8 sale est stockée dans le fond d'un registre physique . La lecture d'un high8 propre nécessite un décalage pour extraire les bits [15: 8], mais la lecture d'un high8 sale peut simplement prendre des bits [7: 0] d'un registre physique comme une lecture normale de registre 8 bits.
Une latence supplémentaire ne signifie pas un débit réduit. Ce le programme peut fonctionner à 1 iter par 2 horloges, même si toutes les instructions add
ont une latence 2c (à partir de la lecture DH, qui n'est pas modifiée.)
global _start
_start:
mov ebp, 100000000
.loop:
add ah, dh
add bh, dh
add ch, dh
add al, dh
add bl, dh
add cl, dh
add dl, dh
dec ebp
jnz .loop
xor edi,edi
mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h
syscall ; sys_exit_group(0)
Performance counter stats for './testloop':
48.943652 task-clock (msec) # 0.997 CPUs utilized
1 context-switches # 0.020 K/sec
0 cpu-migrations # 0.000 K/sec
3 page-faults # 0.061 K/sec
200,314,806 cycles # 4.093 GHz
100,024,930 branches # 2043.675 M/sec
900,136,527 instructions # 4.49 insn per cycle
800,219,617 uops_issued_any # 16349.814 M/sec
800,219,014 uops_executed_thread # 16349.802 M/sec
1,903 lsd_uops # 0.039 M/sec
0.049107358 seconds time elapsed
Quelques corps de boucle de test intéressants :
%if 1
imul eax,eax
mov dh, al
inc dh
inc dh
inc dh
; add al, dl
mov cl,dl
movzx eax,cl
%endif
Runs at ~2.35c per iteration on both HSW and SKL. reading `dl` has no dep on the `inc dh` result. But using `movzx eax, dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge, and creates a loop-carried dep chain. (8c per iteration).
%if 1
imul eax, eax
imul eax, eax
imul eax, eax
imul eax, eax
imul eax, eax ; off the critical path unless there's a false dep
%if 1
test ebx, ebx ; independent of the imul results
;mov ah, 123 ; dependent on RAX
;mov eax,0 ; breaks the RAX dependency
setz ah ; dependent on RAX
%else
mov ah, bl ; dep-breaking
%endif
add ah, ah
;; ;inc eax
; sbb eax,eax
rcr ebx, 1 ; dep on add ah,ah via CF
mov eax,ebx ; clear AH-dirty
;; mov [rdi], ah
;; movzx eax, byte [rdi] ; clear AH-dirty, and remove dep on old value of RAX
;; add ebx, eax ; make the dep chain through AH loop-carried
%endif
La version setcc (avec le %if 1
) a une latence de boucle 20C, et s'exécute à partir du LSD même si elle a setcc ah
et add ah,ah
.
00000000004000e0 <_start.loop>:
4000e0: 0f af c0 imul eax,eax
4000e3: 0f af c0 imul eax,eax
4000e6: 0f af c0 imul eax,eax
4000e9: 0f af c0 imul eax,eax
4000ec: 0f af c0 imul eax,eax
4000ef: 85 db test ebx,ebx
4000f1: 0f 94 d4 sete ah
4000f4: 00 e4 add ah,ah
4000f6: d1 db rcr ebx,1
4000f8: 89 d8 mov eax,ebx
4000fa: ff cd dec ebp
4000fc: 75 e2 jne 4000e0 <_start.loop>
Performance counter stats for './testloop' (4 runs):
4565.851575 task-clock (msec) # 1.000 CPUs utilized ( +- 0.08% )
4 context-switches # 0.001 K/sec ( +- 5.88% )
0 cpu-migrations # 0.000 K/sec
3 page-faults # 0.001 K/sec
20,007,739,240 cycles # 4.382 GHz ( +- 0.00% )
1,001,181,788 branches # 219.276 M/sec ( +- 0.00% )
12,006,455,028 instructions # 0.60 insn per cycle ( +- 0.00% )
13,009,415,501 uops_issued_any # 2849.286 M/sec ( +- 0.00% )
12,009,592,328 uops_executed_thread # 2630.307 M/sec ( +- 0.00% )
13,055,852,774 lsd_uops # 2859.456 M/sec ( +- 0.29% )
4.565914158 seconds time elapsed ( +- 0.08% )
Inexpliqué: il court à partir du LSD, même si elle rend AH sale. (Du moins je pense il n'. TODO: essayez d'ajouter des instructions qui font quelque chose avec eax
avant que mov eax,ebx
ne l'efface.)
Mais avec mov ah, bl
, Il s'exécute en 5.0 c par itération (imul
goulot d'étranglement de débit) sur HSW/SKL. (Le magasin/rechargement commenté fonctionne aussi, mais SKL a un transfert de magasin plus rapide que HSW, et c'est variable-latency...)
# mov ah, bl version
5,009,785,393 cycles # 4.289 GHz ( +- 0.08% )
1,000,315,930 branches # 856.373 M/sec ( +- 0.00% )
11,001,728,338 instructions # 2.20 insn per cycle ( +- 0.00% )
12,003,003,708 uops_issued_any # 10275.807 M/sec ( +- 0.00% )
11,002,974,066 uops_executed_thread # 9419.678 M/sec ( +- 0.00% )
1,806 lsd_uops # 0.002 M/sec ( +- 3.88% )
1.168238322 seconds time elapsed ( +- 0.33% )
Notez qu'il ne fonctionne plus à partir du LSD.