Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

Instructions SIMD sur les réels

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

SIMD est l'acronyme de Single Instruction Multiple Datas (une seule instruction, plusieurs données). Ceci recouvre en fait plusieurs jeux d'instructions qui ont été ajoutés aux microprocesseurs Intel et compatibles ces dernières années. Les instructions MMX ont été les premières de la série en permettant d'effectuer simultanément la même instruction sur plusieurs nombres entiers. Elles ont déjà été évoquées dans deux articles, ici et . Nous allons maintenant étudier les instructions SIMD concernant les nombres réels. Il s'agit des instructions SSE et SSE2 chez Intel ainsi que 3DNow! chez AMD. Ces instructions sont prises en charges par l'assembleur intégré (basm) de Delphi 6 et de Kylix. Donc, il faut abandonner temporairement le Pascal pour l'assembleur afin de les utiliser.

Nous aurons recours à un exemple simple, le calcul du milieu de deux points dans l'espace. Chaque point dispose de trois coordonnées x, y et z et nous devons juste faire la moyenne des coordonnées une à une pour trouver le milieu :

Image non disponible

Ce qui se traduit en Pascal par:

 
Sélectionnez
type 
  TZPoint = record 
    X, Y, Z : Single; 
  end; 

function MidPoint(const P1, P2: TZPoint): TZPoint; 
const 
  Demi : single = 0.5; 
begin 
  Result.X:= (P2.X+P1.X)*Demi; 
  Result.Y:= (P2.Y+P1.Y)*Demi; 
  Result.Z:= (P2.Z+P1.Z)*Demi; 
end;

Nous avons ici le code le plus optimisé possible en Pascal, en particulier la multiplication par 0,5 plutôt que la division par 2 permet de gagner beaucoup de temps. Le recours à une constante permet aussi d'améliorer le code généré. L'utilisation de l'assembleur classique, en supprimant les FWAIT à la fin des lignes de calcul n'apporte pas de gain important. Que reste-t-il pour aller plus vite? Les instructions SIMD! Nous allons commencer par le 3DNow!.

II. 3DNow!

Ces instructions sont disponibles sur les microprocesseurs AMD depuis le K6-2. Il faut aller sur le site web d'AMD pour trouver la description des instructions. Les instructions 3DNow! utilisent les registres MMX. Il s'agit de registres 64 bits qui peuvent ainsi contenir chacun deux nombres réels simple précision. Et les instructions 3DNow! permettent ensuite de réaliser simultanément deux fois la même opération. En pratique, nous devons transférer nos données vers les registres MMX, effectuer les opérations, récupérer les données et désactiver les registres MMX. Comme je ne dispose de machine correspondante, Matthijs Laan a fait le travail de codage pour moi :

 
Sélectionnez

function MidPoint_3DNow(const P1, P2: TZPoint): TZPoint; 
const 
  PackedHalf: packed array[0..1] of Single = (0.5, 0.5); 
asm 
        movq    mm0, [PackedHalf]    // chargement de la constante précédente 
        movq    mm1, [eax]          // chargement de P1.X et P1.Y 
        pfadd   mm1, [edx]           // addition de P2.X et P2.Y 

        movd    mm3, [eax + 8]       // chargement de P1.Z 
        pfadd   mm3, [edx + 8]       // addition de P2.Z 

        pfmul   mm1, mm0            // multiplication résultat sur X et Y par 0.5 
        pfmul   mm3, mm0            // multiplication résultat sur Z par 0.5 

        movq    [ecx], mm1          // déchargement de X et Y 
        movd    [ecx + 8], mm3      // déchargement de Z 

        emms                     // désactivation de l'unité MMX 
end;

Le codage est aisé grâce à Delphi 6 qui reconnaît les instructions 3DNow!. Ça fonctionne et nous étudierons les performances à la fin de cet article en commun avec les autres jeux d'instructions.

III. SSE

Les instructions SSE sont apparues chez Intel avec le Pentium III puis ont été étendues au Celeron à partir du 533A. AMD les a portées sur ces microprocesseurs à partir de l'Athlon XP. Avec ces instructions, 8 nouveaux registres 128 bits ont été ajoutés au microprocesseur. Ils sont nommés de xmm0 à xmm7 et peuvent contenir 4 nombres réels simple précision. Nous pouvons ensuite réaliser quatre opérations simultanément. Néanmoins, Intel nous a réservé une mauvaise surprise en ne mettant, sur les PIII, qu'un bus 64 bits pour accéder aux registres xmm. Il faut donc deux instructions pour charger ou décharger un registre xmm :

 
Sélectionnez
  movlps xmm0,[eax]     //chargement de la partie basse de xmm0 avec P1 
  movhps xmm0,[eax+8]   //chargement de la partie haute de xmm0 avec P1 

