• Aucun résultat trouvé

La programmation générative

Dans le document Vers des noyaux de calcul intensif pérennes (Page 162-165)

La programmation générique permet de décrire des algorithmes abstraits pouvant s’appli-quer à différents conteneurs de données. Ces conteneurs correspondent aux différentes structures de données. La bibliothèque de patrons standard du C++ [73,74] (C++ Standard Template Li-brary ou STL) propose ainsi un large ensemble de conteneurs génériques ainsi que de nombreux algorithmes opérant sur ces conteneurs. Ainsi l’algorithme std::accumulate est une implémen-tation générique des différentes normes. Elle permet par exemple de passer de la norme L2 aux normes L−∞, L1 ou L en modifiant simplement la fonction qui permet d’effectuer l’accumu-lation des valeurs (pour la fonction norm présentée précédemment, cette opération est effectuée

ligne 5,page 144).

Les bibliothèques génériques qui prennent un rôle actif dans la compilation sont dites « ac-tives » par opposition aux simples collections passives de routines de calcul [75,76,77]. Dans sa thèse [77], Todd Veldhuizen fournit la définition suivante :

Une bibliothèque active est une bibliothèque qui fournit à la fois des abstractions propres à un domaine et les connaissances requises pour les optimiser et vérifier leurs exigences sémantiques. En outre, les bibliothèques actives sont composables : un même fichier source peut combiner l’utilisation de plusieurs d’entre elles.

Au contraire des bibliothèques traditionnelles ou génériques, les bibliothèques actives, comme Blitz++ [78], ne sont donc pas un simple agrégat de fonctions, de classes ou d’objets. Elles fournissent un haut niveau d’abstraction et sont capables d’optimiser ces abstractions en fonction du contexte. Elle prennent donc un rôle « actif » lors de la compilation et influencent la génération de code par le compilateur. La possibilité d’opérer des transformations dans le code permet à ces bibliothèques de se rapprocher des compilateurs. Dans l’article Active Libraries : Rethinking the roles of compilers and libraries [75], Todd Veldhuizen présente en détails les fondements des bibliothèques actives et explique comment l’utilisation de connaissances issues d’un haut niveau d’abstraction dans un domaine particulier permet d’effectuer des optimisations que le compilateur ne peut effectuer seul.

Appliquées à notre exemple, ces techniques permettent de supprimer les appels aux fonc-tions virtuelles. Nous allons pour cela utiliser un patron de conception permettant de résoudre le polymorphisme à la compilation. Cette technique, appelée Curiously Recurring Template Pat-tern (CRTP) [79,80] permet de résoudre lors de la compilation les liens d’héritage et donc de supprimer les différents pointeurs de fonction par des appels directs aux fonctions de la classe réelle de l’objet, voire de remplacer ces appels par le corps de ces fonctions. Nous dirons alors de ces fonctions qu’elles sont inlinées.

Le principe du CRTP consiste à paramétrer la classe abstraite par ses classes dérivées :

1 t e m p l a t e <c l a s s DERIVED > c l a s s B a s e { 2 ...

3 p r i v a t e:

4 i n l i n e c o n s t D E R I V E D & c o n v e r t () c o n s t { 5 r e t u r n *s t a t i c _ c a s t<c o n s t D E R I V E D * >(t h i s) ;

6 } 7 }; 8 ... 9 c l a s s D e r i v e d : p u b l i c Base < Derived >{ 10 ... 11 };

Laligne 9signifie que la classe Derived hérite de la classe Base<Derived>. Le CRTP fonctionne sur l’hypothèse qu’il n’y a que la classe Derived qui hérite de Base<Derived>. Grâce à cette hypothèse, toute instance de la classe Base<Derived> peut être convertie en une instance de la classe Derived. Dans l’exemple ci-dessus, la fonction convert (ligne 4) convertit l’instance courante de Base<DERIVED> en une instance de la classe DERIVED. Cette conversion permet en-suite aux autres méthodes de la classe Base<DERIVED> de faire directement appel aux fonctions de la classe DERIVED. Le mot-clé inline précédent la déclaration de la fonction convert en-courage le compilateur à inliner cette fonction. L’appel à cette fonction a donc généralement un coût nul.

