l'assembleur  [livre #4061]

par  pierre maurette


MMX
3DNow
SSE
...

Nous allons dans ce chapitre aborder un certain nombre de technologies additionnelles, dans le droit fil de celui traitant de la FPU. Nous trouverons, en effet, des points communs avec le coprocesseur arithmétique.

La grande différence réside dans le fait que, contrairement à la FPU, ces technologies sont réellement optionnelles. Enfin, elles étaient optionnelles lors de leur mise sur le marché. Mais les jeux SSE/SSE2 deviennent un standard. Seul manque SSE2 dans le haut de gamme AMD 32 bits, mais il est présent dans l'architecture AMD64, qui est aujourd'hui aux portes du marché grand public.

L'application devra toujours les tester, à moins d'être réellement temporaire et personnelle. Même dans ce cas, vous devrez insérer dans chaque application mettant en œuvre un jeu d'instructions particulier un bout de code testant sa présence, et surtout rappelant sa nécessité. Une MessageBox ne coûte pas grand-chose, et il est bon d'indiquer la raison pour laquelle le programme se ferme dès le lancement. Ce code doit faire partie de la boîte à outils ; un simple copier-coller suffit alors pour en bénéficier.

Deux approches au moins sont possibles quant au comportement du programme face à l'absence d'une technologie particulière : il peut continuer à effectuer son travail, mais avec un autre code et de façon moins performante, ou alors il peut afficher un message et se clore.

Dans cette dernière catégorie, nous pouvons trouver des jeux, tributaires d'une génération de processeurs et même de cartes graphiques. Dans un domaine plus sérieux, une entreprise va développer pour quelques postes une application de supervision vidéo de processus industriel. Il sera à ce moment-là rentable, en termes de coûts de développement, de fixer la plate-forme, par exemple et puisque nous sommes dans le domaine de la vidéo, à un Pentium 4 à 3 GHz. Le problème annexe est alors, si le programme accomplit proprement son travail, de trouver dans dix ans sur le marché des P4 3.0, avec les cartes mères, mémoires et disques afférents. Il est permis de penser que les seuls industriels qui apprécient la loi de Moore sont ceux du secteur informatique.

Si vous optez pour une identification fine du processeur hôte et une programmation optimisée, il faut savoir que vous multiplierez non seulement le travail, mais également les sources de bogues.

Après avoir regroupé dans un premier paragraphe, avec un petit programme, ce qui concerne l'identification, nous étudierons de façon assez fine les techniques MMX et 3DNow!. Le nombre d'instructions proposé permet en effet une certaine exhaustivité. De plus, tout le monde pourra tester, même sur des machines un peu anciennes. En même temps, vous affinerez votre façon d'aborder la documentation. Apprendre à apprendre.

La plupart des concepts fondamentaux auront ainsi été abordés. Nous terminerons par une simple évocation des technologies SSE et SSE2. Pour une foule de raisons :

  La place dans cet ouvrage est limitée.

  Le jeu d'instructions devient pléthorique : la publicité fait état de 144 instructions rajoutées par SSE2.

  Les machines prenant en charge ces jeux ne sont pas encore les plus nombreuses.

  Certaines notions, la gestion fine de la mémoire et des caches en particulier, dépassent le cadre de cet ouvrage.

Mais du code SSE sera proposé dans le chapitre suivant, abordant l'optimisation et dans lequel sera évoqué ce qui concerne l'alignement des données, en particulier celles de grande taille abordées dans les paragraphes qui suivent.

9.1 Identification

Voyons donc quelques blocs de code pour vérifier l'implémentation des jeux MMX, 3DNow!, SSE et SSE2. Nous présentons ce code sous la forme d'une validation de boutons dans un projet de test sous Delphi 6.

Deux réponses du programme de test
figure 9.01 Deux réponses du programme de test

Dans le premier écran, toutes les extensions sont reconnues, à l'exception de SSE2. Dans la seconde, l'instruction CPUID  ne semble pas présente. Le programme ne va pas plus loin dans ses recherches.

Si le processeur prend en charge une de ces technologies, il implémente en effet également l'instruction CPUID . C'est à la fois historique et logique, puisque c'est avec les 486, dernière génération, quand les processeurs ont commencé à proposer des options, que cette instruction est apparue. Nous l'utiliserons donc, elle est faite pour cela. Pour une programmation plus propre, nous testons simplement son existence sur la plate-forme, ce point ayant déjà été traité au chapitre sur la FPU.

function TFormID.DetectCPUID():Integer;register;
asm
  push ebx
  push edi
  mov  edi, self.StFondeur // CPUID supporté ?
  // Oui si le bit 21 de EFLAGS peut être modifié
  pushfd              // EFLAGS dans EAX
  pop eax
  mov ebx, eax        // EFLAGS initial dans EBX
  xor eax, 00200000h  // Inversion du bit 21
  push eax            // Actualisation (tentative) de EFLAGS
  popfd
  pushfd              // EFLAGS (nouveau) dans EAX
  pop eax
  cmp eax, ebx        // EFLAGS nouveau = EFLAGS initial ?
  je @CPU_pas_bon     // Si oui, pas bon
 
  xor eax, eax
  cpuid               // appel CPUID fonction 0
 
  // pour afficher le nom du fabricant
  mov [edi], ebx
  mov [edi + 4], edx
  mov [edi + 8], ecx
 
  @CPU_bon:
  mov eax, 1
  jmp @fin
 
  @CPU_pas_bon:
  xor eax, eax
  jmp @fin
 
  @fin:
  pop edi
  pop ebx
end;

Vous remarquez peut-être un semblant de structuration : un jmp @fin qui précède l'étiquette @fin: . Nous n'avons ici aucune contrainte de performance ; l'avantage est de faciliter la maintenance.

Les possibilités sont testées une par une, par des fonctions assembleur renvoyant un Boolean, alors que DetectCPUID() renvoyait un Integer, le nombre de fonctions CPUID accessibles. CPUID est invoquée avec un numéro de fonction dans EAX. La fonction 0 renvoie dans EAX un nombre, qui est la valeur maximale de EAX utilisable, donc le nombre de fonctions moins un. Ce nombre vaut 1 sur la plupart des processeurs, 2 sur le Pentium 4.

procedure TFormID.FormCreate(Sender: TObject);
var
  MaxInput: Integer;
begin
  StFondeur := '123456789ABC';
  MaxInput := DetectCPUID();
  if (MaxInput <> 0) then begin
    LblCPUID.Caption := '   CPUID OK' + #13#10
    +'(' + StFondeur + ')';
    LblCPUID.Visible := True;
    if DetectFPU()
      then BtnFPU.Enabled := True
      else BtnFPU.Enabled := False;
    if DetectCMOVcc()
      then BtnCMOVcc.Enabled := True
      else BtnCMOVcc.Enabled := False;
    if DetectMMX()
      then BtnMMX.Enabled := True
      else BtnMMX.Enabled := False;
    if StFondeur = 'AuthenticAMD' then begin
      if Detect3DNow() then begin
        Btn3DNow.Enabled := True;
        if Detect3DNowExt()
          then Btn3DNow.Caption := '3DNow! + Ext'
          else Btn3DNow.Caption := '3DNow!';
        end
      else Btn3DNow.Enabled := False;
    end
    else begin
      Btn3DNow.Enabled := False;
    end;
    if DetectSSE()
      then BtnSSE.Enabled := True
      else BtnSSE.Enabled := False;
    if DetectSSE2()
      then BtnSSE.Caption := 'SSE/SSE2'
      else BtnSSE.Caption := 'SSE';
    end
  else begin
    LblCPUID.Caption  := 'CPUID ABSENT';
    LblCPUID.Visible  := True;
    BtnFPU.Enabled    := false;
    BtnCMOVcc.Enabled := false;
    BtnMMX.Enabled    := false;
    Btn3DNow.Enabled  := false;
    BtnSSE.Enabled    := false;
    end;
end;

MaxInput n'est pas utilisé. Voici la détection de CPU :

function DetectFPU():Boolean;register;
asm
  push ebx
  xor eax, eax
  inc eax
  cpuid
  and dl, 00000001b;
  mov al, dl;
  pop ebx
end;

Ce code vient en droite ligne de la documentation Intel : CPUID fonction 1 renvoie dans EDX un certain nombre de bits signalant la disponibilité de telle ou telle possibilité :

  Le bit 0 signale la FPU.

  Le bit 15 les CMOVcc  et FCMOVcc si la FPU est présente.

  Le bit 23 signale MMX ; il existe un autre bit chez AMD mais celui-ci est général.

  Le bit 25 est pour SSE, le 26 pour SSE2.

