Professional Documents
Culture Documents
programmation parallèle
2 jocelyn.serot@univ-bpclermont.fr
LASMEA - UMR 6602 - Campus des Cézeaux - 24, Avenue des Landais
F 63177 Aubiére cedex
RÉSUMÉ. Cet article présente une implémentation efficace d’un modèle de programmation pa-
rallèle fondé sur la notion de squelettes, c-à-d. de constructeurs de haut niveau encapsulant
des formes récurrentes de parallélisme. Cette implantation s’appuie sur des techniques de mé-
taprogrammation pour éliminer le surcoût classiquement observé avec les approches de type
squelette. On démontre d’une part la faisabilité et l’efficacité d’une telle approche lorsque le
langage hôte est C++ et comment la bibliothèque obtenue permet de concilier abstraction et
performance dans le domaine de la programmation parallèle.
ABSTRACT. This paper introduces an efficient implementation for a programming model based on
algorithmic skeletons, i.e higher order functions encapsulating recurring parallelism pattern.
This implementation relies on metaprogramming techniques to get rid of the classic overhead
observed with this model et provide a way to make parallel programming able to take advantage
of both abstraction and performances .
MOTS-CLÉS : Programmation parallèle, Métaprogrammation, Squelettes algorithmiques.
KEYWORDS: Parallel programming, Metaprogramming, Algorithmic skeletons.
1. Introduction
Les approches à base de squelettes (Cole, 1989; Darlington et al., 1993; Cole,
1999; Cole, 2004) ont été introduites en vue de résoudre ce problème, pour les archi-
tectures MIMD à passage de messages notamment. Elles sont fondées sur le fait que
la mise en œuvre du parallélisme sur de telles architectures se fait très fréquemment
en utilisant un faible nombre de schémas génériques récurrents, que le programmeur
ne fait qu’instancier pour un problème particulier. Ces schémas explicitent les cal-
culs menés en parallèle et les interactions entre ces calculs. La notion de squelette
correspond alors à tout ce qui, dans ces schémas, permet de contrôler les activités de
calcul spécifiques de l’application. Un squelette algorithmique prend donc en charge
un schéma de parallélisation précis. Ainsi, toutes les opérations bas-niveau néces-
saires à l’exécution de la forme de parallélisme choisie sont contenues dans le code du
squelette et peuvent donc être optimisées pour une architecture ou une classe d’archi-
tecture. L’utilisateur paramètre le squelette en fournissant les fonctions de calcul de
son algorithme. Ces fonctions seront alors exécutées en parallèle et les données transi-
teront entre elles selon le schéma qu’encapsule le squelette. En un sens, les squelettes
sont à la programmation parallèle ce que la programmation structurée fut à la pro-
grammation séquentielle à ses débuts. Ils permettent de restreindre la complexité de la
programmation parallèle en limitant délibérément l’expression du parallélisme à des
formes clairement identifiées et ce sans compromettre l’efficacité (Ginhac, 1999; Gor-
latch, 2004) a priori.
Métaprogrammation pour le parallélisme 647
sous-éléments sont distribués à n processus esclaves dont les résultats sont fusionnés.
D’un point de vue purement applicatif, si fs (n, x) = hx1 , . . . , xn i, on a :
Seq(f, x) = f (x)
Σ ::= Seq f
| Pipe Σ1 . . . Σn
| Pardo Σ1 . . . Σn
| Farm n Σ
| Scm n fs Σ fm
f, fs , fm ::= fonctions séquentielles
n ::= entier ≥ 1
f FarmM h
g g g
2. La gestion de l’arrêt des processus complique la description du modèle et nous avons choisi
de ne pas détailler ce point ici.
Métaprogrammation pour le parallélisme 651
nance du processus dont le pid apparait en premier dans I(p) et écrit cette donnée
dans la variable iv. L’instruction Comp effectue un calcul en appelant une fonction
séquentielle dont l’identifiant est fid. L’argument de cette fonction est lu dans iv et son
résultat écrit dans ov. L’instruction RecvFromAny attend (de manière non détermi-
niste) une donnée del’ensemble des processus dont les pids apparaissent dans I(p).
La donnée reçue est placée dans la variable iv et le pid du processus émetteur est
placé dans la variable q. L’instruction SendToQ envoie le contenu de la variable ov au
processus dont le pid est donné dans la variable q. L’instruction Ifq compare la valeur
contenue dans la variable q au pid listé en premier dans I(p). En cas d’égalité, la
séquence d’instructions instrs1 est exécutée ; sinon instrs2 est exécutée. L’instruction
UpdateWs lit la variable q et met à jour la variable iws en conséquence. La variable
iws maintient la liste des processus esclaves libres pour un processus maître dans le
cas d’un squelette de type FARM. L’instruction GetIdleW extrait un identificateur de
processus de la liste iws et le place dans la variable q. Conjointement, ces deux der-
nières instructions encapsulent la stratégie utilisée au sein du squelette FARM pour
assurer l’équilibre de charge. Les instructions Gather et Scatter gèrent quant à elle
la diffusion et la récupération des parties de la liste de sous-éléments nécessaire au
squelette SCM3
δ∈∆ l = N EW()
(S INGL)
dδe = h{(l, h[], [], δi)}, {l}, {l}i
3. Comme leur nom l’indique, ces instructions sont largement basées sur les fonctions MPI
éponymes.
652 RSTI - TSI. Volume 28 – n° 5/2009
πi = hPi , Ii , Oi i (i = 1, 2)
(PAR)
π1 k π2 = hP1 ∪ P2 , I1 ∪ I2 , O1 ∪ O2 i
où Φ(p, pss , psd ) = Φs (Φd (p, psd ), pss ) et Φs (resp. Φd ) est la généralisation de la
fonction φs (resp. φd ) à une liste de processus :
C[[Seq f ]] = d∆(f )e
C[[Pardo Σ1 . . . Σn ]] = C[[Σ1 ]] k . . . k C[[Σn ]]
C[[Pipe Σ1 . . . Σn ]] = C[[Σ1 ]] • . . . • C[[Σn ]]
C[[Farm n Σ]] = d∆(FarmM)e o
n (C[[Σ]]1 k . . . k C[[Σ]]n )
C[[Scm n fs Σ fm ]] = d∆(ScmMfs fm )e o
n (C[[Σ]]1 k . . . k C[[Σ]]n )
Où :
– ∆ est une fonction qui permet de construire un descripteur à partir d’une fonction
utilisateur :
∆(f ) = h[Comp f], Regulari
– FarmM est le descripteur encapsulant le comportement d’un processus maître au
sein d’un squelette FARM :
∆(FarmM) = h[RecvFromAny; Ifq [GetIdleW; SendToQ] [UpdateWs; SendTo]], FarmMi
– ScmM est une fonction d’ordre supérieur générant le processus maître au sein d’un sque-
lette SCM :
3. Principe de la métaprogrammation
De manière plus formelle, on considère qu’un programme est une fonction Φ qui
s’applique à la fois à des entrées définies au moment de la compilation et des entrées
définies au moment de l’exécution afin de produire un résultat à l’exécution.
Φ : (Istatic , Idynamic ) → O
Γ : (Φ, Istatic ) → Φ∗
Φ ≡ Φ∗ : (Idynamic ) → O
Ici, cette définition ne génère pas de code compilable en soit. C’est seulement à
l’instanciation de cette fonction (en fournissant une valeur concrète pour N) que le
code équivalent sera compilé. Ainsi, l’appel :
int val = f<16>();
PIPE
Génération Production φ 2
Génération
AST du RPSC φ 2 φ 2 de code C+MPI
C
C++
φ1 FARM3 φ3 MPI
φ 1 f φ 3
φ2
Pour chaque squelette, un constructeur explicite est fourni par l’API de QUAFF
sous la forme d’une fonction. Cette fonction génère une valeur dont le type repré-
sente, au sein de l’arbre de syntaxe abstraite (AST) décrivant l’application, le squelette
associé. Par exemple, les fonctions seq et pipeline se définissent ainsi :
658 RSTI - TSI. Volume 28 – n° 5/2009
struct nil ;
Ces structures, définies donc comme de simples types templates, encodent stati-
quement au sein de définitions de types internes les caractéristiques de chacun de ces
objets. Techniquement parlant, l’ensemble de ces éléments est représenté par des listes
de types, manipulées à l’aide de la bibliothèque BOOST::MPL (Abrahams et al., 2004).
La fonction run doit ensuite convertir le type décrivant l’arbre de syntaxe abstraite
généré précédemment en une structure de donnée représentant le graphe de proces-
sus associé, c’est-à-dire implémenter la fonction C. Pour ce faire, run se contente
d’extraire le type de son paramètre et de le transmettre à la métafonction convert,
laquelle réalise la mise en correspondance de la structure de l’arbre avec les différents
cas exprimés par les règles de production de la sémantique (cf. listing 7). Cette mise
en correspondance (pattern matching) s’implante en C++ en utilisant le mécanisme
de spécialisation partielle des templates.
donnée dans le programme 9. Cette structure template prend en paramètres les types
représentant deux graphes de processus et génère le graphe résultant de leur mise
en série. Ce graphe est construit itérativement par le calcul de types intermédiaires
représentant les différents éléments nécessaires à sa définition.
typedef typename mpl :: transform < proc1 , phi_d < _1 , o1 , i2 > >:: type np1 ;
typedef typename mpl :: transform < proc2 , phi_s < _1 , i2 , o1 > >:: type np2 ;
typedef typename mpl :: copy < np2 , mpl :: back_inserter < np1 > >:: type process ;
Les fonctions transform et copy, fournies par la bibliothèque BOOST: MPL, per-
mettent d’appliquer une fonction f à une liste de types et de concaténer les éléments
d’un conteneur, respectivement. phi_s et phi_d sont quant à elles les équivalents
méta-programmés des fonctions du même nom présentées précédemment.
Function mFunction ;
};
Le code finalement généré pour l’exemple du pipeline à deux étages est donné
dans le listing 125 . On remarque que ce code est très similaire à celui qu’aurait écrit
un programmeur habitué à une formulation bas niveau avec MPI.
5. Résultats expérimentaux
Le protocole de test utilisé consiste à effectuer des mesures sur 1 000 à 10 000 exé-
cutions en utilisant des tâches séquentielles « synthétiques » dont la durée d’exécution
est connue et paramétrable, et ce pour l’ensemble des squelettes parallèles proposés
par QUAFF .
Pour le squelette Pipeline, on mesure les écarts de débit et de latence entre un Pipe-
line équilibré de N étages et son équivalent MPI pour N allant de 2 à 10 et des fonc-
tions dont le temps d’exécution varie entre 0, 001s et 0, 5s. Pour la latence, le surcout
reste toujours inférieur à 3 ‰ quel que soit le nombre d’étages du pipeline. Pour le
débit, les mesures du surcoût induit par QUAFF sont indiquées dans le tableau 1. On
note que le surcoût reste toujours inférieur à 1.53 %, ce qui est très satisfaisant.
5. Une version annotée de ce code est récupérable via l’option -fdump-tree-all-all de gcc.
Nous présentons ici sa version reconstituée et expurgée des indications supplémentaires fournies
par le compilateur.
664 RSTI - TSI. Volume 28 – n° 5/2009
Pour le squelette Farm, nous nous plaçons dans le cas où les temps de communica-
tions sont constants (tous les éléments du flux ont une taille similaire) et où le temps de
calcul ne dépend que de la donnée d’entrée. Nous cherchons alors à évaluer le rapport
entre le temps de mise à disposition – c’est-à-dire le temps écoulé entre l’arrivé d’un
élément du flux à l’entrée du squelette et sa disponibilité en sortie de ce dernier – d’un
squelette farm fourni par QUAFF et son implantation MPI en faisant varier le temps
de calcul des processus esclaves (entre 0, 001s et 0, 5s) ainsi que le nombre d’esclaves
(entre 2 à 10). Le surcoût induit par QUAFF reste inférieur à 2 % quels que soient la
durée d’exécution des fonctions séquentielles et le nombre de processeurs utilisés.
τc N=2 N=3 N=4 N=5 N=6 N=7 N >= 8
1 ms 1,71 % 1,35 % 1,12 % 0,93 % 0,61 % 0,60 % 0,56 %
10 ms 0,46 % 0,38 % 0,36 % 0,33 % 0,31 % 0,29 % 0,28 %
50 ms 0,11 % 0,11 % 0,10 % 0,10 % 0,09 % 0,09 % 0,09 %
100 ms 0,07 % 0,07 % 0,07 % 0,05 % 0,06 % <1‰ <1‰
500 ms 0,03 % 0,02 % 0,01 % 0,02 % <1‰ <1‰ <1‰
Pour le squelette SCM, nous nous plaçons dans un cas où le temps de calcul de
chaque processus esclave est fixé et varie entre 0, 001s et 0, 5s. Nous mesurons alors
l’écart entre le ratio calcul/communication de l’implantation QUAFF et du code MPI
équivalent pour un nombre de processus esclaves allant de 2 à 10. Ici, le surcoût de-
vient plus élevé mais reste dans une fourchette acceptable de 2 à 3 %.
Globalement, ces résultats montrent que le surcoût introduit par QUAFF est très
faible (de l’ordre de 3 %) et donc que les objectifs annoncés dans l’introduction sont
atteints. Un profiling fin du code montre que ce surcoût peut être attribué d’une part
aux communications qui, contrairement à un code MPI écrit à la main, utilisent des mé-
thodes de gestion génériques des transferts de données, et, d’autre part au démarrage
des processus de gestion des squelettes.
Métaprogrammation pour le parallélisme 665
lytiquement. Il faut alors avoir recours soit à des formes paramétriques des densités de
probabilités, comme le filtre de (Kalman, 1960) ou sa version étendue, soit à des tech-
niques numériques comme, par exemple, les méthodes dites de Monte-Carlo, basées
sur des tirages aléatoires.
Le filtre à particules (Isard et al., 1998; Arulampalam et al., 2002; Teulière et
al., 2003) est une méthode de Monte-Carlo séquentielle visant à estimer l’état d’un
système à variables cachées modélisé par un processus markovien. Le principe gé-
néral du filtre à particules réside dans l’estimation de la densité de probabilité a
posteriori P (Xt |Z1:t ) via un ensemble d’échantillons pondérés — ou particules —
{(Xnt , πtn ) : n = 1, .., N } et une fonction d’observation P (Zn |Xn ). La mise en
œuvre de cet algorithme est relativement simple et ne nécessite que la définition d’un
modèle de l’état de l’objet suivi, d’un modèle décrivant son évolution, d’une fonction
d’observation et une stratégie d’initialisation. Les détails de cette mise en œuvre sont
donnés dans (Falcou et al., 2006).
approche intégrant dans le vecteur d’état les N personnes à détecter et à suivre (Isard
et al., 2001), il faudrait de l’ordre de 2 000 particules pour suivre seulement deux per-
sonnes. Ceci explique pourquoi nous donnons les temps d’exécution jusqu’à 10 000
particules dans le tableau 5 et justifie d’autant plus une implémentation parallèle.
int main ()
{
QUAFF :: Init ( argc , argv ) ;
run ( scm < NB_PROC >( seq_2 ( Sample , Generate ) ,
seq ( Mesure ) ,
seq_2 ( Update , Display ) )
);
QUAFF :: Finalize () ;
}
Elle met en évidence un certain nombre de points. Tout d’abord, les étapes de gé-
nération des particules, de mise à jour et d’estimation ne représentent que 0,5 % du
temps d’exécution total. Ensuite, l’étape de mesure représente l’essentiel du temps
total de l’exécution de l’application. Elle concentre en effet un grand nombre de trai-
tements — principalement les projections des particules dans les images et le calcul
des scores de vraisemblance. De même, l’étape d’échantillonnage est une étape dont
l’impact sur le temps d’exécution ne devient sensible que pour de grands ensembles
de particules car son temps d’exécution varie comme le carré du nombre de particules.
Par contre, l’étape de prétraitement, qui consiste à précalculer les images intégrales
nécessaires à l’étape de mesure, représente une portion de moins en moins importante
du temps d’exécution. Elle ne dépend en effet que de la taille des images. À la lumière
668 RSTI - TSI. Volume 28 – n° 5/2009
de cette analyse, il apparaît que seule l’étape de mesure est avantageusement parallé-
lisable en distribuant l’ensemble des particules sur l’ensemble des nœuds disponibles,
via un squelette de type SCM. Au final, le code QUAFF correspondant à cet algorithme
est donné dans le listing 13
5.2.4. Résultats
Le tableau 7 présente les résultats temporels de l’algorithme de suivi parallèle pour
des jeux de particules allant de 1 000 à 10 000 éléments. Les résultats donnés sont : le
temps de calcul effectif, le gain par rapport à la version séquentielle et l’efficacité.
6. Travaux similaires
Nous avons déjà positionné QUAFF par rapport aux réalisations existantes dans le
domaine de la programmation parallèle par squelettes dans la section 1. Par rapport à
ces réalisations, la principale originalité est ici d’offrir un compromis abstraction/effi-
cacité inédit, et ce grâce au recours à la métaprogrammation.
A notre connaissance, la voie suivie par QUAFF a été très peu explorée. La seule
réalisation notable semble être les travaux de (Herrmann, 2005) qui décrit un système
similaire au notre mais fondé sur les facilités de méta-programmation dynamique of-
fertes par le langage MetaOcaml (Taha, 2003). Le DSL proposé par Hermmann diffère
toutefois de celui présenté ici en plusieurs points. Premièrement, le modèle de paral-
lélisme sur lequel il repose est restreint aux schémas fixes et déterministes (structures
série-parallèle), il est donc impossible d’y exprimer des squelettes pour lesquels l’or-
donnancement des communications n’est pas connu à la compilation (comme FARM).
Deuxièmement, dans l’approche décrite, les squelettes ne font pas à proprement dit
partie du DSL mais sont définis à partir du vocabulaire de ce dernier. Enfin, la sé-
mantique du langage n’est pas définie formellement mais directement encodée dans
l’interpréteur en MetaOcaml (il n’y pas de représentation intermédiaire explicite sous
la forme de réseau de processus communiquants en particulier). Cette dernière re-
marque s’applique aussi aux précédents travaux de Herrmann sur HDC (Herrmann et
al., 2000), un compilateur produisant du code C+MPI à partir d’un sous-ensemble du
langage Haskell en s’appuyant sur un ensemble fixe et prédéfini de squelettes.
670 RSTI - TSI. Volume 28 – n° 5/2009
Une autre manière de juger QUAFF consiste à regarder dans quelle mesure l’ou-
til proposé répond aux critères énoncés par (Cole, 2004) et repris par Danelutto
dans (Danelutto, 2005). Ces critères, rappelons-le, portent sur
1) les performances du code produit,
2) le gain en temps de développement (par rapport, toujours, à une approche fon-
dée sur des outils comme MPI ou OPENMP),
3) la contribution de l’approche proposée à la diffusion du concept de squelette
pour la parallélisation,
4) la possibilité de réutiliser du code tiers avec un minimum de modification,
5) la possibilité d’exprimer, en dehors des squelettes, des formes de parallélisme
ad-hoc,
6) la possibilité de définir facilement des variantes aux squelettes proposés,
7) le support d’architectures hétérogènes,
8) la possibilité de déployer dynamiquement des squelettes.
QUAFF se positionne très favorablement par rapport aux points 1 et 2. Son implan-
tation sous la forme d’une bibliothèque active en C++ lui permet en outre de répondre
pleinement aux objectifs des points 3 et 4. La possibililté d’appeler des primitives
MPI au sein même des fonctions utilisateurs, si elle ne constitue pas l’approche « nor-
male » reste néanmoins possible, ce qui offre un moyen de supporter, le cas échéant,
des formes ad hoc de parallélisme. En revanche, dans la version actuelle, il n’est pos-
sible d’introduire facilement des variantes aux squelettes proposés (i.e. sans modifier
le code même de QUAFF ). Cette possibilité pourrait être offerte dans une version ulté-
rieure, via un mécanisme similaire à celui offert par le schéma STRATÉGIE au sein des
approches de type design pattern. Cette extension est en cours d’étude. Enfin, de part
sa nature intrinsèquement statique, QUAFF est peu à même de répondre aux objectifs
des points 7 et 8.
7. Conclusion et perspectives
8. Bibliographie
Abrahams D., Gurtovoy A., C++ Template Metaprogramming: Concepts, Tools, and Tech-
niques from Boost and Beyond (C++ in Depth Series), Addison-Wesley Professional, 2004.
Aldinucci M., Danelutto M., Teti P., « An advanced environment supporting structured parallel
programming in Java », Future Generation Computer Systems, vol. 1, p. 611-626, 2003.
Arulampalam S., Maskell S., Gordon N., Clapp T., « A Tutorial on Particle Filters for On-line
Non-linear/Non-Gaussian Bayesian Tracking », IEEE Transactions on Signal Processing,
vol. 50, n° 2, p. 174-188, February, 2002.
Bacci B., Danelutto M., Orlando S., Pelagatti S., Vanneschi M., « P3L: A Structured High
Level Programming Language And Its Structured Support », Concurrency: Practice and
Experience, vol. 7, p. 225-255, 1995.
Beckmann O., Houghton A., Kelly P. H. J., Mellor M., « Run-time code generation in C++
as a foundation for domain-specific optimisation », Domain-Specific Program Generation
International Seminar, p. 291-306, 2003.
Bischof H., Gorlatch S., Leshchinskiy R., « DatTeL: A Data-Parallel C++ Template Library »,
Parallel Processing Letters, vol. 13, n° 3, p. 461-482, September, 2003.
Chateau T., Gay-Belille V., Chausse F., Laprest� J., « Real-Time Tracking with Classifiers »,
WDV Workshop on Dynamical Vision at ECCV2006, 2006.
Chiba S., « A metaobject protocol for C++ », OOPSLA ’95: Proceedings of the tenth annual
conference on Object-oriented programming systems, languages, and applications, ACM
Press, New York, NY, USA, p. 285-299, 1995.
Clément F., Martin V., Vodicka A., Cosmo R. D., Weis P., « Domain Decomposition and Ske-
leton Programming with OCamlP3l », Parallel Computing, vol. 32, p. 539-550, 2006.
Cole M., Algorithmic skeletons: structured management of parallel computation, MIT Press,
1989.
Cole M., Research Directions in Parallel Functional Programming, Springer, chapter 13, Algo-
rithmic skeletons, 1999.
Cole M., « Bringing Skeletons out of the Closet : A Pragmatic Manifesto for Skeletal Parallel
Programming », Parallel Computing, vol. 3, p. 389-406, 2004.
Consel C., Lawall J., Le Meur A.-F., « A Tour of Tempo: A Program Specializer for the C
Language », Science of Computer Programming, 2004.
Consortium UPC., UPC Language Specifications, Version 1.2 edn, Lawrence Berkeley National
Lab Tech Report LBNL-59208, 2005.
Czarnecki K., « Generative Programming: Methods, Techniques, and Applications », ICSR-
7: Proceedings of the 7th International Conference on Software Reuse, Springer-Verlag,
London, UK, p. 351-352, 2002.
Czarnecki K., O’Donnell J., Striegnitz J., Taha W., « DSL implementation in MetaOCaml,
Template Haskell and C++ », in C. Lengauer, D. Batory, C. Consel, M. Odersky (eds),
Domain-Specific Program Generation, vol. 3016 of Lecture Notes in Computer Science,
Springer-Verlag, p. 51-72, 2004.
Danelutto M., « "Second-generation" Skeleton Systems », Parallel Computing: Current & Fu-
ture Issues of High-End Computing, Proceedings of the International Conference ParCo
Métaprogrammation pour le parallélisme 673
Veldhuizen T. L., Gannon D., « Active Libraries: Rethinking the roles of compilers and libra-
ries », Proceedings of the SIAM Workshop on Object Oriented Methods for Inter-operable
Scientific and Engineering Computing (OO’98), SIAM, Philadelphia, PA, USA, 1998.
Veldhuizen T. L., Jernigan M. E., « Will C++ be faster than Fortran? », Proceedings of the 1st In-
ternational Scientific Computing in Object-Oriented Parallel Environments (ISCOPE’97),
Lecture Notes in Computer Science, Springer-Verlag, 1997.
Joel Falcou est maître de conférences en informatique à l’université de Paris Sud. Ses tra-
vaux portent sur le développement d’outil d’aide à la parallélisation ; d’une part, en exploitant
et étendant les techniques de programmation générative en général et de métaprogrammation
en particulier et d’autre part, en étudiant divers modèles de haut-niveau (squelettes parallèles,
BSP, Futures, etc.)
Jocelyn Sérot est professeur à l’université Blaise Pascal de Clermont Ferrand. Ses travaux
portent de manière générale sur la conception et la mise en œuvre de modèles de programma-
tion de haut niveau pour la programmation parallèle, et plus particulièrement sur les apports
des langages de programmation fonctionnels aux techniques de prototypage d’applications sur
systèmes dédiés ou embarqués.