Nous allons maintenant appliquer ceci aux classes d’expression vectorielles :

1 t e m p l a t e <c l a s s V E C T O R _ E X P R E S S I O N > c l a s s V e c t o r T e m p l a t e E x p r e s s i o n { 2 p u b l i c:

3 i n l i n e int s i z e () c o n s t { r e t u r n c o n v e r t () . s i z e () ; }

4 i n l i n e f l o a t get (c o n s t int i ) c o n s t { r e t u r n c o n v e r t () . get ( i ) ; } 5 p r i v a t e:

6 i n l i n e c o n s t V E C T O R _ E X P R E S S I O N & c o n v e r t () c o n s t { 7 r e t u r n *s t a t i c _ c a s t<c o n s t V E C T O R _ E X P R E S S I O N * >(t h i s) ;

8 }

9 };

Nous reconnaissons ici la classe VectorExpression qui a été adaptée pour permettre l’utilisa-tion du CRTP. Comme dans l’exemple introductif de la classe Base, une instance de la classe VectorTemplateExpression<VECTOR_EXPRESSION> peut être convertie en une instance de la classe VECTOR_EXPRESSION. Cette conversion permet d’appeler directement les fonctions size et get (lignes 3 et 4) qui ne sont plus virtuelles mais font directement appel aux implémenta-tions de la classe VECTOR_EXPRESSION. Ces foncimplémenta-tions peuvent maintenant être inlinées par le compilateur. Appeler une de ces fonctions est donc strictement équivalent à appeler directement l’implémentation de la classe VECTOR_EXPRESSION.

Nous pouvons maintenant écrire une version générique de la fonction norm afin de prendre en compte ces modifications :

1 t e m p l a t e <c l a s s VE > 2 f l o a t n o r m (c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < VE > & ve ) { 3 f l o a t out = 0; 4 for(int i =0; i < ve . s i z e () ; ++ i ) { 5 f l o a t tmp = ve . get ( i ) ; 6 out += tmp * tmp ; 7 } 8 r e t u r n s q r t f ( out ) ; 9 }

Cette fonction est paramétrée par le type réel VE de l’expression vectorielle ve. Lors de l’appel à cette fonction, le compilateur instanciera automatiquement norm en fonction du type de ve. Le typage spécifique VectorTemplateExpression<VE> de ve permet d’empêcher le compilateur d’instancier cette fonction avec un argument incompatible. Le reste de la fonction n’a pas besoin d’être modifiée.

Pour les classes, les modifications à apporter sont légèrement plus importantes. Nous allons prendre l’exemple de la classe ScalVectorTemplateExpr :

1 t e m p l a t e <c l a s s VE > c l a s s S c a l V e c t o r T e m p l a t e E x p r 2 : p u b l i c V e c t o r T e m p l a t e E x p r e s s i o n < S c a l V e c t o r T e m p l a t e E x p r < VE > >{ 3 p u b l i c: 4 i n l i n e S c a l V e c t o r T e m p l a t e E x p r ( c o n s t f l o a t & a 5 , c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < VE >& x ) 6 : a_ ( a ) , x_ ( x ) {} 7 i n l i n e int s i z e () c o n s t { r e t u r n x_ . s i z e () ; };

8 i n l i n e f l o a t get (c o n s t int i ) c o n s t { r e t u r n a_ * x_ . get ( i ) ; }; 9 p r i v a t e:

10 c o n s t f l o a t a_ ;

11 c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < VE > & x_ ; 12 };

La classe ScalVectorTemplateExpr prend en charge le produit d’une expression vectorielle par un scalaire. Elle doit donc contenir une référence vers cette expression vectorielle d’une part et ledit scalaire d’autre part. Dans la version précédente, il suffisait de contenir une référence vers une instance de la classe abstraite VectorExpression. Cependant, nos différentes expressions vectorielles n’héritent plus de la même classe. Il faut donc paramétrer ScalVectorTemplateExpr par le type réel de l’expression vectorielle, que nous nommons VE à laligne 1. Naturellement, la classe ScalVectorTemplateExpr<VE> représente une expression vectorielle et hérite donc de la classe VectorTemplateExpression<ScalVectorTemplateExpr<VE> >.