Ensuite, certaines instructions comme les opérations (addition, multiplication ...) mettant en jeu un registre et une adresse mémoire exigent que cette adresse mémoire soit alignée sur 16 octets (=128 bits). Or le compilateur de Delphi aligne les données sur 4 octets (=32 bits) (Ref : Guide du langage Pascal Objet, Gestion de la mémoire). Première solution, ne pas utiliser les instructions à problème et trouver des méthodes de contournement. En terme de performance, c'est mauvais et mieux vaut ne pas utiliser les instructions SSE. Deuxième méthode, aligner les données sur 16 octets en remplaçant le gestionnaire de mémoire par défaut par le nôtre que nous avons pris soin de programmer dans cette optique. Pour ceux qui ne sont pas très familiers avec les gestionnaires de mémoire :-), Robert Lee a déjà fait le travail pour vous sous Delphi. Prenez son gestionnaire de mémoire et utilisez le dans votre projet en l'appelant en premier :

 
Sélectionnez

program Project1; 
uses 
  MultiMM,      // ici, le nouveau gestionnaire de mémoire 
  Forms, 
  Unit1 in 'Unit1.pas' {Form1}; 

Mais ce n'est pas suffisant car le nouveau gestionnaire de mémoire n'aligne que les variables que vous allouez dynamiquement. Et pour allouer une variable, rien de tel que new :

 
Sélectionnez

  T4DPoint = packed record  // définition d'un format de 16 octets 
    X, Y, Z, T : Single; 
  end; 
const 
  CPackedHalf: T4DPoint = (x:0.5; y:0.5; z:0.5; t:0.5);  // constante de multiplication 

var 
  Serie1bis, Serie2bis, Rapidebis, PackedHalf : ^T4DPoint; 
begin 
[...] 
    new(Serie1bis);  // allocation dynamique des variables 
    new(Serie2bis); 
    new(Rapidebis); 
    new(PackedHalf);  // allocation dynamique d'une variable pour la constante 
    PackedHalf^:=CPackedHalf;  // transfert de la constante vers une variable alignée 
[...] 
    MidPoint_SSE(Serie1bis^,Serie2bis^,Rapidebis^); // appel de la procédure de calcul 
[...] 
    dispose(Serie1bis);  // désallocation dynamique des variables 
    dispose(Serie2bis); 
    dispose(Rapidebis); 
    dispose(PackedHalf); 
end;

Nous pouvons enfin passer au calcul proprement dit :

 
Sélectionnez

procedure MidPoint_SSE(const P1, P2 : T4DPoint; var result : T4DPoint); 
asm 
  movlps xmm0,[eax]     // chargement de la partie basse de xmm0 avec P1 
  movhps xmm0,[eax+8]   // chargement de la partie haute de xmm0 avec P1 
  mov eax,[PackedHalf]  // récupération de l'adresse de la constante 

  addps xmm0,[edx]      // addition de P2 à xmm0 (P1) 

  mulps xmm0,[eax]      // multiplication du résultat par 0.5 

  movlps [ecx],xmm0     // déchargement de la partie basse de xmm0 avec P1 
  movhps [ecx+8],xmm0   // déchargement de la partie haute de xmm0 avec P1 
end;

Le calcul est simple, fonctionne et permet des gains de temps significatifs.

IV. SSE2

Les instructions SSE2 sont apparues avec le Pentium 4. Elles devraient être portées par AMD sur les Hammer (génération K8) à la mi-2003. Mais mon exemple fonctionne déjà sur les Athlon XP. Une partie des instruction SSE2 semble donc supportée par ce dernier. Outre l'introduction de nouvelles possibilités de calcul, elles correspondent à l'arrivée d'un bus 128 bits pour les registres xmm. Il devient donc possible de charger un registre xmm à l'aide d'une seule instruction, ce qui simplifie le code SSE sans changement par ailleurs :

 
Sélectionnez

procedure MidPoint_SSE2(const P1, P2 : T4DPoint; var result : T4DPoint); 
asm 
  movaps xmm0,[eax] 
  mov eax,[PackedHalf] 
  addps xmm0,[edx] 
  mulps xmm0,[eax] 
  movaps [ecx],xmm0 
end; 

Lorsque vous avez déjà écrit votre code SSE, le passage au SSE2 est très facile et consiste à simplifier le code. Il ne faut donc pas s'en priver.

V. Performances

