I. MMX▲
En quoi consistent ces instructions ?
Elles permettent de traiter plusieurs calculs sur les nombres entiers simultanément, au lieu de un à la fois avec les instructions classiques. Ceci peut être particulièrement intéressant lorsque vous faites du traitement d'images et que vous devez effectuer les mêmes opérations sur l'ensemble des points de l'image.
Comment les exploiter ?
C'est là que commencent les difficultés. Lorsque vous compilez un programme, Delphi prend votre code Pascal et le transforme en instructions pour le microprocesseur (vous pouvez voir le résultat dans la fenêtre CPU). Mais il n'utilise jamais les instructions MMX, d'une part pour des raisons de compatibilité avec la plate-forme i386, d'autre part parce qu'en Pascal, nous ne codons des opérations que de façon séquentielle alors que les instructions MMX effectuent un traitement parallèle. La seule solution pour exploiter les possibilités MMX est de « court-circuiter » le compilateur et d'indiquer directement les instructions que nous voulons utiliser. En d'autres termes, travailler en ASSEMBLEUR. En assembleur, nous écrivons les instructions microprocesseurs à l'aide de séquences mnémotechniques qui sont directement traduites en binaire. Le Pascal Objet dispose de BASM (Built-in ASseMbler) qui permet d'inclure de l'assembleur dans le code Pascal. Sauf que jusqu'à Delphi 5 inclus, BASM ne connaissait pas les mnémotechniques correspondant aux instructions MMX. Il fallait réaliser soi-même le codage en binaire. Maintenant Delphi 6 et Kylix prennent en charge les instructions MMX. Il n'y a donc plus qu'à les saisir en assembleur et tout va bien.
Il reste à connaître les instructions utilisables. La première étape obligatoire est de récupérer la liste des instructions chez Intel sous le nom de Instruction Set Refernce Manual. Vous récupérez une petite documentation au format pdf de 6,7 Mo et d'un millier de pages. Pour reconnaître les instructions MMX, prenez plutôt celles qui commencent par un P comme Packed, qui travaillent sur des paquets de données.
Allons du côté de Packed Add.
Vous avez trois instructions différentes PADDB/PADDW/PADDD. Elles permettent d'additionner deux séries, dans l'ordre, soit de 8 bytes (PADDByte), soit de 4 words (PADDWord), soit de 2 doubleword (PADDDoubleword). Nous remarquons au passage que ces instructions utilisent des registres spéciaux, les registres MMX. Ces registres, qui sont au nombre de 8 (de mm0 à mm7) ont une dimension de 64 bits. En fait, ce sont les registres ST pour les opérations sur les nombres à virgule flottante et on ne peut pas utiliser simultanément les opérations MMX et celles sur les nombres à virgule flottante.
Passons à la pratique. J'ai voulu essayer l'addition simultanée de 4 entiers 16 bits. Voici le code :
type
T16bit = array
[0
..3
] of
word
; // tableau de 4 entiers 16 bits
procedure
AdditionMMX(A,B : T16bit; var
resultat : T16bit); register
;
// adresse de A dans EAX
// adresse de B dans EDX
// adresse de resultat dans ECX
asm
movq mm0,[EAX] // on met le tableau A dans le registre mm0
paddw mm0,[EDX] // on lui additionne le contenu du tableau B word par word
movq [ECX],mm0 // on renvoie le résultat dans resultat
end
;
Et voilà, trois instructions pour quatre additions, transfert des données compris. Et ça marche.
Pour compliquer, j'ai voulu programmer la multiplication. Il y a un petit problème avec la dimension des paramètres. Je décide de réaliser la multiplication de bytes pour obtenir des words.
type
T8bit = array
[0
..3
] of
byte
;
T16bit = array
[0
..3
] of
word
;
procedure
MultiplicationMMX(A,B : T8bit; var
resultat : T16bit); register
;
// A dans EAX (et non l'adresse)
// B dans EDX
// adresse de resultat dans ECX
asm
pxor mm0,mm0 // mise à 0
movd mm1,EAX // déplacement du contenu du tableau A dans la partie basse de mm1
PUNPCKLBW mm1,mm0 // on intercale des 0 dans mm1 pour passer de 8 en 16 bits
movd mm2,EDX // déplacement du contenu du tableau B dans la partie basse de mm2
PUNPCKLBW mm2,mm0 // on intercale des 0 dans mm1 pour passer de 8 en 16 bits
PMULLW mm1,mm2 // multiplication de mm1 par mm2, word par word
movq [ECX],mm1 // on renvoie mm1 dans resultat
end
;
Nous voyons en particulier l'étape d'extension des données 8 bits en 16 bits.
Le code source de l'ensemble avec les mêmes opérations avec et sans MMX est ici (3 ko). Vous remarquerez un petit détail :
asm
EMMS // désactivation des registres MMX, à faire avant tout calcul sur les réels
end
;
Comme signalé plus haut, il y a incompatibilité entre les instructions MMX et celles sur virgule flottante. Il faut donc désactiver l'unité MMX avant d'effectuer une opération FP, en l'occurrence l'affichage du temps écoulé. Dans l'autre sens (FP -> MMX), ce n'est pas nécessaire, le compilateur mettant d'office un FWAIT en sortie des opérations sur virgule flottante.
Point de vue performances, le recours aux instructions MMX permet de multiplier la vitesse d'un facteur compris entre 4 et 5 aussi bien pour les additions que pour les multiplications. Donc, les instructions MMX sont efficaces. Reste à les utiliser en conditions réelles :))) Ce qui est fait dans mon nouvel article.
Sinon, sachez que Delphi 6 et Kylix prennent aussi en charge d'autres jeux d'instructions SIMD (single intruction multiple datas = une seule instruction, plusieurs données). Il en va ainsi des instructions SSE et SSE2 (calcul simultané sur plusieurs réels) apparues avec les Pentium III et 4 chez Intel et reprises pour les premières par l'Athlon 4 chez AMD. On retrouve aussi les instructions 3D Now! (à partir du K6-2) et Extended 3D Now! (Athlon) de chez AMD (même principe que SSE) et quelques autres détails. Un article présente l'utilisation de ces instructions.
Pour terminer, sachez que les instructions MMX ont été étendues aux registres SSE avec le jeu d'instructions SSE2. Comme les registres correspondants sont deux fois plus grands, vous pouvez traiter deux fois plus de données avec les mêmes instructions.