Lorsque le patron de fonction norm reçoit comme argument une instance ve de la classe ScalVectorTemplateExpr, le compilateur spécialise ce patron. L’objet ve est alors perçu comme une instance de la classe VectorTemplateExpression<ScalVectorTemplateExpr<VE> >. L’ap-pel à la fonction VectorTemplateExpression<ScalVectorTemplateExpr<VE> >::get(i) est alors remplacé par le compilateur par un appel à la fonction ScalVectorTemplateExpr::get(i). Cet appel de fonction sera à son tour remplacé par le code équivalent. Tous les appels de fonction intermédiaires étant inlinés, le code résultant est donc équivalent au code C.

Comme pour la bibliothèque présentée dans la section précédente, la surcharge des opérateurs permet de simplifier l’utilisation de la bibliothèque. Des patrons de fonctions sont donc mis en œuvre afin de surcharger les opérateurs :

1 t e m p l a t e <c l a s s L , c l a s s R > 2 A d d V e c t o r T e m p l a t e E x p r <L , R > o p e r a t o r+ 3 ( c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < L > & l e f t 4 , c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < R > & r i g h t 5 ) 6 { 7 r e t u r n A d d V e c t o r T e m p l a t e E x p r < L , R >( left , r i g h t ) ; 8 } 9 10 t e m p l a t e <c l a s s V E C T O R _ E X P R E S S I O N > 11 S c a l V e c t o r T e m p l a t e E x p r < V E C T O R _ E X P R E S S I O N > o p e r a t o r* 12 ( c o n s t f l o a t & a 13 , c o n s t V e c t o r T e m p l a t e E x p r e s s i o n < V E C T O R _ E X P R E S S I O N > & x 14 ) 15 { 16 r e t u r n S c a l V e c t o r T e m p l a t e E x p r < V E C T O R _ E X P R E S S I O N >( a , x ) ; 17 }

Le compilateur prendra en charge la détermination de ces types et instanciera la spécialisation de ces deux fonctions. L’interface utilisateur est donc la suivante :

0 1 2 3 4 5 6 7 Puissance de calc ul (GFLOPS) C MKL Séquentielle Haskell Ocaml MATLAB BOO BA C optimisé MKL Parallèle Haskell Parallèle BOO optimisée BA optimisé

Figure A.2 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de tests2×432Nehalem pour des vecteurs de plus de 3 × 106 éléments.

1 V e c t o r T e m p l a t e w ( n ) ,x ( n ) ,y ( n ) ; 2 f l o a t a , b , c ;

3 ...

4 f l o a t r e s u l t = n o r m ( a * w + b * x + c * y ) ;

Lorsque le compilateur arrive à laligne 4, il doit analyser l’expression a*w+b*x+c*y. L’ordre de précédence des opérateurs le conduit appeler trois fois la fonction operator* spécialisée avec le type VectorTemplate pour construire aW, bX et cY. Il va ensuite appeler deux fois la fonction operator+ pour construire successivement aW_bX et aW_bX_cY. Ce mécanisme d’interprétation des opérateurs pour générer un AST de haut niveau lors de la compilation est communément appelé expression templates [255,256]. L’objet aW_bX_cY étant construit, la fonction norm pourra être appelée sur cet objet. Le compilateur connaissant toutes les fonction appelées, ces dernières peuvent être inlinées. Cette ligne de code est donc équivalente à la boucle C présentée en début de chapitre. L’article [60] montre comment paralléliser les expression templates en s’appuyant sur la bibliothèque INTEL TBB [139].

La mesure « BA » de la figure A.2 montre les performances obtenues par cette implémenta-tion. Notons tout d’abord que le compilateur a été capable de vectoriser cette implémentaimplémenta-tion. L’implémentation présentée ci-dessus obtient les mêmes performances que l’implémentation C en séquentiel (3,6 GFlops) comme en parallèle (6.5 GFlops). Nous avons mis au point deux versions optimisées : une version vectorisée par le compilateur tandis que nous avons vectorisé la seconde nous-même. Dans les deux cas, les performances obtenues sont identiques.

Dans le document Vers des noyaux de calcul intensif pérennes (Page 162-165)