4. SIMD Within A Register : SWAR (Ex : utilisation de MMX)

Le SIMD (« Single Instruction stream, Multiple Data stream » ou « Un seul flux d'instruction, plusieurs flux de données ») à l'Intérieur d'Un Registre (ou « SIMD Within A Register » : SWAR) n'est pas un concept récent. En considérant une machine dotée de registres, bus de données et unités de fonctions de k bits, il est connu depuis longtemps que les opérations sur les registres ordinaires peuvent se comporter comme des opérations parallèles SIMD sur n champs de k/n bits chacun. Ce n'est en revanche qu'avec les efforts récents en matière de multimédia que l'accélération d'un facteur deux à huit apportée par les techniques SWAR a commencé à concerner l'informatique générale. Les versions de 1997 de la plupart des microprocesseurs incorporent une prise en charge matérielle du SWAR[23] :

Il existe quelques lacunes dans la prise en charge matérielle apportée par les nouveaux microprocesseurs, des caprices comme par exemple la prise en charge d'un nombre limité d'instructions pour certaines tailles de champ. Il est toutefois important de garder à l'esprit que bon nombre d'opérations SWAR peuvent se passer d'accélération matérielle. Par exemple, les opérations au niveau du bit sont insensibles au partitionnement logique d'un registre.

4.1. Quels usages pour le SWAR ?

Bien que tout processeur moderne soit capable d'exécuter un programme en effectuant un minimum de parallélisme SWAR, il est de fait que même le plus optimisé des jeux d'instructions SWAR ne peut gérer un parallélisme d'intérêt vraiment général. A dire vrai, de nombreuses personnes ont remarqué que les différences de performance entre un Pentium ordinaire et un Pentium « avec technologie MMX » étaient surtout dûes à certains détails, comme le fait que l'augmentation de la taille du cache L1 coïncide avec l'apparition du MMX. Alors, concrètement, à quoi le SWAR (ou le MMX) est-il utile ?

  • Dans le traitement des entiers, les plus courts étant les meilleurs. Deux valeurs 32 bits tiennent dans un registre MMX 64 bits, mais forment aussi une chaîne de huit caractères, ou encore permettent d'encoder un échiquier complet, à raison d'un bit par case. Note : il existera une version « virgule flottante » du MMX, bien que très peu de choses aient été dites à ce sujet. Cyrix a déposé une présentation, ftp://ftp.cyrix.com/developr/mpf97rm.pdf, qui contient quelques commentaires concernant MMFP. Apparemment, MMFP pourra prendre en charge le chargement de nombres 32 bits en virgule flottante dans des registres MMX 64 bits. Ceci combiné à deux pipelines MMFP fournirait quatre FLOP [24] en simple précision par cycle d'horloge.

  • Pour le SIMD, ou parallélisme vectorisé. La même opération s'applique à tous les champs, simultanément. Il existe des moyens d'annihiler ses effets sur des champs sélectionnés (l'équivalent du « SIMD enable masking », ou « activation du masquage »), mais ils sont compliqués à mettre en œuvre et grèvent les performances.

  • Pour les cas où la référence à la mémoire se fait de manière localisée et régulière (et de préférence regroupée). Le SWAR en général, et le MMX en particulier se révèlent désastreux sur les accès aléatoires. Rassembler le contenu d'un vecteur x[y] (où y est l'index d'un tableau) est extrêmement coûteux.

Il existe donc d'importantes limitations, mais ce type de parallélisme intervient dans un certain nombre d'algorithmes, et pas seulement dans les applications multimédia. Lorsque l'algorithme est adapté, le SWAR est plus efficace que le SMP ou le parallélisme en clusters… et son utilisation ne coûte rien !

4.2. Introduction à la programmation SWAR

Le concept de base du SWAR (« SIMD Within A Register », ou « SIMD à l'intérieur d'un registre ») réside dans le fait que l'on peut utiliser des opérations sur des registres de la taille d'un mot pour accélérer les calculs en effectuant des opérations parallèles SIMD sur n champs de k/n bits. En revanche, employer les techniques du SWAR peut parfois s'avérer maladroit, et certaines de leurs opérations peuvent au final être plus coûteuses que leurs homologues en série car elles nécessitent des instructions supplémentaires pour forcer le partitionnement des champs.

Pour illustrer ce point, prenons l'exemple d'un mécanisme SWAR grandement simplifié gérant quatre champs de 8 bits dans chaque registre de 32 bits. Les valeurs de ces deux registres pourraient être représentées comme suit :

         PE3     PE2     PE1     PE0
      +-------+-------+-------+-------+
Reg0  | D 7:0 | C 7:0 | B 7:0 | A 7:0 |
      +-------+-------+-------+-------+
Reg1  | H 7:0 | G 7:0 | F 7:0 | E 7:0 |
      +-------+-------+-------+-------+

Ceci indique simplement que chaque registre est vu essentiellement comme un vecteur de quatre valeurs entières 8 bits indépendantes. Alternativement, les valeurs A et E peuvent être vues comme les valeurs, dans Reg0 et Reg1, de l'élément de traitement 0 (ou « PE0 » pour « Processing Element 0 »), B et F comme celles de l'élément 1, et ainsi de suite.

Le reste de ce document passe rapidement en revue les classes de base des opérations parallèles SIMD sur ces vecteurs d'entiers et la façon dont ces fonctions peuvent être implémentées.

4.2.1. Opérations polymorphiques

Certaines opérations SWAR peuvent être effectuées de façon triviale en utilisant des opérations ordinaires sur des entiers 32 bits, sans avoir à se demander si l'opération est réellement faite pour agir en parallèle et indépendemment sur ces champs de 8 bits. On qualifie ce genre d'opération SWAR de polymorphique, parce que la fonction est insensible aux types des champs (et à leur taille).

Tester si un champ quelconque est non-nul est une opération polymorphique, tout comme les opérations logiques au niveau du bit. Par exemple, un « ET » logique bit-à-bit ordinaire (l'opérateur « & » du registre C) effectue son calcul bit-à-bit, quelque soit la taille des champs. Un « ET » logique simple des registres ci-dessus donnerait :

          PE3       PE2       PE1       PE0
      +---------+---------+---------+---------+
Reg2  | D&H 7:0 | C&G 7:0 | B&F 7:0 | A&E 7:0 |
      +---------+---------+---------+---------+

Puisque le bit de résultat k de l'opération « ET » logique n'est affecté que par les valeurs des bits d'opérande k, toutes les tailles de champs peuvent être prises en charge par une même instruction.

4.2.2. Opérations partitionnées

Malheureusement, de nombreuses et importantes opérations SWAR ne sont pas polymorphiques. Les opérations arithmétiques comme l'addition, la soustraction, la multiplication et la division sont sujettes aux interactions de la « retenue » entre les champs. Ces opérations sont dites partitionnées car chacune d'elles doit cloisonner effectivement les opérandes et le résultat pour éviter les interactions entre champs. Il existe toutefois trois méthodes différentes pouvant être employées pour parvenir à ces fins :

4.2.2.1. Instructions partitionnées

L'approche la plus évidente pour implémenter les opérations partitionnées consiste peut-être à proposer une prise en charge matérielle des « instructions parallèles partitionnées » coupant le système de retenue entre les champs. Cette approche peut offrir les performances les plus élevées, mais elle nécessite la modification du jeu d'instruction du processeur et implique en général des limitations sur la taille des champs (Ex : ces instructions pourraient prendre en charge des champs larges de 8 bits, mais pas de 12).

Le MMX des processeurs AMD, Cyrix et Intel, Le MAX de Digital, celui d'HP, et le VIS de Sun implémentent tous des versions restreintes des instructions partitionnées. Malheureusement, ces différents jeux d'instructions posent des restrictions assez différentes entre elles, ce qui rend les algorithmes difficilement portables. Étudions à titre d'exemple l'échantillon d'opérations partitionnées suivant :

  Instruction           AMD/Cyrix/Intel MMX   DEC MAX   HP MAX   Sun VIS
+---------------------+---------------------+---------+--------+---------+
| Différence Absolue  |                     |       8 |        |       8 |
+---------------------+---------------------+---------+--------+---------+
| Maximum             |                     |   8, 16 |        |         |
+---------------------+---------------------+---------+--------+---------+
| Comparaison         |           8, 16, 32 |         |        |  16, 32 |
+---------------------+---------------------+---------+--------+---------+
| Multiplication      |                  16 |         |        |    8x16 |
+---------------------+---------------------+---------+--------+---------+
| Addition            |           8, 16, 32 |         |     16 |  16, 32 |
+---------------------+---------------------+---------+--------+---------+

Dans cette table, les chiffres indiquent les tailles de champ reconnues par chaque opération. Même si la table exclut un certain nombre d'instructions dont les plus exotiques, il est clair qu'il y a des différences. Cela a pour effet direct de rendre inefficaces les modèles de programmation en langages de haut niveau, et de sévèrement restreindre la portabilité.

4.2.2.2. Opérations non partitionnées avec code de correction

Implémenter des opérations partitionnées en utilisant des instructions partitionnées est certainement très efficace, mais comment faire lorsque l'opération dont vous avez besoin n'est pas prise en charge par le matériel ? Réponse : utiliser une série d'instructions ordinaires pour effectuer l'opération malgré les effets de la retenue entre les champs, puis effectuer la correction de ces effets indésirés.

C'est une approche purement logicielle, et les corrections apportent bien entendu un surcoût en temps, mais elle fonctionne pleinement avec le partitionnement en général. Cette approche est également « générale » dans le sens où elle peut être utilisée soit pour combler les lacunes du matériel dans le domaine des instructions partitionnées, soit pour apporter un soutien logiciel complet aux machines qui ne sont pas du tout dotées d'une prise en charge matérielle. À dire vrai, en exprimant ces séquences de code dans un langage comme le C, on permet au SWAR d'être totalement portable.

Ceci soulève immédiatement une question : quelle est précisément l'inefficacité des opérations SWAR partitionnées simulées à l'aide d'opérations non partitionnées ? Eh bien c'est très certainement la question à 65536 dollars, mais certaines opérations ne sont pas aussi difficiles à mettre en œuvre que l'on pourrait le croire.

Prenons en exemple le cas de l'implémentation de l'addition d'un vecteur de quatre éléments 8 bits contenant des valeurs entières, soit x+y, en utilisant des opérations 32 bits ordinaires.

Une addition 32 bits ordinaire pourrait en fait rendre un résultat correct, mais pas si la retenue d'un champ de 8 bits se reporte sur le champ suivant. Aussi, notre but consiste simplement à faire en sorte qu'un tel report ne se produise pas. Comme l'on est sûr qu'additionner deux champs de k bits ne peut générer qu'un résultat large d'au plus k+1 bits, on peut garantir qu'aucun report ne va se produire en « masquant » simplement le bit de poids fort de chaque champs. On fait cela en appliquant un « ET » logique à chaque opérande avec la valeur 0x7f7f7f7f, puis en effectuant un addition 32 bits habituelle.

t = ((x & 0x7f7f7f7f) + (y & 0x7f7f7f7f));

Ce résultat est correct… sauf pour le bit de poids fort de chacun des champs. Il n'est question, pour calculer la valeur correcte, que d'effectuer deux additions 1-bit partitionnées, depuis x et y vers le résultat à 7 bits calculé pour t. Heureusement, une addition partitionnée sur 1 bit peut être implémentée à l'aide d'une opération « OU Exclusif » logique ordinaire. Ainsi, le résultat est tout simplement :

(t ^ ((x ^ y) & 0x80808080))

D'accord. Peut-être n'est-ce pas si simple, en fin de compte. Après tout, cela fait six opérations pour seulement quatre additions. En revanche, on remarquera que ce nombre d'opérations n'est pas fonction du nombre de champs. Donc, avec plus de champs, on gagne en rapidité. À dire vrai, il se peut que l'on gagne quand même en vitesse simplement parce que les champs sont chargés et redéposés en une seule opération (vectorisée sur des entiers), que la disponibilité des registres est optimisée, et parce qu'il y a moins de code dynamique engendrant des dépendances (parce que l'on évite les références à des mots incomplets).

4.2.2.3. Contrôler les valeurs des champs

Alors que les deux autres approches de l'implémentation des opérations partitionnées se centrent sur l'exploitation du maximum d'espace possible dans les registres, il peut être, au niveau du calcul, plus efficace de contrôler les valeurs des champs de façon à ce que l'interaction de la retenue entre ceux-ci ne se produise jamais. Par exemple, si l'on sait que toutes les valeurs de champs additionnées sont telles qu'aucun dépassement ne peut avoir lieu, une addition partitionnée peut être implémentée à l'aide d'une instruction ordinaire. Cette contrainte posée, une addition ordinaire pourrait en fin de compte apparaître polymorphique, et être utilisable avec n'importe quelle taille de champ sans programme de correction. Ce qui nous amène ainsi à la question suivante : Comment s'assurer que les valeurs des champs ne vont pas provoquer d'événements dûs à la retenue ?

Une manière de faire cela consiste à implémenter des instructions partitionnées capables de restreindre la portée des valeurs des champs. Les instructions vectorisées de maximum et de minimum du MAX de Digital peuvent être assimilées à une prise en charge matérielle de l'écrêtage des valeurs des champs pour éviter les interactions de la retenue entre les champs.

En revanche, si l'on ne dispose pas d'instructions partitionnées à même de restreindre efficacement la portée des valeurs des champs, existe-t-il une condition qui puisse être imposée à un coût raisonnable et qui soit suffisante pour garantir le fait que les effets de la retenue n'iront pas perturber les champs adjacents ? La réponse se trouve dans l'analyse des propriétés arithmétiques. Additionner deux nombres de k bits renvoie un résultat large d'au plus k+1 bits. Ainsi, un champ de k+1 bits peut recevoir en toute sécurité le résultat d'une telle opération, même s'il est produit par une instruction ordinaire.

Supposons donc que les champs de 8 bits de nos précédents exemples soient désormais des champs de 7 bits avec « espaces de séparation pour retenue » d'un bit chacun :

              PE3          PE2          PE1          PE0
      +----+-------+----+-------+----+-------+----+-------+
Reg0  | D' | D 6:0 | C' | C 6:0 | B' | B 6:0 | A' | A 6:0 |
      +----+-------+----+-------+----+-------+----+-------+

Mettre en place un vecteur d'additions 7 bits se fait de la manière suivante : On part du principe qu'avant l'exécution de toute instruction partitionnée, les bits des séparateurs de retenue (A', B', C', et D') sont nuls. En effectuant simplement une addition ordinaire, tous les champs reçoivent une valeur correcte sur 7 bits. En revanche, certains bits de séparation peuvent passer à 1. On peut remédier à cela en appliquant une seule opération supplémentaire conventionnelle et masquant les bits de séparation. Notre vecteur d'additions entières sur 7 bits, x+y, devient ainsi :

((x + y) & 0x7f7f7f7f)

Ceci nécessite seulement deux opérations pour effectuer quatre additions. Le gain en vitesse est évident.

Les lecteurs avertis auront remarqué que forcer les bits de séparation à zéro ne convient pas pour les soustractions. La solution est toutefois remarquablement simple : Pour calculer x-y, on garantira simplement la condition initiale, qui veut que tous les bits de séparation de x valent 1 et que tous ceux de y soient nuls. Dans le pire des cas, nous obtiendrons :

(((x | 0x80808080) - y) & 0x7f7f7f7f)

On peut toutefois souvent se passer du « OU » logique en s'assurant que l'opération qui génère la valeur de x utilise | 0x80808080 plutôt que & 0x7f7f7f7f en dernière étape.

Laquelle de ces méthodes doit-on appliquer aux opérations partitionnées du SWAR ? La réponse est simple : « celle qui offre le gain en rapidité le plus important ». Ce qui est intéressant, c'est que la méthode idéale peut être différente selon chaque taille de champ, et ce à l'intérieur d'un même programme, s'exécutant sur une même machine.

4.2.3. Opérations de communication et de conversion de type

Bien que certains calculs en parallèle, incluant les opérations sur les pixels d'une image, ont pour propriété le fait que la nième valeur d'un vecteur soit une fonction des valeurs se trouvant à la nième position des vecteurs des opérandes, ce n'est généralement pas le cas. Par exemple, même les opérations sur les pixels telles que l'adoucissement ou le flou réclament en opérande les valeurs des pixels adjacents, et les transformations comme la transformée de Fourrier (FFT) nécessitent des schémas de communications plus complexes (et moins localisés).

Il est relativement facile de mettre efficacement en place un système de communication à une dimension entre les valeurs du voisinage immédiat pour effectuer du SWAR, en utilisant des opérations de décalage non partitionnées. Par exemple, pour déplacer une valeur depuis PE vers PE(n+1), un simple décalage logique suffit. Si les champs sont larges de 8 bits, on utilisera :

(x << 8)

Pourtant, ce n'est pas toujours aussi simple. Par exemple, pour déplacer une valeur depuis PEn vers PE(n-1), un simple décalage vers la droite devrait suffire… mais le langage C ne précise pas si le décalage vers la droite conserve le bit de signe, et certaines machines ne proposent, vers la droite, qu'un décalage signé. Aussi, d'une manière générale, devons-nous mettre explicitement à zéro les bits de signe pouvant être répliqués.

((x >> 8) & 0x00ffffff)

L'ajout de connexions « à enroulement » (« wrap-around ») à l'aide de décalages non partitionnés [25] est aussi raisonnablement efficace. Par exemple, pour déplacer une valeur depuis PEi vers PE(i+1) avec enroulement :

((x << 8) | ((x >> 24) & 0x000000ff))

Les problèmes sérieux apparaissent lorsque des schémas de communications plus généraux doivent être mis en œuvre. Seul le jeu d'instructions du MAX de HP permet le réarrangement arbitraire des champs en une seule instruction, nommée Permute. Cette instruction Permute porte vraiment mal son nom : Non seulement elle effectue une permutation arbitraire des champs, mais elle autorise également les répétitions. En bref, elle met en place une opération arbitraire de x[y].

Il reste malheureusement très difficile d'implémenter x[y] sans le concours d'une telle instruction. La séquence de code est généralement à la fois longue et inefficace, parce qu'en fait, il s'agit d'un programme séquentiel. Ceci est très décevant. La vitesse relativement élevée des opérations de type x[y] sur les les supercalculateurs SIMD MasPar MP1/MP2 et Thinking Machines CM1/CM2/CM200 était une des clés majeures de leur succès. Toutefois, un x[y] reste plus lent qu'une communication de proximité, même sur ces supercalculateurs. Beaucoup d'algorithmes ont donc été conçus pour réduire ces besoins en opérations de type x[y]. Pour simplifier, disons que sans soutien matériel, le meilleur est encore très certainement de développer des algorithmes SWAR en considérant x[y] comme étant interdit, ou au moins très coûteux.

4.2.4. Opérations récurrentes (réductions, balayages, et cætera)

Une récurrence est un calcul dans lequel il existe une relation séquentielle apparente entre les valeurs à traiter. Si toutefois des opérations associatives sont impliquées dans ces récurrences, il peut être possible de recoder ces calculs en utilisant un algorithme parallèle à structure organisée en arbre.

Le type de récurrence parallélisable le plus courant est probablement la classe connue sous le nom de réduction associative. Par exemple, pour calculer la somme des valeurs d'un vecteur, on écrit du code C purement séquentiel, comme :

t = 0;
for (i=0; i<MAX; ++i) t += x[i];

Cependant, l'ordre des additions est rarement important. Les opérations en virgule flottante et les saturations peuvent rendre différents résultats si l'ordre des additions est modifié, mais les additions ordinaires sur les entiers (et qui reviennent au début après avoir atteint la valeur maximum) renverront exactement les mêmes résultats, indépendemment de l'ordre dans lequel elles sont effectuées. Nous pouvons ainsi réécrire cette séquence sous la forme d'une somme parallèle structurée en arbre, dans laquelle nous additionnons d'abord des paires de valeurs, puis des paires de ces sous-totaux, et ainsi de suite jusqu'à l'unique somme finale. Pour un vecteur de quatre valeurs 8 bits, seulement deux additions sont nécessaires. La première effectue deux additions de 8 bits et retourne en résultat deux champs de 16 bits, contenant chacun un résultat sur 9 bits :

t = ((x & 0x00ff00ff) + ((x >> 8) & 0x00ff00ff));

La deuxième fait la somme de ces deux valeurs de 9 bits dans un champs de 16 bits, et renvoie un résultat sur 10 bits :

((t + (t >> 16)) & 0x000003ff)

En réalité, la seconde opération effectue l'addition de deux champs de 16 bits… mais les 16 bits de poids fort n'ont pas de signification. C'est pourquoi le résultat est masqué en une valeur de 10 bits.

Les balayages (« scans »), aussi connus sous le nom d'opérations « à préfixe parallèle » sont un peu plus difficile à mettre en œuvre efficacement. Ceci est dû au fait que contrairement aux réductions, les balayages peuvent produire des résultats partitionnées.

4.3. SWAR MMX sous Linux

Pour Linux, nous nous soucierons principalement des processeurs IA32. La bonne nouvelle, c'est qu'AMD, Cyrix et Intel implémentent tous le même jeu d'instructions MMX. En revanche, les performances de ces différents MMX sont variables. Le K6, par exemple, n'est doté que d'un seul pipeline là où le Pentium MMX en a deux. La seule nouvelle vraiment mauvaise, c'est qu'Intel diffuse toujours ces stupides films publicitaires sur le MMX… ;-)

Il existe trois approches sérieuses du SWAR par le MMX :

  1. L'utilisation des fonctions d'une bibliothèque dédiée au MMX. Intel, en particulier, a développé plusieurs « bibliothèques de performances », offrant toute une gamme de fonctions optimisées à la main et destinées aux tâches multimédia courantes. Avec un petit effort, bon nombre d'algorithmes non-multimédia peuvent être retravaillés pour permettre à quelques unes des zones de calcul intensif de s'appuyer sur une ou plusieurs de ces bibliothèques. Ces bibliothèques ne sont pas disponibles sous Linux, mais pourraient être adaptées pour le devenir.

  2. L'utilisation directe des instructions MMX. C'est assez compliqué, pour deux raisons : D'abord, le MMX peut ne pas être disponible sur le processeur concerné, ce qui oblige à fournir une alternative. Ensuite, l'assembleur IA32 utilisé en général sous Linux ne reconnaît actuellement pas les instructions MMX.

  3. L'utilisation d'un langage de haut niveau ou d'un module du compilateur qui puisse directement générer des instructions MMX appropriées. Quelques outils de ce type sont actuellement en cours de développement, mais aucun n'est encore pleinement fonctionnel sous Linux. Par exemple, à l'Université de Purdue (http://dynamo.ecn.purdue.edu/~hankd/SWAR/), nous développons actuellement un compilateur qui admettra des fonctions écrites dans un « dialecte » explicite parallèle au langage C et qui générera des modules SWAR accessibles comme des fonctions C ordinaires, et qui feront usage de tous les supports SWAR disponibles, y compris le MMX. Les premiers prototypes de compilateurs à modules ont été générés à Fall, en 1996. Amener cette technologie vers un état réellement utilisable prend cependant beaucoup plus de temps que prévu initialement.

En résumé, le SWAR par MMX est toujours d'un usage malaisé. Toutefois, avec quelques efforts supplémentaires, la seconde approche décrite ci-dessus peut être utilisée dès à présent. En voici les bases :

  1. Vous ne pourrez pas utiliser le MMX si votre processeur ne le prend pas en charge. Le code GCC suivant vous permettra de savoir si votre processeur est équipé de l'extension MMX. Si c'est le cas, la valeur renvoyée sera différente de zéro, sinon nulle.

    inline extern
    int mmx_init(void)
    {
    	int mmx_disponible;
    
    	__asm__ __volatile__ (
    		/* Récupère la version du CPU */
    		"movl $1, %%eax\n\t"
    		"cpuid\n\t"
    		"andl $0x800000, %%edx\n\t"
    		"movl %%edx, %0"
    		: "=q" (mmx_disponible)
    		: /* pas d'entrée */
    	);
    	return mmx_disponible;
    }
    

  2. Un registre MMX contient essentiellement ce que GCC appellerait un unsigned long long. Ainsi, ce sont des variables de ce type et résidant en mémoire qui vont former le mécanisme de communication entre les modules MMX et le programme C qui les appelle. Vous pouvez aussi déclarer vos données MMX comme étant des structures de données alignées sur des adresses multiples de 64 bits (il convient de garantir l'alignement sur 64 bits en déclarant votre type de données comme étant membre d'une union comportant un champ unsigned long long).

  3. Si le MMX est disponible, vous pouvez écrire votre code MMX en utilisant la directive assemble .byte pour encoder chaque instruction. C'est un travail pénible s'il est abattu à la main, mais pour un compilateur, les générer n'est pas très difficile. Par exemple, l'instruction MMX PADDB MM0,MM1 pourrait être encodée par la ligne d'assembleur in-line GCC suivante :

    __asm__ __volatile__ (".byte 0x0f, 0xfc, 0xc1\n\t");
    
    Souvenez-vous que le MMX utilise une partie du matériel destiné aux opérations en virgule flottante, et donc que le code ordinaire mélangé au MMX ne doit pas invoquer ces dernières. La pile en virgule flottante doit également être vide avant l'exécution de tout code MMX. Cette pile est normalement vide à l'entrée d'une fonction C ne faisant pas usage de la virgule flottante.

  4. Clôturez votre code MMX par l'exécution de l'instruction EMMS, qui peut être encodée par :

    __asm__ __volatile__ (".byte 0x0f, 0x77\n\t");
    

Tout ce qui précède paraît rébarbatif, et l'est. Ceci dit, le MMX est encore assez jeune… de futures versions de ce document proposeront de meilleures techniques pour programmer le SWAR MMX.



[23] N.D.T. : initialement, huit liens étaient proposés à cet endroit, devenus pour la plupart obsolètes.

[24] N.D.T. : FLOP = « FLoating point OPeration », ou « OPération en Virgule Flottante ».

[25] N.D.T. : il s'agit en fait de simuler la ré-entrée par le coté opposé des données éjectées par le décalage. L'exemple qui suit permet d'implémenter ce cas en langage C, mais la plupart des microprocesseurs sont équipés d'instructions de rotation effectuant cela en une seule opération.

Hosting by: Hurra Communications GmbH
Generated: 2007-01-26 18:01:15