Le résultat des fonctions est un Boolean, donc un Byte en Delphi. Cela explique les méthodes simplifiées, éventuellement malvenues dans ce contexte, comme pour CMOVcc  :

  cpuid
  and dx, 1000000000000000b;
  mov al, dh;

et la méthode générale, utilisée pour SSE, SSE2 et MMX :

  cpuid
  xor eax, eax
  and edx, 10000000000000000000000000b;
  jz @fin
  inc eax
  @fin:

Pour les technologies AMD, il est fait appel aux fonctions étendues de CPUID. Nous avons conditionné ces appels, et donc la recherche de 3DNow!, à la signature AuthenticAMD.

function Detect3DNow():Boolean;register;
asm
  push ebx
  //test 3DNow!
  mov  eax, 80000000h
  cpuid
  cmp  eax, 80000000h
  jbe  @pas3DNow
  mov  eax, 80000001h
  cpuid
  test edx, 80000000h
  jz   @pas3DNow
  mov eax, 1
  jmp @fin
  @pas3DNow:
  xor eax, eax
  jmp @fin
  @fin:
  pop ebx
end;
 
function Detect3DNowExt():Boolean;register;
asm
  push ebx
  //test 3DNow!
  mov  eax, 80000000h
  cpuid
  cmp  eax, 80000000h
  jbe  @pas3DNowExt
  mov  eax, 80000001h
  cpuid
  test edx, 40000000h
  jz   @pas3DNowExt
  mov eax, 1
  jmp @fin
  @pas3DNowExt:
  xor eax, eax
  jmp @fin
  @fin:
  pop ebx
end;

Nous testons d'abord la présence du jeu de fonctions CPUID étendues. Ensuite, par l'appel de la fonction 80000001h et son retour, nous testons les bits 31 (3DNow!) et 30 (extended 3DNow!).

Ce code peut, naturellement, être amélioré ; mais à quoi bon améliorer un code qui ne s’exécutera généralement qu'une fois, en début de programme ?

9.2 MMX

La technologie MMX (MultiMedia eXtended) développée par Intel est apparue avec le Pentium MMX. AMD intègre MMX et propose sur l’Athlon une extension du jeu d’instructions. Elle ne fait pas partie de la norme de fait ; son implémentation dans une version ultérieure, même chez Intel, n'est donc pas garantie. Il faudra par conséquent systématiquement interroger CPUID avant d'utiliser MMX.

MMX apporte de nouveaux registres, de nouveaux types de données et les nouvelles instructions qui vont avec. C’est donc une extension au modèle du programmeur.

Historiquement, c’est l’apparition du concept  SIMD  (Single-Instruction, Multiple-Data). Dans les applications orientées multimédia, qui travaillent sur de vastes tableaux de nombres (images 2D et 3D, sons), le goulet d’étranglement en termes de performances correspond souvent à l’exécution d’une même opération sur l’ensemble du tableau. MMX va faciliter le traitement simultané de plusieurs opérations, permettant d’accéder à un certain niveau de parallélisme. Ce parallélisme explicite vient ajouter ses bienfaits à celui utilisé par le processeur de façon transparente, pour améliorer ses performances générales.

9.2.1 Registres

MMX apporte 4 nouveaux types de données, tous sur 64 bits et 8 nouveaux registres MM0 à MM7. Nouveaux ?

Intel aime différencier les notions de registres physiques et logiques. Par exemple, EAX et AX sont présentés comme deux registres à part entière. À propos de MM0 à MM7, il est dit qu’ils sont aliased  aux registres de la FPU. Et encore, à ce niveau-là, il est fait référence aux R0 à R7, qui sont les registres physiques avant l’intervention de la notion de pile donnant naissance aux FP(i). Nous dirons donc, quant à nous, que MM0 à MM7 sont installés dans les 64 bits de la partie mantisse des registres R0 à R7 de la FPU.

Les registres MMX
figure 9.02 Les registres MMX [the .swf]

Première conséquence, c’est fromage XOR dessert. La FPU et MMX ne pourront pas être utilisés simultanément. Il sera judicieux de ne pas interrompre une session FPU par une session MMX, et vice versa. Ou alors, vous devrez utiliser les instructions de sauvegarde restauration du contexte FXSAVE/FSAVE/FNSAVE et FXRSTOR/FRSTOR .

Il n’y a pas de méthode spécifique pour entrer en mode MMX. Toute instruction MMX a cet effet :

  Chaque instruction MMX (autre que EMMS) écrit des 00b , donnée valide, dans le registre de TAG.

  En même temps, le TOS (Top Of Stack) dans le registre d’état est positionné à 000b , s’il ne l’était déjà.

  Quand une instruction MMX écrit dans un des 8 registres de données, les bits 64 à 79, correspondant pour la FPU au signe et à l’exposant, sont remplis par des 1.

L’instruction  EMMS  (Empty MMx State) ne doit absolument pas être oubliée pour terminer une session MMX et pouvoir utiliser à nouveau la FPU. Son effet principal est de marquer tous les registres comme vides (TAG à 11b ).

9.2.2 Types de données

Nous avons accès à 8 registres de 64 bits de largeur. Quatre types de données pourront y accéder.

Rappel : nous avons déjà rencontré des situations nous laissant le choix dans le stockage d’octets dans des mots plus grands, à propos des types BCD (CPU et FPU). En mode Compacté ou Packed, qui favorisait la capacité mémoire au détriment de la vitesse, plusieurs octets étaient stockés dans chaque word, dword, voire tword (80 bits). À l’inverse, au détriment de la capacité mémoire, mais favorisant la vitesse de traitement, le mode Unpacked ne stocke qu’un octet par mot, quelle que soit sa taille.

Les types de données MMX
figure 9.03 Les types de données MMX [the .swf]

En MMX, hormis bien sûr le quadword de 64 bits, les données sont compactées. C’est bien là le principe SIMD, qui veut que la même opération soit effectuée simultanément sur plusieurs données.

Les accès mémoire s’effectuent par blocs de 64 bits. Dans certaines circonstances, cet accès peut se faire en mode 32 bits. En revanche, toutes les instructions arithmétiques et logiques du jeu MMX s'appliquent sur chaque donnée, comme si elle était seule. Il faut que cette notion soit bien claire. Soit deux mémoires de 32 bits contenant respectivement 1004A46B et 0A20DFE3 .

Additionnés en tant que dwords, le résultat sera 1A25844E . Et il sera exact. Si maintenant nous appliquons une instruction packed ADD , qui additionne les words composant la mémoire individuellement, nous obtenons en sortie 1A24844E , c'est-à-dire 1A24 , qui est exact et 844E qui a perdu sa retenue en route. Nous verrons d'ailleurs au paragraphe suivant qu'il aurait peut-être été préférable de garder  FFFF pour cette dernière valeur.

Chaque donnée de base, byte, word, dword, etc., est évidemment implantée en mémoire selon la convention little endian. Qu'en est-il de la position relative des données d'un même paquet de 64 bits ? Assez logiquement, la donnée de plus faible indice est à l'adresse la plus basse. Et, encore une fois, la représentation de la mémoire à l'envers de sa progression habituelle (les adresses descendent de la gauche vers la droite) permet de mieux voir la logique sous-jacente.

Implantation des données en mémoire
figure 9.04 Implantation des données en mémoire [the .swf]

La conséquence de ces détails est importante : si nous avons un tableau en mémoire, qu'il soit de bytes, de words, de dwords ou de qwords, une seule instruction de transfert suffit.

9.2.3 Arithmétique par saturation

Quand nous avons abordé le sujet de la numération, nous avons pris l'exemple du compteur kilométrique pour illustrer le comportement d'un registre ou d'une mémoire en cas de dépassement de sa capacité. Il a été, de surcroît, signalé que ce comportement était le même, que nous interprétions la mémoire comme un entier signé ou non signé. Enfin, nous avions remarqué que, avec l'aide d'une paire de flags, tout cela était parfait pour faire du calcul.

Nous allons maintenant essayer de faire siffler notre ordinateur. Siffler au sens acoustique du terme bien entendu.

Distorsion de compteur kilométrique
figure 9.05 Distorsion de compteur kilométrique [the .swf]

