L'optimisation de Goom en MMX.

Fonctionnement du filtre de Goom

Pour passer d'une image a la suivante, on doit trouver pour chaque pixel sa nouvelle couleur a partir des couleurs de 4 autres pixels voisins :

Je stocke donc pour chaque pixel, la position d'un des 4 points (celui d'en-haut a droite).
A chaqu'un des 4 points sources est associé un coefficient. La somme des 4 coefficients vaut 64.
Pour trouver la valeur d'un pixel : on ajoute entre eux la valeur de chaqu'un des pixel, multipliée par leur coeffient. (ceci pour chaque composante RGBA).
La valeur obtenue se situe donc entre 0 et 4 * 64 * 256... on divise donc par 256 (decalage de 8) pour de nouveau obtenir la valeur entre 0 et 256, pour chaque composante.

Voici le code en C faisant ceci :
  for (position=0; position < SIZE; position++)
    {
      getPixelRGB_(pix1,pos10[position],&col1);
      getPixelRGB_(pix1,pos10[position]+1,&col2);
      getPixelRGB_(pix1,pos10[position]+prevX,&col3);
      getPixelRGB_(pix1,pos10[position]+prevX+1,&col4);
		
      couleur.r = col1.r * c1[position]
                + col2.r * c2[position]
                + col3.r * c3[position]
                + col4.r * c4[position];
      couleur.r >>= pertedec ;

      couleur.v = col1.v * c1[position]
                + col2.v * c2[position]
                + col3.v * c3[position]
                + col4.v * c4[position];
      couleur.v >>= pertedec ;
		
      couleur.b = col1.b * c1[position]
                + col2.b * c2[position]
                + col3.b * c3[position]
                + col4.b * c4[position];
      couleur.b >>= pertedec ;
		
      setPixelRGB_(pix2,position,couleur);
    }


avec :
pos    -> buffer donnant la position du pixel source.
c1..c4 -> buffer donnant la valeur de chaque coefficient.

pix1   -> le buffer source
pix2   -> le buffer destination
Je ne detaille ici que l'utilisation de ces buffers, et pas leurs calculs, car ce n'est pas une partie aussi critique, et ne necessite pas forcement une optimisation en assembleur. (la version en C est deja tres optimisee).
Les benchmarks ont montres que le programme passe plus de 85% de son temps dans cette boucle.

Passage a l'assembleur

Remarques concernant les acces a la memoire

Commencons par les remarques valables pour toutes les plateformes:

D'abord concernant l'acces aux buffers pos10, c1, c2, c3, c4.
Ces 5 buffers pourront etre a des endroits differents en memoire, ce qui fait qu'a chaque passage, le processeur devra aller chercher dans plusieurs pages differentes du cache. voir pire, devoir tres souvent aller rechercher en memoire les infos qui n'auront pas su etre prevu pas le gestionnaire de cache.
(les echecs de cache sont en effet extremements penalisant, puisqu'ils obligent le processeur a attendre que le transfert se fasse directement de la memoire -- pour une memoire a 100 Mhz, avec un processeur a 800 Mhz, cela fait tout de meme 8 cycles d'horloge, par acces memoire pendant lesquels il ne fait rien).

Pour eviter ce probleme : la version assembleur fonctionne avec des buffers entralacee, ce qui a reduit enormement les temps d'attente durant les acces memoires, puisqu'ils se font maintenant sequentiellement dans un grand buffer.
(prevision tres facile pour le gestionnaire de memoire cache).