Venons en aux performances. J'ai mis dans le tableau suivant le temps moyen en nombre de cycle d'horloge pour calculer le milieu de deux points :

Architecture Proc / fréquence Pascal asm 1 (perso) asm 2 (Barry Kelly et al .) 3DNow ! (Matthijs Laan et al.) SSE (perso) SSE2 (perso) Puissance effective / MFlops
Intel P5 Pentium 90 38.5 35.5 32.6 X X X 17
  P 200 MMX 51.2 51.2 63.3 X X X 23
                 
Intel P6 Celeron 333@375 18.0 16.8 16.7 X X X 138
  P III 500@560 18.1 17.0 17.1 X 12.4 X 270
  P III 600 18.1 16.8 17.2 X 12.3 X 290
  P III 633 18.2 17.0 17.0 X 12.3 X 310
  P III 733 18.6 17.2 17.0 X 12.1 X 360
  P III 1 GHz 18.6 17.0 17.0 X 12.1 X 500
                 
Intel P7 P4 1.5 19.8 16.0 18.2 X 20.1 12.1 740
                 
AMD K6 K6-2 333 52.3 45.6 60.7 22.3 X X 90
                 
AMD K7 Duron 800 12.0 12.5 15.0 12.1 13.0 13.0 10.4 10.1 X X 460 480
  Athlon 700 13.3 12.0 12.0 9.3 X X 450
  Athlon 1.0 14.8 12.0 15.0 10.0 X X 600
  Athlon 1.1 12.0 12.0 13.0 10.0 X X 660
  Athlon 1.35 12.1 12.0 13.0 10.0 X X 810
  Athlon XP 1600 + 13.0 12.0 15.0 10.0 10.1 10.1 840

Pour les microprocesseurs de la famille Intel P6 avec instructions SSE, le gain est de 30 % en temps d'exécution, ce qui peut justifier le recours aux instructions SSE. Avec un Pentium 4, les instructions SSE sont pénalisantes et seul le recours au SSE2 permet un gain de 40%. Vu comme ça, le SSE2 ressemble à une arnaque qui rend obsolète le code SSE écrit pour les PIII et vous oblige à passer au SSE2 pour rattraper les anciennes performances.

Un tour chez AMD montre d'abord que le 3DNow! est très performant avec les K6 (gain 57 %). A l'opposé, avec la nouvelle architecture (Athlon et Duron), les gains sont faibles et l'opportunité d'écrire un code spécial 3DNow! se pose.

Enfin, la comparaison entre Intel et AMD est toute à l'avantage de ce dernier avec un code en Pascal aussi performant que celui optimisé SSE/SSE2 chez Intel. Ce constat est d'ailleurs confirmé par pas mal de tests sur Internet. Il ne faut toutefois pas perdre de vue que nous avons travaillé ici sur un exemple un peu fictif. Celui ci utilise peu les registres. Or, les instructions SSE disposent de 8 registres 128 bits contre 8 registres 64 bits pour 3DNow!. Avec le SSE, on peut alors envisager de stocker une matrice 4x4, très utile en 3D, et conserver l'autre moitié des registres disponibles pour faire passer les vecteurs à multiplier par la matrice. Ceci est impossible avec le 3DNow! et imposera de nombreux transferts en mémoire. De plus, le 3DNow! bloque l'unité FPU classique contrairement au SSE. Peut-être qu'un jour j'écrirai un article sur l'utilisation en conditions réelles des instructions SIMD :)))

VI. Kylix

Si l'assembleur intégré de Kylix supporte lui aussi les nouveaux jeux d'instructions SIMD, il n'existe par contre pas de gestionnaire de mémoire adapté aux instructions SSE et SSE2. Seules les instructions 3DNow! pourront être utilisées sous Linux.

VII. Conclusion

Nous avons vu dans cet article comment utiliser les instructions 3DNow!, SSE et SSE2. Leur mise en œuvre est simplifiée par l'assembleur de Delphi 6 qui reconnaît les mnémotechniques correspondants. Les gains en performances sont surtout intéressant pour le SSE et le SSE2.

Code source (13 ko, y compris le gestionnaire de mémoire)

Liste de mes articles :
Types énumérés, intervalle et ensemble
MMX avec Delphi 6 / Kylix
Préchargement de données dans le cache
MMX ( 2 ) avec Delphi 6 / Kylix
Instructions SIMD sur les réels
Internationaliser un projet Delphi
Installer D6 sous Windows 95
Tramage d'une image
Coder le PNG soi-même
Utiliser LibTiff avec Delphi
Ecrire une UDF FireBird avec Kylix

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.