Nous avons représenté, en haut à gauche, le signal qu'un module de calcul doit nous permettre d'obtenir. À droite, les échantillons théoriques, c'est-à-dire les valeurs que nous devrions générer, sur 16 bits, comme dans tout bon CD Audio. Malheureusement, notre module de calcul est tel qu'il dépasse les capacités des 16 bits. En bas à gauche est donc représenté ce que le module va sortir, en arithmétique compteur kilométrique ; et à droite, ce que notre oreille en pensera. À ce niveau, ce n'est plus de la distorsion, c'est de la violence.

Quand la FPU n'en peut plus, que le résultat théorique est plus grand que son plus grand nombre positif représentable, elle va certes générer éventuellement une exception, mais elle retournera plus l'infini, pas un grand nombre négatif.

Nous allons procéder à peu près à l’identique avec les entiers MMX. C'est l'arithmétique de saturation (rien à voir avec la plongée profonde). Le résultat est extrêmement satisfaisant pour les oreilles.

La saturation
figure 9.06 La saturation [the .swf]

Nous avons réalisé un écrêtage, ou clipping. Nous aurions pu prendre comme exemple un filtre d'image qui, voulant obscurcir globalement un cliché, va préférer laisser toutes les zones trop sombres au noir 0 plutôt que de les illuminer soudainement.

Bien entendu, ce traitement est rudimentaire : il sera préférable de faire intervenir une compression du signal un peu avant la saturation, que ce soit en audio, par le soft clipping par exemple, ou en image, par les courbes de gamma. À noter que les pellicules photographiques ont un très bon comportement à cet égard.

Ainsi, MMX ne va pas se préoccuper de dépassement, donc flags CF ou OF. Ce qui est heureux, puisqu'en mode Packed bytes, il aurait fallu gérer 8 de chacun de ces flags. D'une façon plus générale, en mode Saturation ou non, MMX se comporte plus comme un outil multimédia que comme une machine à calculer : les instructions ne positionnent pas de flags et aucune exception numérique n'est déclenchée.

Le mode que nous appelons Compteur kilométrique est pour Intel le mode Portefeuille, wrap-around, qui évoque le papier d'emballage autour d'un objet.

9.2.4 Jeu d'instructions

EMMS a déjà été vue ; ce qui va suivre ne s'applique généralement pas à cette instruction. La structure d'une instruction est comme d'habitude INST DEST, SRC.

Les instructions sont peu nombreuses, mais largement préfixées et suffixées. Un peu de logique permettra pratiquement de mémoriser le jeu d'instructions complet. Un exemple : dans le cas des opérations logiques bit à bit, il est inutile de préciser le type de données, l'instruction est considérée de type qword ou non compacté.

Toutes les instructions débutent par la lettre P. Pour le reste, voici un petit guide de décryptage. Dans l'ordre où elles apparaissent généralement après le mnémonique principal, voici la signification des lettres :

  US : mode saturation unsigned, non signé.

  S : mode saturation signé, le mode wrap-around par défaut n'étant pas indiqué.

  L : low ou bas, H : high ou haut.

  B : byte, W : word, D : dword et Q : qword, quand il est utile de préciser la taille de la donnée compactée.

Sauf indication contraire, les instructions décrites ci-après agissent entre deux registres MMX, ou entre un registre MMX et une mémoire 64 bits. Cela est vrai que ce soit en B, W, DW ou Q : c'est le concept SIMD. À cause de la multiplicité des opérations, les opérations MMX ne positionnent généralement pas de flag.

Transferts

  MOVQ  : déplacement sur 64 bits.

Le bloc mémoire est donné par l'adresse de son octet le plus faible.

  MOVD  : un déplacement de seulement 32 bits. Seule la partie basse des registres MMX est accessible par cette instruction, en lecture comme en écriture. Un des opérandes est un registre MMX, l'autre une mémoire ou un registre général 32 bits. Donc, pas de transfert mémoire/mémoire ni registre MMX/registre MMX.

Arithmétique

  PADDB , PADDW , PADDD   : effectuent l'équivalent de 8, 4 et 2 opérations ADD simultanées sur 8, 16 et 32 bits. Comme ADD, ces instructions ne différencient pas signé et non signé. À l'inverse de ADD, aucun flag n'est positionné.

  PADDSB , PADDSW  : effectuent 8 et 4 additions simultanées signées, sur 8 et 16 bits, avec saturation en cas de dépassement. Les valeurs de saturation sont respectivement -128/+127 et -32768/+32767. Aucun flag n'est positionné.

  PADDUSB , PADDUSW  : effectuent 8 et 4 additions simultanées non signées, sur 8 et 16 bits, avec saturation en cas de dépassement, nécessairement par excès. Les valeurs de saturation sont respectivement 255 et 65535. Aucun flag n'est positionné.

  PSUBB , PSUBW , PSUBD  : effectuent l'équivalent de 8, 4 et 2 opérations SUB simultanées sur 8, 16 et 32 bits. Comme SUB et ADD, ces instructions ne différencient pas signé et non signé. À l'inverse de SUB, aucun flag n'est positionné.

  PSUBSB , PSUBSW  : effectuent 8 et 4 soustractions DEST - SRC simultanées signées, sur 8 et 16 bits, avec saturation en cas de dépassement. Les valeurs de saturation sont respectivement -128/+127 et -32768/+32767. Aucun flag n'est positionné.

  PSUBUSB , PSUBUSW  : effectuent 8 et 4 soustractions DEST - SRC simultanées non signées, sur 8 et 16 bits, avec saturation en cas de dépassement, nécessairement par défaut. Les valeurs de saturation sont 0 dans les deux cas. Aucun flag n'est positionné.

  PMULL , PMULH  (ou PMULLW  et PMULHW , autres mnémoniques) : la destination est nécessairement un registre MMX. Ces deux instructions effectuent 4 multiplications signées sur 16 bits pour obtenir 4 résultats intermédiaires sur 32 bits. Dans le cas de PMULL(W) , les 16 bits de poids faible sont stockés dans le registre MMX de destination. Dans le cas de PMULH(W) , les 16 bits de poids fort sont stockés dans le registre MMX de destination.

  PMADD  (ou PMADDWD , autre mnémonique) : Multiply and Add. La destination est nécessairement un registre MMX. L'histoire commence comme pour PMULL ou PMULH . Une fois obtenus les 4 résultats intermédiaires, ils sont additionnés par 2 groupes de 2 et stockés dans le registre MMX de destination. Ce qui se résume en :

dest_dword0 = (dest_word0 * src_word0)  + (dest_word1 * src_word1)
dest_dword1 = (dest_word2 * src_word2)  + (dest_word3 * src_word3)
Comparaisons

La destination est nécessairement un registre MMX.

  PCMPEQB , PCMPEQW , PCMPEQD  : comparent des données pour l'égalité. Les données de 8, 16 ou 32 bits sont comparées deux à deux. En cas d'égalité, la destination est remplacée par FFh , FFFFh ou FFFFFFFFh (tous les bits à 1). Dans le cas contraire, elle est remplacée par des 0.

  PCMPGTPB , PCMPGTPW , PCMPGTPD  : comparent pour l'inégalité. Même chose que le précédent, la condition d'égalité étant remplacée par DEST > SRC.

Logique bit à bit

La destination est nécessairement un registre MMX.

  PAND , PANDN , POR , PXOR  : sont simplement les opérations  AND , OR et XOR bit à bit, appliquées à l'ensemble des 64 bits. PANDN correspond à AND NOT , la destination est inversée et ensuite le AND est effectué.

Décalages

La destination est nécessairement un registre MMX. La source, un registre MMX, une mémoire 64 bits ou encore une valeur immédiate sur 8 bits.

  PSLLW , PSLLD , PSLLQ  : décalage logique vers la gauche du nombre de bits spécifié par la source. Des 0 entrent par la droite. Les bits tombés à gauche sont tous perdus.

  PSRLW , PSRLD , PSRLQ  : l'inverse du précédent, décalage logique vers la droite, des 0 sont entrés par la gauche ; ce qui tombe à droite est perdu.

  PSRAW , PSRAD  : décalage arithmétique vers la droite. Les bits tombés à droite sont toujours perdus ; mais, à gauche, est entrée la valeur initiale du bit le plus à gauche (bit de signe).

Revoir éventuellement les instructions de décalage du jeu d'instructions normal, au chapitre intitulé Le jeu d'instructions .

Conversions

La destination est nécessairement un registre MMX. La source, un registre MMX, une mémoire 64 bits, ou encore, dans le cas des 3 versions L de PUNCK , une mémoire 32 bits.

  PUNPCKHBW , PUNPCKHWD , PUNPCKHDQ , PUNPCKLBW , PUNPCKLWD , PUNPCKLDQ  : ces instructions prennent, dans la source et dans la destination, les données par blocs d'une certaine taille et fabriquent des données de taille double, par entrelacement.

