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 destinationJe 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).
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 :
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.
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 :
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