MMX ( 2 ) avec Delphi 6 / Kylix

Eric SIBERT

Dans mon premier article, j'ai présenté le principe d'utilisation des instructions MMX et le résultat, en terme de performance, sur un programme test. Il est maintenant temps de travailler en conditions réelles. Pour ceci, j'ai repris mon programme de traitement d'images et j'ai essayé de reprogrammer les opérations en utilisant les instructions MMX. Un petit tour dans la documentation d'Intel s'impose. ?

- Titre : MMX ( 2 ) avec Delphi 6 / Kylix

- Auteur : Eric SIBERT

- Parution : 3 novembre 2002

- Mise à jour : 3 novembre 2002

- Licence : Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

Voici donc le début de la page 374 de la documentation d'Intel. On trouve de haut en bas :

- le nom abrégé et le nom développé de l'instruction ;

- un tableau avec le code binaire (Opcode), le format de l'instruction et une description sommaire. Déjà avec ceci, vous avez la majorité des infos sur l'instruction. Elle multiplie les words de mm (mm pour registre mmx) par ceux de mm/m64 (un registre mmx ou une adresse en mémoire), les additionne deux à deux et remet le tout dans mm (celui de départ) ;

- une description détaillée si vous n'avez pas bien compris avec des détails supplémentaires ;

- parfois, un graphe pour visualiser le principe de l'opération, et c'est bien pratique pour les instructions MMX ;

- ensuite, un tas de choses dont vous n'avez pas besoin en général et en particulier pour utiliser les instructions MMX.

Ensuite l'utilisation de la fenêtre FPU qui peut afficher le contenu des registres MMX est d'un grand secours :

Passons maintenant à la transformation d'une image couleur en niveau de gris. Le niveau de gris est calculé par combinaison de la luminosité des trois couleurs primaires (RGB pour Red, Green, Blue) :
L = R*0.715160 + G*0.212671 + B*0.072169
Néanmoins, un tel calcul faisant appel à des réels serait assez lourd aussi j'utilise une petite variante :
L = (R*183 + G*54 + B*18) / 256
qui ne travaille que sur des entiers avec une division très facile à mettre en œuvre.
J'emploie cette méthode aussi bien sans qu'avec les instructions MMX.

Étudions plus précisément l'emploi des instructions MMX. Les registres MMX (notés de mm0 à mm7) sont constitués de 64 bits qui peuvent être décomposés en 2 dword (appelé integer en pascal), en 4 word ou en 8 bytes. J'ai retenu cette dernière représentation pour la figure suivante. Concernant les pixels, ils sont stockés dans des integer, qui se décomposent en 4 bytes correspondant aux trois couleurs primaires et à la transparence.

La figure suivante présente l'ensemble des opérations que subit un pixel :

- Au départ, l'adresse du pixel est stockée dans EAX. Movd ramène le pixel dans mm2 ;

- punpcklbw sert à entrelacer mm2 avec mm0 qui a été préalablement mis à zéro. Ceci revient à passer de byte en word, en prévision de l'instruction suivante ;

- On multiplie chaque couleur par son coefficient de luminosité avec pmaddwd et on commence à additionner les résultats.([a1;a2]=B*u+G*v et [b1;b2]=R*w) ;

- psrld divise par 256 en décalant de 8 bits vers la droite ;

- désentralacement avec packuswb pour tout ramener en partie basse du registre mmx ;

- la multiplication et addition ( pmaddwd ) par mm3 permet d'additionner les deux restants (c=a1+b1) ;

- movd renvoie le résultat dans un registre classique ;

- la multiplication (imul) par ECX préalablement mis à $010101 permet d'étendre le byte de luminosité à l'ensemble du pixel ;

- enfin, mov renvoie le résultat dans le pixel d'origine.

Voici le code source (8 ko) correspondant. Il a été écrit sous D3, mais les instructions MMX ne fonctionnent que sous D6 ou Kylix.
J'ai effectué quelques tests de performance avec mon PC (Celeron 375 Mhz, cache L2 128 ko) sur une image de 1024x815 et j'en ai déduit le nombre moyen (écart type < 1 %) de cycles d'horloge utilisés par pixel :

Opération

Instructions MMX

Nb d'instructions

Nb cycles
avec D6

Gain MMX

Nb cycles
avec Kylix

Gain MMX

Gain
Kylix / D6

Gris

Non

25

23,9

25,6

-7,2 %

Oui

14

21,5

10,3 %

21,2

17,3 %

+1,2 %

Négatif

Non

4

14,0

14,9

-6,3 %

Oui

3

10,2

27,4 %

14,8

0,7 %

- 42,1 %

Flou

Non

65

120,4

119,1

+1,0 %

Oui

29

25,0

79,3 %

25,4

78,7 %

-1,8 %

J'ai aussi rajouté le nombre d'instructions déduit de la fenêtre CPU.

Quelques commentaires sur les résultats

Mise en niveaux de gris
sans MMX : exécution très efficace avec un nombre de cycles proche, voir en dessous du nombre d'instructions, certainement grâce à l'architecture superscalaire du microprocesseur qui permet parfois d'exécuter deux instructions en parallèle.
avec MMX : améliore de la vitesse, mais pas autant que ne le laissait espérer le nombre d'instructions.

Négatif
sans MMX : nous sommes largement au-dessus de la théorie. Un autre facteur doit limiter la vitesse.
avec MMX : sous Windows, nous avons un gain proportionnel au nombre d'instructions, qui reste donc largement au-dessus de la théorie. Sous Linux, il n'y a pas de gain. Le même facteur limitant que sans MMX semble exister.

Réalisation d'un flou
sans MMX : le nombre de cycles est le double de celui des instructions. Cette différence provient sans doute du fait que les variables locales ne peuvent être stockées dans les registres, mais doivent être transférées en la mémoire, ce qui peut prendre du temps
avec MMX : d'abord, j'ai un peu triché, car je n'ai pas trouvé de moyen aisé de diviser par neuf après avoir additionné le contenu de 3x3 pixels, alors j'ai divisé par huit, qui s'obtient par un simple décalage de 3 bits. Le résultat est par contre fulgurant, non seulement j'ai divisé le nombre d'instructions par deux, mais en plus les nombres de cycles et d'instructions sont équivalents, conduisant à une vitesse globale multipliée par 5. Une part de cette performance est certainement due à la gestion des variables locales, qui tiennent toutes dans les registres.

En conclusion, je trouve qu'à part pour le flou, le recours aux instructions MMX n'est pas justifié. Je pense qu'en dessous d'un certain nombre d'instructions par pixel, le transfert de l'image (3,3 Mo) entre la mémoire externe et le microprocesseur devient le facteur limitant. Ce seuil serait de 10 cycles sous Win32 et de 15 sous Linux, la différence pouvant provenir de gestions différentes de la mémoire cache du microprocesseur par l'OS.

Il convient donc de bien étudier une opération avant d'essayer de l'optimiser avec les instructions MMX. Parmi les critères favorables :

- possibilité de ramener les variables locales dans les registres en utilisant les instructions MMX ;

- calcul lourd (plutôt que rapide et limité par la mémoire) ;

- possibilité de réduire significativement le nombre d'instructions avec le MMX.