Comme il suffit de 32 bits pour fabriquer les 64 bits de la destination, il y a deux versions, L (low) et H (high).

Les données peuvent être des bytes qui deviennent de words, des words qui deviennent des dwords et enfin des dwords qui deviennent des qwords, ce qui explique les terminaisons BW, WD et DQ.

Ces instructions étant un peu obscures, nous avons codé :

procedure TForm1.Button1Click(Sender: TObject);
var
  I0, I1: int64;
begin
I0 := $0011223344556677;
I1 := $8899AABBCCDDEEFF;
asm
  movq mm0, I0
  movq mm1, I1
  PUNPCKLBW mm0, mm1 // CC44 DD55 EE66 FF77
  PUNPCKHBW mm0, mm1 // 8800 9911 AA22 BB33
  PUNPCKLWD mm0, mm1 // CCDD4455 EEFF6677
  PUNPCKHWD mm0, mm1 // 88990011 AABB2233
  PUNPCKLDQ mm0, mm1 // CCDDEEFF44556677
  PUNPCKHDQ mm0, mm1 // 8899AABB00112233
  movq I0, mm0
  emms
end; //asm
Memo1.Lines.Add(IntToHex(I0,16));
end;

Bien entendu, les six lignes de test sont à essayer une par une, à l'aide de mises en commentaires. Nous avons noté en face de chaque instruction le résultat obtenu. Nous avons la faiblesse de croire que ce résultat, allié au décryptage des mnémoniques, vaut mieux qu'une explication.

L'exemple est mal choisi sur un point : aucun des mots bas n'a un MSB à 1. Mais nous pouvons y remédier en inversant  I0 et  I1 et vérifier que malheureusement le bit de signe ne se propage pas.

C'est une méthode séduisante pour prendre contact avec des instructions ; abusons donc. Les trois instructions PACKxxxx font fondamentalement l'inverse des PUNPCKxxx , c'est-à-dire que les données d'une certaine taille sont réduites à la taille moitié. Les deux opérandes 64 bits sont donc utilisés entièrement pour construire la destination 64 bits. Elles fonctionnent en saturation et différencient par conséquent les données signées et non signées.

Nous avons continué à coder sur le même programme (voir le CD-Rom). Nous rapportons simplement les données en entrée, la fonction et le résultat.

  PACKUSWB  : non signé, word vers byte (16 vers 8).

I0 := $0011002200330044;
I1 := $0055006600770088;
PACKUSWB mm0, mm1
5566778811223344

Aucune saturation sur ce premier essai ; nous découvrons simplement le mécanisme de l'instruction. Tentons maintenant de provoquer la saturation :

I0 := $0011002201330044;
I1 := $0055006612770088;
PACKUSWB mm0, mm1
5566FF881122FF44

La saturation ( FFh ou 255) survient bien dès que le word fort n'est pas nul.

  PACKSSWB  : source et destination considérées comme signées, toujours de 156 vers 8. Rappelons que les seuils de saturation sous 8 bits signés sont 7Fh (127) et 80h (-128), qui fait FF80h en 16 bits.

I0 := $0011002200330044;
I1 := $0055006600770088;
PACKSSWB mm0, mm1
5566777F11223344

Le seul cas de saturation est 0088h , valeur positive égale à 136. Il sature bien à 7Fh (+127). Affinons un peu sur les seuils de saturation :

I0 := $FF81FF7E00330044;
I1 := $FF80FF7F007F0080;
PACKSSWB mm0, mm1
80807F7F81803344

Les résultats s'interprètent avec un peu de concentration quant à la représentation des négatifs.

  PACKSSDW  : source et destination toujours signés, mais conversion de dword vers word (32 vers 16).

I0 := $F000000000004567;
I1 := $000089AB0000CDEF;
PACKSSDW mm0, mm1
7FFF7FFF80004567

Les saturations se font bien à 7FFFh (32767) et 8000h (-32768).

Nous venons de tester les instructions certainement les plus difficiles. Les autres sont peut-être plus simples à utiliser que les instructions arithmétiques standard. Comme nous sommes curieux, mettons en commentaire l'instruction  EMMS . Rien ne se passe. Maintenant, déclarons fp0 et fp1 comme Single et ajoutons avant et après le bloc asm  :

fp0 := 2.0;
fp1 := pi;
asm
..
..
end; //asm
fp0 := fp1 * fp0;
Memo1.Lines.Add(FloatToStr(fp0));

Le programme se bloque alors si, et seulement si, nous enlevons EMMS, avec le message EInvalidOp , opération en virgule flottante incorrecte.

Remarque

MMX – Extensions AMD

Nous verrons au paragraphe suivant que la technologie 3DNow! d'origine AMD est essentiellement une évolution de MMX par ajout d'un type de données packed float. Quand AMD a proposé, à l'occasion de la sortie de l'Athlon, une extension du jeu d'instructions multimédias, il a présenté les ajouts concernant le type flottant comme une extension 3DNow! et celles ne concernant que les quatre types d'entiers compactés comme un jeu d'extensions MMX. Nous en ferons une présentation groupée un peu plus loin. Toutefois, les extensions MMX ne sont pas liées à 3DNow! ni à AMD.

9.3 3DNow!

Implémentée semble-t-il uniquement sur des processeurs d'origine AMD, depuis le K6-2, la technologie 3DNow! fut développée en grande partie par ce fondeur, au sein d'une équipe comportant des développeurs et des fabricants de périphériques graphiques. C'est de cette façon qu'AMD présente les choses. Nous dirons, quant à nous, que 3DNow! est une technologie AMD.

Une cible de 3DNow! est le graphisme en trois dimensions. Sous le terme 3D, se cache en fait la représentation en deux dimensions d'une réalité, imaginée ou existante, en volume, c'est-à-dire la reconstitution de scènes texturées et éclairées (raytracing) mais, qui plus est, animées, donc recalculées en temps réel. Cela demande des calculs sur des réels très nombreux. Même si ces techniques ont plusieurs applications possibles, c’est commercialement le domaine du jeu qui est visé. Pour obtenir une amélioration de performances à partir d'une technologie comme 3DNow!, le point clé n'est pas l'écriture du jeu, mais la disponibilité de drivers de carte graphique supportant le nouveau jeu d'instructions. À ce moment-là, l'amélioration globale des performances se fera indépendamment du logiciel ou du système d'exploitation en vigueur.

Les techniques de compression vidéo sont devenues incontournables, y compris dans le domaine professionnel (broadcast). Les procédés utilisés par le grand public et la télévision (au moins au niveau des émissions d'actualités) sont des procédés destructifs, comme les MPEG, c'est-à-dire qu'il y a perte d'information (dans un ratio hallucinant parfois), nos yeux étant supposés se satisfaire du reliquat.

Un film est une succession d'images fixes. En fait, une image extraite d'un film peut être moins bonne qu'une photographie ; elle sera même meilleure si elle est floue dans le sens du mouvement, filée en quelque sorte. Nous pouvons penser que les procédés de compression applicables à la photographie (JPEG) sont applicables, dans un premier temps, à l'image film. Mais interviennent ensuite les compressions liées à l'animation.

Le principe de ces algorithmes de compression est d'envoyer une image complète toutes les 6 images (c'est un exemple). Entre deux images, sont insérées des images incomplètes et un important travail d'interpolation doit être effectué. Ces procédés ont des limites, bien visibles sur nos petits (plus ils le sont, mieux c'est) écrans. Voir une scène mobile sur fond de feuillages bruissant dans le vent. En revanche, une copie DivX d'un film d'animation sera très satisfaisante (hors éthique), puisque d'une part les images fixes sont simples, et d'autre part des interpolations numériques de mouvement ont été utilisées à la fabrication. 3DNow! va intégrer des instructions facilitant ces interpolations, les xAVGxx effectuant des moyennes en particulier.

3DNow! est en quelque sorte une extension virgule flottante de MMX. L'existence de la technologie 3DNow! implique nécessairement celle de MMX. Il n'y a pas à changer de mode pour effectuer des calculs sur entiers et sur flottants, ou plus exactement il n'y a qu'un seul mode : MMX/3DNow!. Pour résumer, 3DNow! est un MMX amélioré sur les points suivants :

  Un nouveau type de donnée, deux flottants compactés, packed single (précision float), dans les registres MMX.

  La prise en compte de la structure pipeline, qui permet d'effectuer en un cycle d'horloge deux instructions. Ces instructions étant elles-mêmes de type SIMD ; ce sont quatre opérations simultanées sur des flottants qui seront effectuées.

  21 nouvelles instructions.

  Parmi ces instructions, FEMMS qui permet de basculer entre les modes FPU et MMX/3DNow! beaucoup plus rapidement.