La boucle assembleur commence donc par charger les valeurs de pos10 (4 octets) et c1,c2,c3,c4 (1 octet chaqu'un) en un seul acces memoire 64 bits. (grace au MMX, et au bus 64 bits equipant les AMD).
Le plus souvent sans echec de cache, cela ne prend donc maintenant plus qu'1 seul cycle d'horloge.

On entre maintenant dans les specificites du MMX :

Toujours en ce qui concerne les acces memoires, une optimisation etait aussi possible pour recuperer les valeurs des 4 pixels sources, puisqu'ils sont groupes par 2 pixels successifs.
On donc peut charger les 4 pixels en seulement 2 acces memoire 64 bits :

  • 1 pour les 2 pixels du haut.
  • 1 pour les 2 pixels du bas.

    Il sera en revanche difficile ici d'eviter les echecs de cache, car meme si les acces seront souvent regroupes dans une meme zone pour des pixels destinations proche, rien de permet de le prevoir.
    La solution est d'avoir suffisament de cache.. Et c'est ce qui fait que Goom est vraiment plus rapide sur un Athlon de meme frequence que mon Duron... A tel point que j'en revenais pas : J'aurais jamais cru voir tourner goom aussi fluide en 800x600 !!!

    Voila pour la memoire... La derniere remarque est d'eviter de faire des acces memoires successifs.
    et aussi de penser a faire quelque chose d'independant de l'acces memoire juste apres celui-ci... pour ne pas laisser le processeur inactif si ce dernier se deroule mal.

    Le coeur de l'optimisation en MMX

    Finies les petites optimisations locales, entrons maintenant dans le coeur du sujet : les instructions de calcul vectoriel MMX
    Car en fin de compte, c'est bien pour cela qu'il est interressant de faire du MMX.

    Pour commencer : rappels sur le principe des instructions MMX.

    MMX offre 8 registres entiers 64 bits.
    pour le calcul sur des couleurs 32 bits, ces registres sont utilisables comme s'il s'agissait de 4 registres 16 bits.
    L'instruction MMX 'punpcklbw' permet de transformer la partie 32 bits d'un registre MMX de la maniere suivante :

    
         63                  31                   0
          +----+----+----+----+----+----+----+----+
    MM0 = | XX | XX | XX | XX | rr | gg | bb | aa |
          +----+----+----+----+----+----+----+----+
    
     ||
     || punpcklbw MM7, MM0   (MM7 vaut 0; ce qui n'est pas obligatoire,
     ||                           si on veut faire une autre utilisation cette instruction)
     \/
    
          63                  31                   0
          +----+----+----+----+----+----+----+----+
    MM0 = | 00 | rr | 00 | gg | 00 | bb | 00 | aa |
          +----+----+----+----+----+----+----+----+
    
    Bien entendu, l'instruction inverse existe aussi.

    Voyons ici quelles sont les instructions interressantes pour nous :

  • pmullw : permet de multiplier entre eux 2 registres MMX par tranche de 16 bits.
    Pour Goom : permet de faire [ r*c1 | g*c1 | b*c1 | a*c1 ] en 3 cycles CPU, pendant lesquels il est possible d'effectuer autre chose. (il y a 2 unites d'execution MMX).

  • paddw : permet d'ajouter entre eux 2 registrs MMX par tranche de 16 bits.
    Pour Goom : permet de faire [ r1 + r2 | g1 + g2 | b1 + b2 | a1 + a2 ] en 1 seul cycle CPU. (meme remarque qu'avant..)

  • psrlw : permet de faire un declage vers la droite de chaque partie 16 bits du registre MMX.
    Pour Goom : permet de faire [ r >> 8 | g >> 8 | b >> 8 | a >> 8 ] en 1 seul cycle CPU. (idem).

    Pour le detail de l'implementation, je ferais l'explication plus tard si ca interresse quelqu'un :

    Voici le code assembleur en notation GASM.

    
    ;// file : mmx_zoom.s
    ;// author : JC Hoelt <jeko@free.fr>
    ;//
    ;// history
    ;// 07/01/2001 : Changing FEMMS to EMMS : slower... but run on intel machines
    ;//	03/01/2001 : WIDTH and HEIGHT are now variable
    ;//	28/12/2000 : adding comments to the code, suppress some useless lines
    ;//	27/12/2000 : reducing memory access... improving performance by 20%
    ;//		coefficients are now on 1 byte
    ;//	22/12/2000 : Changing data structure
    ;//	16/12/2000 : AT&T version
    ;//	14/12/2000 : unrolling loop
    ;//	12/12/2000 : 64 bits memory access
    
    
    .data
    
    thezero:
    	.long 0x00000000
    	.long 0x00000000
    
    
    .text
    
    .globl mmx_zoom		;// name of the function to call by C program
    .extern coeffs		;// the transformation buffer
    .extern expix1,expix2 ;// the source and destination buffer
    .extern mmx_zoom_size, mmx_zoom_width ;// size of the buffers
    
    .align 16
    mmx_zoom:
    
    push %ebp
    push %esp
    
    ;// initialisation du mm7 à zero
    movq (thezero), %mm7
    
    movl mmx_zoom_width, %eax
    movl $4, %ebx
    mull %ebx
    movl %eax, %ebp
    
    movl (coeffs), %eax
    movl (expix1), %edx
    movl (expix2), %ebx
    movl $10, %edi
    movl mmx_zoom_size, %ecx
    
    .while:
    	;// esi <- nouvelle position
    	movl (%eax), %esi
    	leal (%edx, %esi), %esi
    
    	;// recuperation des deux premiers pixels dans mm0 et mm1
    	movq (%esi), %mm0		/* b1-v1-r1-a1-b2-v2-r2-a2 */
    	movq %mm0, %mm1			/* b1-v1-r1-a1-b2-v2-r2-a2 */
    
    	;// recuperation des 4 coefficients
    	movd 4(%eax), %mm6		/* ??-??-??-??-c4-c3-c2-c1 */
    	;// depackage du premier pixel
    	punpcklbw %mm7, %mm0	/* 00-b2-00-v2-00-r2-00-a2 */
    
    	movq %mm6, %mm5			/* ??-??-??-??-c4-c3-c2-c1 */
    	;// depackage du 2ieme pixel
    	punpckhbw %mm7, %mm1	/* 00-b1-00-v1-00-r1-00-a1 */
    
    	;// extraction des coefficients...
    	punpcklbw %mm5, %mm6	/* c4-c4-c3-c3-c2-c2-c1-c1 */
    	movq %mm6, %mm4			/* c4-c4-c3-c3-c2-c2-c1-c1 */
    	movq %mm6, %mm5			/* c4-c4-c3-c3-c2-c2-c1-c1 */
    
    	punpcklbw %mm5, %mm6	/* c2-c2-c2-c2-c1-c1-c1-c1 */
    	punpckhbw %mm5, %mm4	/* c4-c4-c4-c4-c3-c3-c3-c3 */
    
    	movq %mm6, %mm3			/* c2-c2-c2-c2-c1-c1-c1-c1 */
    	punpcklbw %mm7, %mm6	/* 00-c1-00-c1-00-c1-00-c1 */
    	punpckhbw %mm7, %mm3	/* 00-c2-00-c2-00-c2-00-c2 */
    	
    	;// multiplication des pixels par les coefficients
    	pmullw %mm6, %mm0		/* c1*b2-c1*v2-c1*r2-c1*a2 */
    	pmullw %mm3, %mm1		/* c2*b1-c2*v1-c2*r1-c2*a1 */
    	paddw %mm1, %mm0
    	
    	;// ...extraction des 2 derniers coefficients
    	movq %mm4, %mm5			/* c4-c4-c4-c4-c3-c3-c3-c3 */
    	punpcklbw %mm7, %mm4	/* 00-c3-00-c3-00-c3-00-c3 */
    	punpckhbw %mm7, %mm5	/* 00-c4-00-c4-00-c4-00-c4 */
    
    	;// recuperation des 2 derniers pixels
    	movq (%esi,%ebp), %mm1
    	movq %mm1, %mm2
    	
    	;// depackage des pixels
    	punpcklbw %mm7, %mm1
    	punpckhbw %mm7, %mm2
    	
    	;// multiplication pas les coeffs
    	pmullw %mm4, %mm1
    	pmullw %mm5, %mm2
    	
    	;// ajout des valeurs obtenues à la valeur finale
    	paddw %mm1, %mm0
    	paddw %mm2, %mm0
    
    	;// division par 256 = 16+16+16+16, puis repackage du pixel final
    	psrlw $8, %mm0
    	packuswb %mm7, %mm0
    	
    	;// passage au suivant
    	leal 8(%eax), %eax
    
    	decl %ecx
    	;// enregistrement du resultat
    	movd %mm0, (%ebx)
    	leal 4(%ebx), %ebx
    
    	;// test de fin du tantque
    	cmpl $0, %ecx				;// 400x300
    
    jz .fin_while
    jmp .while
    
    .fin_while:
    emms
    
    pop %esp
    pop %ebp
    
    ret                  ;//The End