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/ou mov ah, bl exécute à 4 par cycle. Il faut un uop ALU, donc il n'est pas éliminé comme mov 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-rupture xor eax,eax l'intérieur de la boucle supprime le goulot d'étranglement.)
  • Répété setz ah ou setc ah exécute à 1 par cycle. (Un dep-breaking xor eax,eax permet de goulot d'étranglement sur le débit p06 Pour setcc 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 que mov r8, r/m8 ne le fait pas (pour reg ou memory src)?{[98] }( et qu'en est-il de mov 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 occasionnel xor 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-breaking xor 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 + 6x mov 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 pour dl.
  • 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 si ah est "sale", mais le mov 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 cas same,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.

25
demandé sur Peter Cordes 2017-08-13 15:05:33

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 fait mov 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 un mov 32 bits ou un 8 bits movzx.
  • 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 que movzx 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 avec mov 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'ancien ah, mais le salit toujours. Je pense que mov 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 boucle rcr à la fin de ce post. Peut-être que tant que ah 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 dans rax. 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 8 inc al ralentie par les conflits de ressources des uops pour ah. Aussi le inc 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 seule setcc pour optimiser le cas setcc ah, car il est très rare que le code généré par le compilateur soit setcc 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, comme add/inc, pas div r8 ou mul 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] ou xor 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: test mov 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:

SKL150: les boucles courtes qui utilisent des registres AH / BH / CH / DH peuvent provoquer un système imprévisible Comportement

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

17
répondu Peter Cordes 2017-09-22 18:35:39