Nous allons donc présenter 3DNow! comme une extension à MMX, ce qui est quelque peu artificiel.

9.3.1 Registres et types de données

Pour les registres, aliasés sur ceux de la FPU, il n'y a strictement rien à ajouter à ce qui a été présenté à propos de MMX. Un type de donnée supplémentaire peut être hébergé dans ces registres, ce qui donne le récapitulatif suivant.

Les types de données MMX/3DNow!
figure 9.07 Les types de données MMX/3DNow! [the .swf]

Les deux réels simple précision qui, compactés, composent le nouveau type, sont conformes à la norme IEEE 754 ; donc, au format mémoire de la FPU, puisque nous avons vu que celle-ci traitait en interne ce type comme un extended. Nous rappelons ce format, 1 bit de signe, 8 bits d'exposant décalé (biased) de 127, 23 bits de fraction complétés par un 1 implicite pour former une mantisse 24 bits.

Ce type de donnée réelle n'est pas orienté calcul scientifique (précision et reproductibilité des résultats), mais multimédia (rapidité). Cela explique un certain flou dans la norme. Quoi qu'il en soit, cette norme est peu intéressante ; c'est l'implémentation qu'en a fait AMD qui compte. Par exemple, le choix du mode d'arrondi automatique n'est pas proposé ; c'est soit au plus proche, comme chez AMD, soit vers 0 (par troncation). Il en est de même pour la valeur de saturation, qui peut être soit la plus grande (plus petite) valeur possible, soit plus (moins) l'infini.

Pas de support des infinis et des NaN en tant qu'opérandes. Pas de flag positionné, pas d'exception numérique : il faut voir 3DNow! (et MMX) comme un instrument de musique, en cas de fausse note, le mieux est de passer à la suivante.

9.3.2 Jeu d'instructions

La documentation AMD fait état de deux unités de calcul, les registres X et Y, qui possèdent certaines ressources en propre et en partagent d'autres. Le but est de traiter deux instructions simultanément. Cette spécificité des ressources partage les instructions en deux familles :

  Catégorie 1 : PFADD , PFSUB , PFSUBR , PFACC , PFCMPx , PFMIN , PFMAX , PI2FD , PF2ID , PFRCP , PFRSQRT .

  Catégorie 2 : PFMUL , PFRCPIT1 , PFRSQIT1 , PFRCPIT2 .

Deux instructions pourront démarrer leur exécution simultanée sans délai si elles n'appartiennent pas à la même famille. Nous n'irons pas plus loin dans cette voie, puisque pour écrire du code optimisé il faudra travailler avec la documentation sous les yeux. Cette documentation en anglais est disponible en ligne ; consultez le CD-Rom pour des fichiers et des liens. Pour les mêmes raisons, nous ne détaillerons pas les instructions de préchargement (prefetch) PREFETCH  et PREFETCHW , dont le comportement dépend du type exact de processeur.

Attention, toutes les instructions vues dans la rubrique MMX sont disponibles, et même indispensables, en MMX/3DNow!.

Pour tous les tests et explications, sauf indications contraires, les opérations se font entre mm0 (destination) et mm1 (source) : OPER mm0, mm1 . Le squelette de code est le suivant :

procedure TForm1.Button2Click(Sender: TObject);
var
  F0: Array[0..1] of Single;
  F1: Array[0..1] of Single;
begin
  F0[0] := 1.1;
  F0[1] := 4.4;
  F1[0] := 1.0;
  F1[1] := 4.0;
  asm
  movq mm0, F0
  movq mm1, F1
  // Instructions à tester
  movq F0, mm0
  movq F1, mm1
  femms
  end; //asm
Memo1.Lines.Add(FloatToStr(F0[0]));
Memo1.Lines.Add(FloatToStr(F0[1]));
end;

La notation utilisée pour les données compactées sera de la forme mm00 , mm01 , mm10 et mm11 , à la signification évidente.

  FEMMS  (Fast EMMS) : version rapide de EMMS. Elle serait plus rapide, car ne préservant pas les registres de données. La récupération des données MMX en mode FPU et vice versa est une opération certainement rare. Nous avons vérifié, sur le code ci-dessus et sur celui accompagnant le chapitre MMX, que FEMMS avait le même effet que  EMMS  : éviter le blocage. En l'absence d'instruction de sortie de mode, ce blocage survient par exemple sur la fonction FloatToStr() .

  PFADD  : addition sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

mm00 = mm00 + mm10
mm01 = mm01 + mm11

  PFACC  : accumulation sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

mm00 = mm00 + mm01
mm01 = mm10 + mm11

  PFSUB  et PFSUBR  : soustraction et soustraction inverse sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

PFSUB fait DEST - SRC dans DEST :

mm00=mm00-mm10 et mm01=mm01-mm11

PFSUBR fait SRC - DEST dans DEST :

mm00=mm10-mm00 et mm01=mm11-mm01

  PFMUL  : multiplication sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

mm00 = mm00 * mm10
mm01 = mm01 * mm11

  PFCMPEQ  : compare pour l'égalité. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Chaque mot de 32 bits de la destination est rempli de 1 en cas d'égalité source destination, de 0 dans le cas contraire.

si mm00 = mm10, alors mm00 = FFFFFFFFh. Sinon, mm00 = 00000000f.
si mm01 = mm11, alors mm01 = FFFFFFFFh. Sinon, mm01 = 00000000f.

  PFCMPGE  : compare pour l'inégalité. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Chaque mot de 32 bits de la destination est rempli de 1 si la destination est supérieure ou égale à la source, de 0 dans le cas contraire.

si mm00 >= mm10, alors mm00 = FFFFFFFFh. Sinon, mm00 = 00000000f.
si mm01 >= mm11, alors mm01 = FFFFFFFFh. Sinon, mm01 = 00000000f.

  PFCMPGT  : compare pour l'inégalité. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Chaque mot de 32 bits de la destination est rempli de 1 si la destination est strictement supérieure à la source, de 0 dans le cas contraire.

si mm00 > mm10, alors mm00 = FFFFFFFFh. Sinon, mm00 = 00000000f.
si mm01 > mm11, alors mm01 = FFFFFFFFh. Sinon, mm01 = 00000000f.

  PFMAX  : trouve le maximum. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Pour chacun des deux réels, la destination reçoit les plus grands de la source et de la destination.

si mm00 >= mm10, alors mm00 = mm00. Sinon, mm00 = mm10.
si mm01 >= mm11, alors mm01 = mm01. Sinon, mm01 = mm11.

  PFMIN  : trouve le minimum. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Pour chacun des deux réels, la destination reçoit les plus petits de la source et de la destination.

si mm00 <= mm10, alors mm00 = mm00. Sinon, mm00 = mm10.
si mm01 <= mm11, alors mm01 = mm01. Sinon, mm01 = mm11.

  PMULHRW  : une réécriture de PMULH (ou PMULHW ) qui améliore, dans certains cas, la précision du résultat, qui n'est plus obtenu par troncation mais par arrondi. Ce qui est obtenu en ajoutant 8000h au résultat intermédiaire.

  PAVGUSB  : moyennes arrondies sur 8 bits non signés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits, tous deux considérés comme des octets non signés compactés. Pour chaque position, les deux octets sont additionnés, puis 1 est ajouté au résultat intermédiaire, et enfin celui-ci est divisé par 2 par décalage à droite.

mm00 = shr((mm00 + mm10 + 1), 1)
mm01 = shr((mm01 + mm11 + 1), 1)

..

mm07 = shr((mm07 + mm17 + 1), 1)

  PI2FD  : conversion d'une paire d'entiers signés 32 bits compactés en une paire de réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. La conversion se fait de SRC vers DEST, avec perte éventuelle de précision par troncation.

  PF2ID  : conversion d'une paire de réels compactés en une paire d'entiers signés 32 bits compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. La conversion se fait de SRC vers DEST, par troncation.

  PFRCP  : inversion rapide d'un réel. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Malgré la précision de 24 bits des opérandes, le résultat n'est obtenu qu'avec une précision de 14 bits. Le contenu de mm0 est indifférent et perdu. Le résultat se retrouve dupliqué dans mm00 et mm01 . mm11 est indifférent. Ce comportement étrange n'est pas une erreur (vérifier à partir de squelette, en profiter pour tester 1/0.0 = 3,40282346638529E38 sur un Athlon XP).

mm00 = mm01 = 1 / mm10

Cette instruction est le premier élément d'un dispositif plus complet.

  PFRSQRT   : racine carrée inverse rapide d'un réel. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Malgré la précision de 24 bits des opérandes, le résultat n'est obtenu qu'avec une précision de 15 bits. Le contenu de mm0 est indifférent et perdu. Le résultat se retrouve dupliqué dans mm00 et mm01 . mm11 est indifférent. Ce comportement étrange n'est pas une erreur (vérifier, à partir de squelette, 1/racine(16.0) = 0,249996185302734 sur un Athlon XP).

mm00 = mm01 = 1 / racine(mm10) ou mm00 = mm01 = racine (1 / mm10)

Cette instruction est le premier élément d'un dispositif plus complet.

  PFRCPIT1 , PFRCPIT2  et PFRSQIT1  : n'ont pas de sens utilisées seules. Elles font partie d'une stratégie globale et ne signifient quelque chose que combinées à PFRCP ou PFRSQRT (le  IT est pour mis itération). La documentation évoque la méthode de Newton-Raphson. Les mathématiciens (pas ceux de la numération, ceux des nombres réels, les analystes) vous parleraient de méthodes géométriques itératives pour approcher les racines d'une équation non résolue par l'algèbre. La méthode en question fait appel aux tangentes à la courbe pour, dans certaines circonstances favorables quant aux dérivées, converger très rapidement vers un bon résultat.

Approche graphique d'une racine d'équation
figure 9.08 Approche graphique d'une racine d'équation [the .swf]

Nous allons coder des exemples inspirés directement de la note d'AMD sur la division et l'extraction de racine carrée, ce qui éclairera ces dernières instructions un peu en kit et constituera un bon exercice.

D'abord, une division mm00/mm10 en faible précision :

procedure TForm1.Button2Click(Sender: TObject);
var
  F0: Array[0..1] of Single;
  F1: Array[0..1] of Single;
  fp0, fp1: Extended;
begin
  F0[0] := 1.2;   // b
  F0[1] := 0.0;
  F1[0] := 374.9472; // a (= 312.456 * (-1.2));
  F1[1] := 0.0;
  fp0 := 1.2;
  fp1 := 374.9472;
  asm
    movq mm0, F0
    movq mm1, F1
    pfrcp mm0, mm0   // 1/b
    pfmul mm1, mm0   // a * (1/b) = a/b
    movq F0, mm0
    movq F1, mm1
    femms
  end; //asm
  fp1 := fp1 / fp0;
  Memo1.Lines.Add(FloatToStr(fp1));
  Memo1.Lines.Add(FloatToStr(F1[0]));
end;

Le résultat est calamiteux :

312,456
312,456970214844

Utilisons le kit d'amélioration de la précision. Pour cela, l'instruction pfrcp mm0, mm0 est remplacée par un bloc :

    //pfrcp mm0, mm0   // 1/b
    pfrcp     mm2, mm0
    punpckldq mm0, mm0
    pfrcpit1  mm0, mm2
    pfrcpit2  mm0, mm2
 
    pfmul mm1, mm0   // a * (1/b) = a/b

Le résultat est bien meilleur :

312,456
312,455993652344

Le résultat est faux par défaut, mais l'écart est inférieur à 0,000007, alors que dans le cas précédent, il était de l'ordre de 0,001, ce qui est 150 fois meilleur. Ces résultats, rappelons-le, peuvent dépendre du processeur utilisé.

Pour le calcul de racine carrée de A, nous avons (à peine) modifié le code AMD, puisque nous désirions faire A = racine(A) et non B = racine(A).

procedure TForm1.Button2Click(Sender: TObject);
var
  F0: Array[0..1] of Single;
  fp0, fp1: Extended;
begin
  F0[0] := 25;   // a
  F0[1] := 0.0;
  fp0 := 25.0;
  asm
    movq      mm0, F0
    pfrsqrt   mm1, mm0
    punpckldq mm0, mm0
    pfmul     mm0, mm1
    movq F0,  mm0
    femms
  end; //asm
  fp0 := sqrt(fp0);
  Memo1.Lines.Add(FloatToStr(fp0));
  Memo1.Lines.Add(FloatToStr(F0[0]));
end;

Pour expliquer le code, il faut faire appel à nos amis mathématiciens une fois de plus : pourquoi multiplier A par l'inverse de sa racine, c'est en effet ce qui est fait à la ligne pfmul  mm0, mm1 . Il suffit de remarquer que A = racine(A) * racine(A) pour en déduire que A * (1 / racine(A)) = racine(A) . Les résultats sont :

5
4,99997138977051

Nous avons vu pire.

Récrivons le bloc asm en utilisant le kit :

  asm
    movq      mm0, F0
    pfrsqrt   mm1, mm0
    movq      mm2, mm1
    pfmul     mm1, mm1
    punpckldq mm0, mm0
    pfrsqit1  mm1, mm0
    pfrcpit2  mm1, mm2
    pfmul     mm0, mm1
    movq F0,  mm0
    femms
  end; //asm

Résultats :

5
5

Bien bien.

Il est, en première approche, inutile de maîtriser ces séquences instruction par instruction. Ces deux méthodes peuvent être appliquées telles quelles, par clonage. Elles peuvent même éventuellement faire l'objet d'une macro si l'environnement le permet.

9.3.3 Extensions aux jeux d'instructions MMX/3DNow!

Les cinq premières instructions sont de petites améliorations du mode 3DNow!, c'est-à-dire jouant sur des réels.

  PSWAPD  : iIntervertit les deux mots de 32 bits de la source et place le tout dans la destination. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

  PI2FW  : conversion d'une paire d'entiers signés 16 bits compactés en une paire de réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Les zones 0..15 et 32..47 sont considérées comme entiers, le reste est ignoré.

  PF2IW   : conversion d'une paire de réels compactés en une paire d'entiers signés 32 bits compactés. Cette version est limitée à 16 bits, donc saturée en 32 bits à FFFF8000h et 00007FFFh . La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. La conversion se fait de SRC vers DEST, par troncation.

  PFNACC  : accumulation par soustraction sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

mm00 = mm00 - mm01
mm01 = mm10 - mm11

  PFPNACC  : accumulation mixte sur deux fois deux réels compactés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits.

mm00 = mm00 - mm01
mm01 = mm10 + mm11

 

La suite ne concerne pas les réels, c'est une extension au jeu MMX.

Nous traiterons les instructions PREFETCHNTA , PREFETCHT0 , PREFETCHT1 , PREFETCHT2 , auxquelles nous ajoutons SFENCE  comme PREFETCH et PREFETCHW , c'est-à-dire en renvoyant à la documentation.

  MASKMOVQ  : cette instruction réalise un transfert rapide (streaming, en passant outre le cache) de données à travers un masque dont la résolution est le byte. Elle prend deux registres MMX en opérande. Le premier est la source du transfert, le second, le masque. Et la destination ? Elle est implicitement en mémoire, pointée par EDI. Si nous épluchons la documentation AMD pour savoir comment se comporte le masque, nous trouvons :

memory[edi][63:56] = mmreg2[63] ? mmreg1[63:56] : memory[edi][63:56]
memory[edi][55:48] = mmreg2[55] ? mmreg1[55:48] : memory[edi][55:48]
memory[edi][47:40] = mmreg2[47] ? mmreg1[47:40] : memory[edi][47:40]
memory[edi][39:32] = mmreg2[39] ? mmreg1[39:32] : memory[edi][39:32]
memory[edi][31:24] = mmreg2[31] ? mmreg1[31:24] : memory[edi][31:24]
memory[edi][23:16] = mmreg2[23] ? mmreg1[23:16] : memory[edi][23:16]
memory[edi][15:8]  = mmreg2[15] ? mmreg1[15:8]  : memory[edi][15:8]
memory[edi][7:0]   = mmreg2[7]  ? mmreg1[7:0]   : memory[edi][7:0]

Ceux qui savent reconnaîtront la syntaxe d'une macro utilisée en C/C++ :

(condition logique) ? A : B

Cette expression vaut A si condition logique est vraie, sinon B. À l'aune de cela, nous déduisons que les octets seront transférés si le MSB de l'octet correspondant du masque est à 1. Sinon, l'octet de la destination reste inchangé, puisqu'il est égal à lui-même.

Les autres, ceux pour qui cette notation est mystérieuse peuvent lancer leur Delphi et tâtonner. C'est encore une fois un excellent réflexe.

var
  i0, i1, i2: int64;
begin
  i0 := $A000FF0000F00000;
  i1 := $807FA05040302010;
  i2 := $1111111111111111;
  Memo1.Lines.Add(IntToHex(i2, 16));
  asm
    lea      edi, i2
    movq     mm0, i0
    movq     mm1, i1
    maskmovq mm0, mm1
    movq i0, mm0
    movq i1, mm1
  end; //asm
  Memo1.Lines.Add(IntToHex(i0, 16));
  Memo1.Lines.Add(IntToHex(i1, 16));
  Memo1.Lines.Add(IntToHex(i2, 16));
end;

Ce qui donne les résultats :

11 11 11 11 11 11 11 11
A0 00 FF 00 00 F0 00 00
80
 7F 
A0
 50 40 30 20 10
A0 11 FF 11 11 11 11 11

Nous avons un peu triché par rapport à la sortie écran réelle, en séparant les octets. Il suffit ensuite de bien identifier, dans la troisième ligne, le masque, les octets dont le MSB est à 1, négatifs s'ils sont signés.

  MOVNTQ  : comme la précédente, une instruction de transfert rapide, cette fois-ci d'un registre MMX source vers une mémoire 64 bits destination. Comme disait Napoléon, un bon dessin...

begin
  i0 := $3333222211110000;
  i1 := $0000000000000000;
  asm
    movq     mm0, i0
    movntq i1, mm0
    movq i0, mm0
  end; //asm
  Memo1.Lines.Add(IntToHex(i1, 16));
end;

Résultat : 3333222211110000.

  PSHUFW  : cette instruction est assez amusante. Comme son nom l'indique, en quelque sorte, elle bat les cartes. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. Elle prend de plus un troisième opérande, qui est une valeur immédiate sur 8 bits. L'instruction prend la source considérée comme un packed word (4 mots de 16 bits), les mélange en fonction du troisième opérande et dépose le résultat dans la destination. En fait de mélange, c'est un peu différent d'un jeu de cartes, puisque le même mot de la source peut apparaître plusieurs fois dans la destination. Cela fait justement 4*4*4*4 soit 256 combinaisons possibles. Testons :

procedure TForm1.Button2Click(Sender: TObject);
var
  i0, i1: int64;
  melange: Byte;
begin
  i0 := $0000000000000000;
  i1 := $3333222211110000;
  for melange := 0 to 255 do begin
  asm
    call @ici
    @ici:
    pop eax
    add eax, 20
    mov cl, melange
    mov byte ptr [eax], cl
    movq     mm0, i0
    movq     mm1, i1
    pshufw   mm0, mm1, 8
    movq i0, mm0
  end; //asm
  Memo1.Lines.Add(IntToHex(i0, 16));
  end;
end; 

Nous voulions réaliser une boucle pour mettre en lumière la logique du mélange. Malheureusement, le troisième opérande de PSHUFW est une valeur immédiate. Nous avons donc fait appel à une vilaine ruse : modifier cette valeur immédiate en écrivant dans le code. Cela fonctionne sous Delphi et débogueur, et certainement pas ailleurs. Testé sous Windows 98SE.

Voici le début et la fin des résultats :

0000000000000000
0000000000001111
0000000000002222
0000000000003333
0000000011110000
0000000011111111
0000000011112222
0000000011113333
0000000022220000
0000000022221111
.
.
3333333322223333
3333333333330000
3333333333331111
3333333333332222
3333333333333333

Un exercice pour nos amis mathématiciens : il y a 4*3*2 = 24  combinaisons qui sont des permutations comportant les 4 words initiaux, mais mélangés. Correspondent-elles à un critère particulier sur le troisième opérande ?

  PMULHUW  : la destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits, tous deux considérés comme des words non signés. L'instruction multiplie les mots de la source et ceux de la destination, le résultat intermédiaire est donc sur 32 bits. Les 16 bits forts de ce résultat sont placés dans la destination.

  PMAXSW , PMAXUB , PMINSW , PMINUB  : ces instructions, dont la destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits, se comportent strictement comme PFMAX et PFMIN , mais sur des données de 16 bits signées ( SW ) et 8 bits non signés ( UB ).

  PSADBW  : la destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits. La destination est considérée comme un packed byte au départ et devient un packed word, la source est un packed byte. L'instruction fait les différences deux à deux des 8 bytes de la source et de la destination, en additionne les valeurs absolues et stocke le résultat dans le word de poids faible de la destination. Les autres words sont mis à 0.

mm01 = mm02 = mm03 = 0
mm00 = somme de i = 0 à i = 7 (abs (mm0i - mm1i))

  PAVGB  : moyennes arrondies sur 8 bits non signés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits, tous deux considérés comme des octets non signés compactés. Pour chaque position, les deux octets sont additionnés, puis 1 est ajouté au résultat intermédiaire, et enfin celui-ci est divisé par 2 par décalage à droite.

mm00 = shr((mm00 + mm10 + 1), 1)
mm01 = shr((mm01 + mm11 + 1), 1)
..
mm07 = shr((mm07 + mm17 + 1), 1)

C'est l'instruction PAVGUSB du jeu 3DNow!.

  PAVGW  : moyennes arrondies sur 16 bits non signés. La destination est un registre MMX, la source un registre MMX ou une mémoire 64 bits, tous deux considérés comme des words non signés compactés. Pour chaque position, les deux words sont additionnés, puis 1 est ajouté au résultat intermédiaire, et enfin celui-ci est divisé par 2 par décalage à droite.

mm00 = shr((mm00 + mm10 + 1), 1)
mm01 = shr((mm01 + mm11 + 1), 1)
mm02 = shr((mm02 + mm12 + 1), 1)
mm03 = shr((mm03 + mm13 + 1), 1)

  PEXTRW   : la destination est un registre général 32 bits, la source un registre MMX vu comme un packed word, et il existe en troisième opérande une valeur immédiate sur 8 bits. L'instruction extrait le mot de 16 bits désigné par la valeur immédiate du registre MMX et le dépose dans le word bas du registre général, le reste étant mis à 0 :

begin
  i0 := $4444333322221111;
  asm
    movq     mm0, i0
    mov eax, $FFFFFFFF
    pextrw eax, mm0, 3
    movq i0, mm0
    mov u32_0, eax
  end; //asm
  Memo1.Lines.Add(IntToHex(u32_0, 16));
end;

Le résultat est 0000000000004444 .

  PINSRW  : la destination est un registre MMX vu comme un packed word, la source est un registre général 32 bits, et il existe en troisième opérande une valeur immédiate sur 8 bits. L'instruction extrait le mot de 16 bits du word bas du registre général et le dépose dans le word du registre MMX désigné par la valeur immédiate :

begin
  i0 := $4444333322221111;
  asm
    movq     mm0, i0
    mov eax, $FFFFEEEE
    pinsrw mm0, eax, 7
    movq i0, mm0
    mov u32_0, eax
  end; //asm
  Memo1.Lines.Add(IntToHex(i0, 16));
end;

Le résultat est EEEE333322221111 . Remarquez que la valeur immédiate est interprétée modulo 4 (7 = 4+3).

  PMOVMSKB  : la destination est un registre général 32 bits, la source un registre MMX vu comme un packed byte. Chaque byte du registre MMX est interprété comme dans un masque, seul compte le MSB. Avec ces MSB, l'instruction fabrique un octet qui sera déposé dans le byte faible du registre général, le reste étant mis à 0 :

begin
  i0 := $807F807F807F807F;
  i0 := $7F807F807F807F80;
  asm
    movq     mm0, i0
    mov eax, $FFFFFFFF
    pmovmskb eax, mm0
    movq i0, mm0
    mov u32_0, eax
  end; //asm
  Memo1.Lines.Add(IntToHex(u32_0, 16));
end; 

En mettant en commentaire une de deux initialisations de  i0 , nous obtenons :

i0 := $807F807F807F807F donne 00000000000000AA
i0 := $7F807F807F807F80 donne 0000000000000055

Il suffit, pour interpréter, de noter les valeurs suivantes :

$80 = 10000000b
$7F = 01111111b
$AA = 10101010b
$55 = 01010101b

9.4 Technologies SSE (SSE - SSE2)

La technologie SSE est apparue avec le Pentium 3, dont elle était même une caractéristique majeure. Encore devons-nous nous rappeler que c'est bien l'inscription du numéro de série au cœur de la puce qui a suscité le plus d'articles de presse. Il est vrai que les droits fondamentaux de l'homme et du citoyen étaient bafoués. Par bonheur, le monde libre s'en est remis.

SSE est un acronyme d'acronyme : Streaming SIMD Extensions. Nous savons que SIMD signifie Single Instruction Multiple Datas. Le streaming concerne la façon dont sont traités les transferts mémoire, de façon directe, avec une certaine indépendance par rapport à la circuiterie habituelle du microprocesseur. Peut-être avez-vous eu l'occasion de voir une machine bloquée gravement, alors qu'une image ou un son continue pendant un certain temps à de dérouler. Le streaming sur Internet est autre chose, bien entendu, mais l'idée s’en rapproche : des transferts de données multimédias en flux, et non plus en fichiers, ce qui permet d'en commencer le visionnage ou l'écoute dès que la réception a commencé. Donc, comme déjà les extensions de 3DNow! Et MMX, les nouvelles technologies orientées multimédias sont en rapport étroit avec le hardware.

Les gros problèmes de MMX étaient le partage des registres avec la FPU et le coût du chargement de mode en termes de performances. C'est ce qu'affirmait Intel, semblant ignorer que 3DNow! avait fait un pas dans la bonne direction. Or, les opérations en virgule flottante devenaient cruciales pour les traitements multimédias. De surcroît, il fallait de préférence des flux de données comme ils viennent, c'est-à-dire à grande vitesse très souvent.

SSE possède donc son propre jeu de registres, sur 128 bits chacun et portant les appellations xmm0 à xmm7.

Registres MMX/FPU et SSE
figure 9.14 Registres MMX/FPU et SSE [the .swf]

En principe, chacun de ces registres autorisera des calculs simultanés sur 4 flottants de 32 bits, que nous connaissons maintenant bien, par registre. À noter que, si la technologie évolue, notre œil reste plus ou moins égal à lui-même. Ces 32 bits de précision semblent suffire pour transcrire les nuances visuelles ou sonores de notre environnement multimédia, pour peu que nous puissions les traiter très rapidement, c'est-à-dire en parallèle.

Si les registres font bien 128 bits de large, l'accès en est malheureusement limité à 64 à la fois. Ce qui complique la programmation, puisqu'il faudra doubler les instructions de transfert, version low et high : movlps reg_xmm, adresse et movhps reg_xmm, adresse+8 .

Ce format de données compactées mobilise 50 des 70 nouvelles instructions. Plus 12 pour les autres types de données, entières, et 8 pour la gestion du mode de transfert des données et des caches.

Comme en technologie MMX, les instructions peuvent fonctionner en mode Packed, que nous connaissons déjà. De plus, apparaît un autre mode de fonctionnement, dit Scalaire (scalar), équivalent au mode Packed, mais l'opération n'affecte que sur la donnée d'ordre le plus bas (xmm00, par exemple). En repérant les couples p/s, et également l/h, et à la lumière de tout ce que nous avons vu au cours de ce chapitre, vous pourrez certainement repérer le fonctionnement général d'une grande partie des instructions du jeu SSE :

  Instructions arithmétiques : addps , addss , subps , subss , mulps , mulss , divps , divss , sqrtps , sqrtss , maxps , maxss , minps , minss .

  Instructions logiques : andps , andnps , orps , xorps .

  Instructions de comparaison : cmpps , cmpss , comiss , ucomiss .

  Instructions de brassage : shufps , unpchkps , unpcklps .

  Instructions de conversion : cvtpi2ps , cvtpi2ss , cvtps2pi , cvtss2si .

  Instructions de transfert : movaps , movups , movhps , movlps , movmskps , movss .

  Instructions de contrôle : ldmxcsr , fxsave , stmxscr , fxstor .

  Instructions de contrôle de cache : maskmovq , movntq , movntps , prefetch , sfence .

  Instructions entières supplémentaires du jeu SSE : pextrw , pinsrw , pmaxub , pmaxsw , pminub , pminsw , pmovmskb , pmulhuw , pshufw .

Nous retrouvons, à la dernière ligne, les instructions d'extension du jeu MMX déjà vues : elles s'appliquent aux registres MMX.

SSE ajoute également un registre d'état et de contrôle, MXCSR, dont le rôle est proche de celui des registres FPU équivalents, masquage d'exceptions, mode d'arrondi, par exemple.

SSE nécessite que le système d'exploitation le gère pour pouvoir être utilisé. Cela ne devrait pas poser de problème, puisque c'est le cas depuis 98 et 2000 et dans les deux gammes.

SSE2 est apparu sur le Pentium 4. Cette livraison ajoute au jeu standard SSE de nouvelles instructions. De plus, comme c'est devenu une habitude, l'amélioration est liée au hardware de la génération Pentium 4. La principale amélioration, et qui intéresse le programmeur, est la présence d'un bus 128 bits, donc de possibilités de transferts mémoires simplifiés, à partir de nouvelles instructions.

9.5 Fenêtres de débogage FPU

Chose promise, chose due, nous allons pour terminer inspecter les registres FPU/MMX/3DNow! à l'aide d'une fenêtre FPU. Dans ce but, nous avons rapidement porté sous C++Builder 6 Pro trois extraits de code déjà vu sous Delphi. Avec l'avantage supplémentaire de tester autre chose que Delphi. Ce code d'essai est le suivant :

//-----------------------------------------------------
void __fastcall TForm1::BtnFPUClick(TObject *Sender)
{
float Racine1;
float Delta = 25;
float B = 14;
float A = 1.5;
Integer deux = 2;
 
//Racine1 := (-B + Sqrt(Delta)) / (2 * A);
asm {
  fld Delta
  fsqrt
  fld B
  fchs
  faddp
  fild deux
  fmul A
  fdivp
  fstp Racine1
  wait
  }//asm
MemoSortie->Lines->Add(FloatToStr(Racine1));
}
//-----------------------------------------------------
void __fastcall TForm1::BtnMMXClick(TObject *Sender)
{
__int64  I0, I1;
 
I0 = 0x0011223344556677;
I1 = 0x8899AABBCCDDEEFF;
asm{
  movq mm0, I0
  movq mm1, I1
  PUNPCKLBW mm0, mm1 // CC44 DD55 EE66 FF77
  movq mm2, mm0
  movq mm3, mm1
  movq mm4, mm0
  movq mm5, mm1
  movq mm6, mm0
  movq mm7, mm1
  paddb  mm2, mm3
  paddsb mm4, mm5
  paddw  mm6, mm7
  movq I0, mm0
  emms
  } //asm
MemoSortie->Lines->Add(IntToHex(I0,16));
}
//-----------------------------------------------------
void __fastcall TForm1::Btn3DNowClick(TObject *Sender)
{
Single F0[2];
F0[0] = 25.0;
F0[1] = 0.0;
 
asm{
  movq      mm0, qword ptr[F0]
  pfrsqrt   mm1, mm0
  movq      mm2, mm1
  pfmul     mm1, mm1
  punpckldq mm0, mm0
  pfrsqit1  mm1, mm0
  pfrcpit2  mm1, mm2
  pfmul     mm0, mm1
  movq      qword ptr [F0],  mm0
  femms
  }//asm
MemoSortie->Lines->Add(FloatToStr(F0[0]));
}
//-----------------------------------------------------

La fenêtre de l'application est la suivante.

Application d'essai
figure 9.09 Application d'essai

La procédure est la suivante :

Vous devez mettre des points d'arrêt dans la fenêtre d'édition de code.

Ensuite, ouvrez la fenêtre de débogage FPU par le menu Voir/Fenêtres de débogage/Fenêtre FPU ou par  CTRL  +  ALT  +  F  .

Enfin, pensez à placer cette fenêtre à côté des autres, afin qu'elle ne soit pas en permanence recouverte. Nous vous laissons découvrir, par l'intermédiaire du fameux clic droit, les différentes configurations de l'affichage. Bien entendu, les modes d'affichages, FPU, MMX, etc., sont libres. Le mode MMX + binaire permettra par exemple d'étudier le format des réels. Seule manque, et c'est embêtant, la possibilité d'inspecter les deux réels du mode 3DNow!.

Vue MMX/Hexa/16 bits
figure 9.10 Vue MMX/Hexa/16 bits

 

Vue FPU
figure 9.11 Vue FPU

 

Vue MMX/Décimal signé/16 bits
figure 9.12 Vue MMX/Décimal signé/16 bits

 

Vue MMX/Binaire/16 bits
figure 9.13 Vue MMX/Binaire/16 bits

Puisque nous en terminons et venons de voir un autre environnement, C++Builder, notons que toutes les instructions proposées ont été acceptées sans problème par le compilateur.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1