You are on page 1of 31

Une bibliothèque métaprogrammée pour la

programmation parallèle

Joël Falcou1 — Jocelyn Sérot2


1 joel.falcou@u-psud.fr
LRI - UMR 8623 - Bâtiment 490 - Centre Scientifique d’Orsay
F 91405 Orsay cedex

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.

RSTI - TSI. Volume 28 – n° 5/2009, pages 645 à 675


646 RSTI - TSI. Volume 28 – n° 5/2009

1. Introduction

Dans le domaine de la programmation parallèle, le principal problème de nos jours


consiste à concilier abstraction et efficacité. Par abstraction, on entend la possibilité
pour le programmeur d’écrire des programmes sans se soucier des détails de mise
en œuvre du parallélisme sous-jacent (Skillicorn et al., 1998). Cette abstraction passe
par l’usage de modèles de programmation de haut niveau masquant plus ou moins le
modèle d’exécution et l’architecture matérielle. Par efficacité, on entend la possibilité
pour le code produit en final d’offrir des performances similaires à celles d’un code
écrit « à la main », en utilisant un modèle de programmation de plus bas niveau. Dans
l’état de l’art actuel, ces deux objectifs sont difficiles à concilier. D’un côté, de nom-
breux modèles de programmation de haut niveau ont été développés en réponse au
besoin d’abstraction. Sans rentrer dans les détails d’une revue exhaustive, on constate
simplement que ces modèles couvrent la plupart des formes de parallélisme d’une part
(contrôle, données) et des modèles architecturaux d’autre part (MIMD à mémoire dis-
tribuée, à mémoire partagée, SIMD) et sont disponibles pratiquement sous la forme
de langages dédiés (Koebel et al., 1994; Fatni et al., 1997; Mark et al., 2003; Consor-
tium, 2005) ou de bibliothèques pour des langages existants (Hill et al., 1998; Falcou
et al., 2003; Bischof et al., 2003). D’un autre côté, des outils de programmation de
plus bas niveau, comme MPI ou OPENMP sont encore largement dominants de nos
jours pour des raisons d’efficacité justement. Le surcoût introduit par les formalismes
de haut niveau est en effet le plus souvent jugé inconciliable avec les objectifs de per-
formances du code parallèle. A ceci s’ajoute parfois des problèmes très pragmatiques
d’intégration de code séquentiel existant au sein de ces outils.

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

La plupart des systèmes de programmation par squelettes sont implantés sous


la forme d’une bibliothèque au sein d’un langage séquentiel hôte traditionnel. ES -
KEL (Cole, 2004), par exemple, est une extension de la bibliothèque MPI au sein
de laquelle les squelettes sont présentés comme des généralisations des fonctions
de communications collectives. OcamlP3L (Clément et al., 2006) est un système
de programmation parallèle par squelettes écrit en Caml et fondé sur le modèle de
P3L (Bacci et al., 1995). LITHIUM (Danelutto et al., 2002; Aldinucci et al., 2003),
qui descend aussi de P3L, est une bibliothèque JAVA, orientée objet, et adaptée à la
programmation par squelettes sur des architectures de type cluster ou grille de calcul.
MUESLI (Kuchen, 2002) propose un ensemble de squelettes adapté au parallélisme de
données et de contrôle sous la forme d’une hiérarchie de classes C++.
Les solutions évoquées ci-avant, si elles l’atténuent, ne résolvent pas complète-
ment le dilemme abstraction/efficacité. La bibliothèque ESKEL, par exemple, permet
d’obtenir des performances très proches de celles obtenues avec un code MPI mais
au prix d’une interface (API) fortement contrainte par les contingences de l’API C de
MPI (nécessité de structurer les données en tableaux, usage systématique des pointeurs
void*, etc.). LITHIUM et MUESLI offrent un niveau d’abstraction supérieur, grâce no-
tamment au support objet de leur langage hôte mais cette abstraction se paye en termes
d’efficacité. Pour LITHIUM, l’exécution parallèle passe par l’exécution d’un interpré-
teur distribué implantant un modèle d’exécution macroflot de données. Pour MUESLI,
le surcoût est essentiellement dû au mécanisme d’appel des fonctions virtuelles en
C++ .

Dans un contexte plus général, la notion de bibliothèque active (Veldhuizen et


al., 1998) est parfois présentée comme une solution au dilemme abstraction/perfor-
mance. L’idée est ici d’opérer, en amont des optimisations usuelles effectuées par le
compilateur, des optimisations de haut niveau liées à la sémantique propre du mo-
dèle de programmation utilisé, en exploitant un système de métadéfinitions déduites
de ce modèle. Parmi les réalisations concrètes de cette approche, on peut citer par
exemple les projet Xroma (Czarnecki, 2002), MPC++ (Ishikawa et al., 1996), Open
C++ (Chiba, 1995), Magik (Engler, 1997) ou TaskGraph (Beckmann et al., 2003). Ces
systèmes permettent d’injecter au sein de langages existants des extensions fournissant
à ce dernier les indications sémantiques nécessaires à une compilation efficace.
Le concept de bibliothèque active est souvent lié à celui de langage orienté do-
maine ou Domain Specific Language. Par DSL, on entend un langage dont la syntaxe
et la sémantique sont adaptées à une classe d’applications particulière. Les biblio-
thèques actives constituent alors un moyen d’implémenter des DSL directement au
sein d’un langage hôte sans avoir à développer un compilateur ou un interpréteur au-
tonome, ce qui dispense d’avoir à écrire un analyseur syntaxique ou un générateur de
code spécifique (Czarnecki et al., 2004; Herrmann et al., 2006). Les techniques de
métaprogrammation (Sheard, 2001; Jones et al., 2002) sont au cœur de ces approches.

Dans cet article, nous décrivons l’application des techniques de méta-


programmation à l’implémentation d’un DSL dédié à la programmation parallèle par
648 RSTI - TSI. Volume 28 – n° 5/2009

squelettes. Comme on le verra, cette approche permet de concilier parfaitement abs-


traction et efficacité. Afin de faciliter l’intégration de code séquentiel existant1 , le
langage hôte retenu est C++.
Le plan retenu est le suivant. Dans la section 2 nous présentons un DSL dédié
à la programmation parallèle par squelettes. Dans la section 3, nous rappelons les
principes de la méta-programmation en général et comment ces principes peuvent se
réaliser concrètement en C++ en utilisant le mécanisme des templates (« patrons »).
La section 4 décrit l’implémentation de ce DSL à l’aide des techniques de méta-
programmation. Les résultats expérimentaux, permettant de juger de le pertinence de
cette implantation sont présentés dans la section 5. Les travaux en rapport sont pas-
sés en revue dans la section 6 et la section 7 donne les perspectives ouvertes par ces
travaux.

2. Un DSL pour la programmation parallèle par squelettes

On décrit dans cette section la syntaxe et la sémantique d’un DSL – appelons-le


SKL – adapté à la programmation parallèle par squelette. Afin de ne pas alourdir la
présentation, le jeu de squelettes est limité aux quatre plus fréquemment employés.
Nous montrons par la suite que l’extension de ce jeu ne pose pas de problème a priori
dès lors que ces squelettes peuvent être décrits dans les termes d’une algèbre de pro-
cessus.
– Le squelette PIPE encapsule un parallélisme de type flux : n étapes de calcul
sont enchaînées sur chacune des données d’entrée, chaque étape pouvant s’exécuter
en parallèle sur des données distinctes. D’un point de vue purement applicatif, on a :

P ipe(f1 , . . . , fn−1 , fn , x) = fn (fn−1 (. . . f1 (x))

– Le squelette FARM encapsule un parallélisme de type données : une même opé-


ration est appliquée à l’ensemble des données d’entrée ; le modèle d’exécution sous-
jacent est celui dit en « ferme de processus », où un processus maître distribue dyna-
miquement les données à traiter à un ensemble de processus esclaves et collecte les
résultats, afin de réaliser un équilibre de charge :

F arm(f, hx1 , . . . , xn i) = hf (x1 ), . . . , f (xn )i

– Le squelette PARDO encapsule un parallélisme de type tâche : n opérations sont


appliquées en parallèle sur chaque donnée d’entrée :

P ardo(f1 , . . . , fn , x) = hf1 (x), . . . , fn (x)i

– Le squelette SCM encapsule un parallélisme de données régulier : chaque élé-


ment du flux de données d’entrées est transformé en une liste de sous-éléments. Ces
1. Dans le domaine du calcul scientifique, une part significative du code susceptible d’être pa-
rallélisé est écrite en C ou C++.
Métaprogrammation pour le parallélisme 649

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 :

Scm(n, fs , f, fm , x) = fm (hf (x1 ), . . . , f (xn )i)

Dans la description précédente, les « étapes » ou les « opérations » sont elles-


mêmes des squelettes ; en d’autres termes, le langage supporte naturellement
l’imbrication des squelettes. L’insertion des fonctions séquentielles se fait donc via
un squelette particulier, nommé SEQ.

Seq(f, x) = f (x)

La syntaxe abstraite des programmes SKL est la suivante :

Σ ::= Seq f
| Pipe Σ1 . . . Σn
| Pardo Σ1 . . . Σn
| Farm n Σ
| Scm n fs Σ fm
f, fs , fm ::= fonctions séquentielles
n ::= entier ≥ 1

Imaginons par exemple un programme pouvant se décomposer en trois étapes : une


étape de production de données (réalisée par une fonction f ), une étape de traitement
de ces données en parallèle à l’aide d’une ferme à trois esclaves (chacun exécutant la
même fonction g) et une étape de traitement des résultats (réalisée par une fonction
h). La figure 1 représente le réseau de processus correspondant.

f FarmM h

g g g

Figure 1. Un exemple d’application définie sous forme de squelette

Ce programme s’écrit de la manière suivante en SKL:

σ = Pipe(Seq f, Farm(3, Seq g), Seq h)


650 RSTI - TSI. Volume 28 – n° 5/2009

Sémantique opérationnelle des programmes SKL

La sémantique des programmes SKL est explicitée en termes de réseaux de proces-


sus séquentiels communiquants (RPSC). Cette sémantique servira de base à la fonc-
tion d’interprétation qui, codée en C++ , va permettre de générer le code à exécuter
par chaque processeur de la machine parallèle cible. En un sens, SKL peut être vu
comme une commodité syntaxique permettant de dénoter concisément, et de manière
structurée, des programmes décrits sous la forme de RPSC.

On commence par définir formellement un réseau de processus séquentiels comme


un triplet π = hP, I, Oi où :
– P est un ensemble de processus étiquetés, c’est-à-dire de paires (pid , σ) où pid
est un identificateur (unique) de processus et σ un triplet contenant : une liste de
prédécesseurs (pid des processus p pour lesquels il existe un canal de communication
de p au processus en question), une liste de successeurs (pid des processus p pour
lesquels il existe un canal de communication du processus en question à p) et un
descripteur ∆. On note L(π) l’ensemble des pid s d’un réseau de processus π. Pour
un processus p, ses prédécesseurs, successeurs et descripteur seront notés I(p), O(p)
et δ(p) respectivement.
– I(π) ⊆ L(π) désigne l’ensemble des processus p pour lesquels I(p) = ∅
– O(π) ⊆ L(π) désigne l’ensemble des processus p pour lesquels O(p) = ∅
Le descripteur de processus ∆ est une paire (instrs, kind) où instrs est une
séquence de macro-instructions et kind, un drapeau dont la signification sera donnée
plus loin.

∆ ::= hinstrs, kindi


instrs ::= instr1 , . . . , instrn
kind ::= Regular | FarmM

La séquence de macro-instructions décrivant le comportement du processus est im-


plicitement itérée. En première approximation on considère que les processus opèrent
sur un flux de données infini2 . Les macro-instructions utilisent toutes un mode d’adres-
sage implicite se référant à quatre registres (variables locales à chaque processus)
nommés iv, ov, q et iws. Le jeu de macro-instructions est décrit ci-après. Dans les
explications afférentes, p désigne le processus exécutant l’instruction en question.

instr ::= SendTo | RecvFrom | Comp fid | RecvFromAny | SendToQ |


Ifq instrs1 instrs2 | GetIdleW | UpdateWs | Gather | Scatter

L’instruction SendTo envoie le contenu de la variable ov au processus dont le pid


apparait en dernier dans O(p). L’instruction RecvFrom reçoit une donnée en prove-

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

On définit ensuite une algèbre simple permettant de construire, de manière com-


positionnelle des réseaux de processus. On utilisera pour cela les notations suivantes.
Si E est un ensemble, on note E[e ← e0 ] l’ensemble obtenu en substituant e par e0
(en posant E[e ← e0 ] = E si e ∈ / E). Cette notation est supposée associative à
gauche : E[e ← e0 ][f ← f 0 ] signifie (E[e ← e0 ])[f ← f 0 ]. Si e1 , . . . , em est un
sous-ensemble de E et φ : E → E une fonction, on notera E[ei ← φ(ei )]i=1..m
l’ensemble (. . . ((E[e1 ← φ(e1 )])[e2 ← φ(e2 )]) . . .)[em ← φ(em )]. Sauf mention ex-
plicite, on notera I(πk ) = {i1k , . . . , im 1 n
k } et O(πk ) = {ok , . . . , ok }. Par souci de conci-
j j j j
sion, les listes I(ok ) et O(ik ) seront notées sk et dk respectivement. Pour les listes,
on suppose définie l’opération de concaténation ++ usuelle : si l1 = [e11 , . . . , em 1 ] et
l2 = [e12 , . . . , en2 ] alors l1 ++l2 = [e11 , . . . , em
1 , e1
2 , . . . ; e n
2 ]. La liste vide est notée []. La
longueur d’une liste l ou le cardinal d’un ensemble l sont tous deux notés |l|.

L’opérateur d.e crée un réseau de processus contenant un seul processus à partir de


son descripteur. La fonction N EW() fournit un nouveau pid à chaque appel.

δ∈∆ l = N EW()
(S INGL)
dδe = h{(l, h[], [], δi)}, {l}, {l}i

L’opérateur • « sérialise » deux réseaux de processus, en connectant les sorties du


second aux entrées du premier :

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) |O1 | = |I2 | = m


(S ERIAL)
π1 • π2 = h(P1 ∪ P2 )[(o1 , σ) ← φd ((oj1 , σ), ij2 )]j=1...m
j

[(ij2 , σ) ← φs ((ij2 , σ), oj1 )]j=1...m ,


I1 , O2 i

Cette règle utilise deux fonctions auxiliaires φs et φd définies comme suit :

φs ((p, hs, d, hδ, Regularii), p0 ) = (p, h[p0 ]++s, d, h[RecvFrom]++δ, Regularii)


φd ((p, hs, d, hδ, Regularii), p0 ) = (p, hs, d++[p0 ], hδ++[SendTo], Regularii)
φs ((p, hs, d, hδ, FarmMii), p0 ) = (p, h[p0 ]++s, d, hδ, FarmMii)
φd ((p, hs, d, hδ, FarmMii), p0 ) = (p, hs, d++[p0 ], hδ, FarmMii)

La fonction φs (resp. φd ) ajoute un processus p0 aux prédécesseurs (resp. succes-


seurs) d’un processus p et met à jour sa liste de macro-instructions. Concrètement,
cela consiste à ajouter au début (resp. à la fin) de cette liste une instruction RecvFrom
(resp. SendTo), sauf pour les processus maîtres au sein d’un squelette de type FARM,
pour lesquels la liste d’instructions n’est pas modifiée (ces processus sont identifiés à
l’aide du drapeau kind, positionné à la valeur FarmM).

L’opérateur k met deux réseaux de processus en parallèle en fusionnant leurs en-


trées et sorties respectivement.

πi = hPi , Ii , Oi i (i = 1, 2)
(PAR)
π1 k π2 = hP1 ∪ P2 , I1 ∪ I2 , O1 ∪ O2 i

L’opérateur on relie deux réseaux de processus en connectant chaque entrée et


chaque sortie du second à la sortie du premier. Cet opérateur est utilisé pour décrire la
structure cyclique qui correspond à une ferme de processus ou à une structure de type
SCM :

πi = hPi , Ii , Oi i (i = 1, 2) |O1 | = 1 |I2 | = m |O2 | = n


(J OIN)
n π2 = h(P1 ∪ P2 )[(o1 , σ) ← Φ((o1 , σ), I(π2 ), O(π2 ))]
π1 o
j j
[(i2 , σ) ← φs ((i2 , σ), o1 )]j=1...m
[(oj2 , σ) ← φd ((ij2 , σ), o1 )]j=1...n ,
I1 , O1 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 :

Φs (p, [p1 , . . . , pn ]) = φs (. . . , φs (φs (p, p1 ), p2 ), . . . , pn )


Métaprogrammation pour le parallélisme 653

Φd (p, [p1 , . . . , pn ]) = φd (. . . , φd (φd (p, p1 ), p2 ), . . . , pn )


On peut dès lors expliciter la fonction C transformant un programme SKL en un
réseau 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 :

∆(ScmM fs fm ) = h[Comp fs ; Scatter; Gather; Comp fm ], Regulari

Nous venons donc de définir un DSL permettant de décrire de manière suffisam-


ment abstraite des programmes parallèles et un ensemble de règles permettant d’as-
socier à ces programmes une sémantique de plus bas niveau sous forme de réseaux
de processus séquentiels communiquants. Comme indiqué dans l’introduction, nous
allons dans la suite de cet article décrire comment, pratiquement, ce langage peut
être implanté au sein d’un langage comme C++ en utilisant les facilités de méta-
programmation offerte par ce langage. Pour cela, nous rappelons dans la section sui-
vante les principes généraux des techniques de méta-programmation.
654 RSTI - TSI. Volume 28 – n° 5/2009

3. Principe de la métaprogrammation

On peut définir simplement la méta-programmation comme l’écriture de pro-


grammes (les « méta-programmes ») capables, au sein d’un système adéquat, d’ex-
traire, de transformer et de générer des fragments de code afin de produire de nouveaux
programmes (les programmes « objets ») répondant à un besoin précis. L’exemple le
plus simple de méta-programme est le compilateur qui, à partir de fragments de code
dans un langage donné, est capable d’extraire des informations et de générer un nou-
veau code, cette fois en langage machine, qui sera plus tard exécuté par le processeur.
En pratique, on distingue souvent deux types de système de méta-programmation :
– les analyseurs/transformateurs de code qui utilisent la structure et l’environ-
nement d’un programme afin de calculer un nouveau fragment de programme. On
retrouve dans cette catégorie des outils comme PIPS (Irigoin et al., 1991) ;
– les générateurs de programmes dont l’utilisation première est de résoudre une
famille de problèmes connexes dont les solutions sont souvent des variations d’un mo-
dèle connu. Pour ce faire, ces systèmes génèrent un nouveau programme capable de
résoudre une instance particulière de ces problèmes et optimisé pour cette résolution.
On distingue plus précisément les générateurs statiques qui génèrent un code néces-
sitant une phase de compilation classique et les générateurs capables de construire
des programmes et de les exécuter à la volée. On parle alors de multi-stage program-
ming (Taha, 2003).
L’avantage principal est le fait que la méta-programmation permet, à partir d’une
description générique et adapté au problème traité, de produire un nouveau pro-
gramme dont les performances sont plus élevées. De manière empirique, cette aug-
mentation des performances est proportionnelle à la quantité d’éléments évaluables
statiquement au sein du programme initial.

3.1. L’évaluation partielle

Parmi les différentes techniques de méta-programmation, on s’intéresse plus par-


ticulièrement aux techniques d’évaluation partielle (Jones, 1996; Futamura, 1999).
Effectuer l’évaluation partielle d’un code consiste à discerner les parties statiques du
programme – c’est à dire les parties du code calculables à la compilation – des par-
ties dynamiques – calculables à l’exécution. Le compilateur est alors amené à évaluer
cette partie statique et à générer un code – dit code résiduel – ne contenant plus que
les parties dynamiques. On retrouve donc ici une définition de la méta-programmation
– la programmation par extraction et génération de code à partir d’une description gé-
nérique – dans un cadre particulier : au lieu de nécessiter à la fois un langage de
spécification et un langage pour le code résiduel, l’évaluation partielle ne nécessite
qu’un seul et unique langage. Mais, pour permettre l’évaluation partielle, ce langage
doit permettre l’annotation explicite des structures statiques. Ceci nécessite donc un
langage à deux niveaux dans lequel il est possible de manipuler de tels marqueurs.
Métaprogrammation pour le parallélisme 655

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

On définit alors un évaluateur partiel Γ comme une fonction qui transforme le


couple (Φ, Istatic ) en un nouveau programme – le code résiduel – Φ∗ dans lequel
Istatic a été entièrement évalué.

Γ : (Φ, Istatic ) → Φ∗

Bien entendu, Φ∗ est tel que Φ et Φ∗ restent fonctionnellement équivalents mais,


en général, les performances de Φ∗ sont supérieures à celle de Φ.

Φ ≡ Φ∗ : (Idynamic ) → O

Afin de déterminer les portions de code à évaluer statiquement, un évaluateur par-


tiel doit analyser le code source original pour y détecter des structures et des données
statiques. Cette détection permet par la suite de spécialiser les fragments de code.

3.2. L’évaluation partielle en C++

La mise en œuvre d’un mécanisme d’évaluation partielle en C++ revient à four-


nir un outil capable d’analyser un code contenant des éléments statiques. Il est évi-
demment possible de définir un outil externe au langage qui effectuerait cette tâche
au niveau du code source et génèrerait un nouveau code source partiellement évalué
comme le fait par exemple l’outil Tempo (Consel et al., 2004). Cette solution, bien
qu’envisageable, nous paraît peu élégante dans le sens où elle complexifie la chaîne
de développement. Il nous semble préférable de trouver un moyen d’annoter direc-
tement au sein du langage C++ ces éléments statiques. Pratiquement, la seule pos-
sibilité s’avère être le recours aux templates, seul mécanisme du C++ permettant de
manipuler des fragments de code.
Les templates (ou patron) sont un mécanisme du langage C++ dont le but principal
est de fournir un support pour la programmation dite « générique », favorisant ainsi la
réutilisation de code. Un patron définit une famille de classes ou de fonctions paramé-
trées par une liste de valeurs ou de types. Fixer les paramètres d’une classe ou d’une
fonction template permet de l’instancier et donc de fournir au compilateur un frag-
ment de code complet et compilable. Les templates supportent aussi un mécanisme
de spécialisation partielle. Le résultat d’une telle spécialisation est alors non pas un
fragment de code compilable mais un nouveau patron devant lui même être instancié.
Ainsi, il devient possible de fournir des patrons dépendant de certaines caractéristiques
paramètrables et d’optimiser le code ainsi généré en fonction de ces caractéristiques.
656 RSTI - TSI. Volume 28 – n° 5/2009

Lorsque le compilateur tente de résoudre un type template, il commence par cher-


cher la spécialisation la plus complète et remonte la liste des spécialisations partielles
jusqu’à trouver un cas valide. Nous retrouvons donc ici, la définition même de l’éva-
luation partielle : la programmation par spécialisation. En effet, l’instanciation d’une
classe template correspond à l’application d’une fonction qui calcule une nouvelle
classe à partir d’un patron et de ses paramètres. De la même manière, la spéciali-
sation partielle de template fournit un équivalent du mécanisme de filtrage (pattern
matching) présent dans de nombreux langages fonctionnels (ML, Haskell. . . ).
On peut en fait démontrer, par construction (Veldhuizen, 2000), que les templates
forment un sous-ensemble Turing-complet du C++. Un des premiers exemples de tels
programmes fut proposé par Erwin Unruh (Unruh, 1994). Ce programme ne s’exécu-
tait pas mais renvoyait une série de messages à la compilation qui énumérait la liste
des N premiers nombres premiers. Il démontrait ainsi que les patrons permettaient
d’exprimer plus que la simple généricité des classes et des fonctions.
Le C++ propose donc un modèle d’exécution à deux niveaux : une étape statique,
résolue lors de la compilation, qui évalue les méta-programmes et une étape dyna-
mique qui correspond à l’exécution classique de programme compilé. On peut alors
voir les templates comme un système complet d’évaluation partielle :
– une déclaration de fonction ou de classe template permet d’annoter un objet
de type « fragment de code » défini comme le contenu effectif de la classe ou de la
fonction.
template<int N> int f() {return 4*N; }

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>();

génèrera un fragment de code résiduel réellement compilable;


– l’évaluation d’un méta-programme template est effectuée par l’utilisation du
mot-clé typedef ou de manière implicite par le compilateur lorsque ce dernier ren-
contre une instance de classe template dont le type doit être résolu afin de continuer
la compilation. Dans le cas de classes templates, il faut noter que seul le code des
méthodes effectivement utilisées est instancié et ceci au moment précis de leur appel.

Dans le cadre de la programmation parallèle par squelettes algorithmiques, la plu-


part des applications utilisent un schéma d’association de squelettes défini une fois
pour toute lors du développement de l’application. Le caractère statique de cette struc-
ture fait donc de ces applications de parfaits candidats pour des outils d’optimisation
méta-programmés.
Métaprogrammation pour le parallélisme 657

4. De SKL à QUAFF : métaprogrammation d’un DSL

Nous présentons ici comment les règles de production et la fonction de construc-


tion C introduite à la fin de la section 2 sont implantées sous forme de méta-
programmes. Le processus, dans son ensemble, est décrit par la figure 2.

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

Figure 2. Processus de génération de code de QUAFF

Il se compose de trois étapes : la génération de l’arbre de syntaxe abstraite cor-


respondant à l’application définie par les squelettes, la transformation de cet arbre en
un réseau de processus séquentiels communiquant et la production du code résiduel
associé à chaque processeur faisant appel aux primitives MPI. Chacune de ces étapes
est effectué à la compilation en utilisant d’une part une représentation sous forme de
types template des éléments constitutifs du réseau de processus et d’autre part une
écriture sous forme de méta-programmes. Pour illustrer les mécanismes mis en jeu,
nous considèrerons une application délibérement très simple, constituée d’un pipeline
de deux étages (F1 et F2 étant des fonctions séquentielles définies par l’utilisateur.).
Cette application s’écrit de la manière suivante avec QUAFF :

Listing 1 – Exemple d’application QUAFF


# include < quaff / quaff . hpp >

typedef task < f1 , void , int > F1 ;


typedef task < f2 , int , void > F2 ;

int main ( int argc , char * argv [] )


{
run ( pipeline ( seq ( F1 ) , seq ( F2 ) ) ) ;
}

4.1. Génération de l’arbre de syntaxe abstraite

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

Listing 2 – Constructeurs des squelettes S EQ et P IPE


template < class F >
Seq <F > seq ( const F &)
{
return Seq <F >() ;
}

template < class S0 , class S1 > static inline


Serial < S0 , S1 > pipeline ( const S0 & , const S1 & )
{
return Serial < S0 , S1 >() ;
}

Dans l’exemple du pipeline simple, l’appel à la fonction run() nécessite l’appel


à pipeline puis à seq. Ces appels déclenchent successivement l’instanciation des
templates associés et la résolution des types nécessaires à leur résolution. Le code
suivant est alors généré :

run( Serial< Seq<F1>, Seq<F2> >() );

Le type de l’argument passé à la fonction run encode désormais précisément


l’AST du programme SKL correspondant à l’application.

La technique présentée sur cet exemple se généralise aisément à des programmes


quelconques en utilisant le mécanisme des Expression Templates. Les Expression Tem-
plates (Veldhuizen, 1995) sont un idiome classique du C++ permettant d’obtenir des
informations sur les déclarations du langage. La technique repose sur l’utilisation de la
surcharge d’opérateurs pour les types définis par l’utilisateur afin de reproduire, sous
la forme d’un type template récursif, l’arbre de syntaxe abstraite d’une expression. Il
est alors possible de manipuler cet arbre, régénérer un arbre suivant un schéma d’opti-
misation précis et générer le code résiduel correspondant. Dans le cas d’un DSL, cette
représentation de l’AST est décorée en utilisant des données annexes extraites de la
sémantique du langage considéré, ce qui permet de fournir au mécanisme d’évaluation
partielle des informations plus pertinentes que celle qu’un compilateur serait capable
d’inférer (Veldhuizen et al., 1997; Falcou et al., 2004).

4.2. Génération du réseau de processus

La génération du réseau de processus passe par l’encodage des structures de


données définies précédemment (processus, réseau de processus. . . ). Cet encodage
exploite la possibilité de représenter, sous la forme de types template et de méta-
programmes respectivement, des types de données abstraits et les opérations les mani-
pulant. Une liste d’éléments, par exemple, pourra être représentée en utilisant le type
suivant, où le type cons contient classiquement une tête et une queue de liste et le
type nil représente la liste vide :
Métaprogrammation pour le parallélisme 659

Listing 3 – Liste statique de type - définition


template < class H , class T > struct cons
{
typedef H head ;
typedef T tail ;
};

struct nil ;

Il est intéressant de noter ici comment l’utilisation des templates modifie la


sémantique des déclarations des types C++. En effet, d’un point de vue dynamique,
les déclarations des structures nil et cons définissent effectivement des patrons de
classes. Statiquement, nil et cons sont vus comme des types abstraits de données à
part entière, ce qui est corroboré par le fait que la déclaration – et non la définition –
de nil soit strictement suffisante pour rendre ce programme valide. Pratiquement, il
est possible de définir une implantation template de la plupart des conteneurs usuels
tels que tableau, liste ou table associative de types et de constantes. La manipulation
de ce type de liste statique est ensuite effectuée à l’aide de méta-programmes dé-
diés (Abrahams et al., 2004). Dans une grande majorité des cas, ces méta-programmes
s’appuient sur la structure naturellement récursive de l’implantation de ces structures
de données abstraites et sur le mécanisme de pattern matching template que nous
explicitons plus loin.

Les listings 4 à 6 illustrent les structures représentant un graphe de processus, un


processus et un descripteur de processus.

Listing 4 – Structure de donnée statique pour le type processus


template < class P , class I , class O > struct p r oc es s _n e tw or k
{
typedef P process ;
typedef I inputs ;
typedef O outputs ;
};

Listing 5 – Structure de donnée statique pour le type descripteur de processus


template < class ID , class DESC , class IT , class OT > struct process
{
typedef ID pid ;
typedef DESC descriptor ;
typedef IT input_type ;
typedef OT output_type ;
};
660 RSTI - TSI. Volume 28 – n° 5/2009

Listing 6 – Structure de donnée statique pour le type réseau de processus


template < class IPID , class OPID , class CODE , class KIND > struct descriptor
{
typedef IPID i_pids ;
typedef OPID o_pids ;
typedef CODE instrs ;
typedef KIND kind ;
};

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.

Listing 7 – Implantation de la fonction run


template < class SKL > typename convert < SKL >:: type run ( const SKL & )
{
return typename convert < SKL >:: type () ;
}

Le listing 8 donne ainsi la spécialisation – par simple surcharge – de la fonction


convert associée au squelette pipeline. Ici, convert extrait les informations sur les
squelettes contenus dans le pipeline depuis les types S0 et S1, les convertit en graphe
de processus, calcule un nouvel identifiant pour les prochains processus et applique la
règle de production idoine (S ERIAL).

Listing 8 – Surcharge template de convert pour pipeline


template < class S0 , class S1 , class ID > struct convert < Serial < S0 , S1 > , ID >
{
typedef Serial < S1 , mpl :: void_ > tail ;
typedef typename convert < S0 , ID >:: type proc1 ;
typedef typename convert < S0 , ID >:: new_id next_id ;
typedef typename convert < tail , next_id >:: new_id new_id ;
typedef typename convert < tail , next_id >:: type proc2 ;
typedef typename rule_serial < proc1 , proc2 >:: type type ;
};

Les règles de production introduites précédemment sont elles-mêmes implantées


sous forme de métafonctions. La méta-fonction correspondante à la règle S ERIAL est
Métaprogrammation pour le parallélisme 661

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.

Listing 9 – Version métaprogrammée de la règle (SERIAL)


template < class P1 , class P2 > struct rule_serial
{
typedef typename P1 :: process proc1 ;
typedef typename P2 :: process proc2 ;
typedef typename P1 :: inputs i1 ;
typedef typename P2 :: inputs i2 ;
typedef typename P1 :: outputs o1 ;
typedef typename P2 :: outputs o2 ;

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 ;

typedef process_network < process , i1 , o2 > type ;


};

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.

4.3. Génération du code résiduel parallèle

La dernière étape consiste à transformer le type représentant le graphe de proces-


sus en code C + MPI. Cette transformation est effectuée au sein de la fonction run en
parcourant la liste de macro-instructions associée à chaque processus et en générant
le code résiduel correspondant. Techniquement, cette transformation est déclenchée
par l’instanciation de la structure process_network et s’appuie sur la classe tuple
définie dans le listing 10.

Listing 10 – Définition de la classe tuple


template < class TL >
struct tuple : public tuple < typename TL :: tail_t >
{
typedef typename TL :: head_t type ;
type mValue ;
};

template < > struct tuple < nil > {};

La classe tuple se comporte comme un conteneur hétérogène. Son instanciation


construit une structure dont les membres sont obtenus par instanciation successive
de ses arguments. Dans le cas de QUAFF, chaque type contenu dans ce tuple est une
662 RSTI - TSI. Volume 28 – n° 5/2009

instance d’une classe exposant un opérateur() surchargé4 . Chacun de ces foncteurs va


alors contenir un fragment de code qui correspondt aux différentes macro-instructions
présentées précédement. Le listing 11 décrit ainsi le foncteur des macro-instructions
Comp et SendTo.

Listing 11 – Macro-instructions Comp et SendTo


template < class Function > struct Comp
{
template < class In , class Out > void operator () ( In & i , Out & o ) const
{
if ( pro cess_s tatus () ) mFunction (i ,o , m ) ;
}

Function mFunction ;
};

template < class OPID > struct Send


{
template < class In , class Out > void operator () ( In & , Out & o ) const
{
int tag = pr ocess_ status () ? comm :: Data : comm :: Stop ;
send ( at_c < OPID ,0 >:: type :: value ,o , tag ) ;
}
};

Dans la classe Comp, le type Function encapsule l’adresse de la fonction séquen-


tielle à appeler sur les éléments des tuples d’entrées et de sortie i et o. Pour SendTo,
la fonction send effectue un appel à MPI_Send en tenant compte du type effectif de o.
En parcourant récursivement chaque portion du tuple et en appelant cet opérateur, on
déclenche le déroulage en ligne du code de ces fonctions.

Listing 12 – Code résiduel pour l’exemple pipeline


if ( Rank () == 0 )
{
do {
out = F1 () ;
MPI_Send (& out ,1 , MPI_INT ,1 ,0 , MPI_C OMM_WO RLD ) ;
} while ( isValid ( out ) )
}
else if ( Rank () == 1 )
{
do {
MPI_Recv (& in ,1 , MPI_INT ,0 ,0 , MPI_COMM_WORLD ,& s ) ;
F2 ( in ) ;
} while ( isValid ( in ) )
}

4. On parle classiquement de foncteur.


Métaprogrammation pour le parallélisme 663

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

Dans cette section, on cherche à quantifier les performances des applications


écrites via l’interface de QUAFF en les comparant avec celles d’un code utilisant seule-
ment les primitives de base fournies par MPI, le but étant de mesurer le surcoût intro-
duit par cet outil par rapport à la version MPI. Deux types de tests sont détaillés. Dans
un premier temps, on évalue le surcoût introduit pour chaque squelette pris isolément.
Puis, nous évaluons ce surcoût dans le cadre d’une application de complexité réaliste.
Ces mesures ont été effectuées sur un cluster de 14 Power PC G5 XServe équipés de
2 processeurs PPC 790 cadencés à 2GHz. Les tests ont été réalisés avec l’implantation
LAM-MPI de MPI sous MAC OS 10.4.

5.1. Évaluation du surcoût par squelette

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.

τ N=2 N=3 N=4 N=5 N=6 N=7 N >= 8


1 ms 1,53 % 1,45 % 1,29 % 1,12 % 1,01 % 0,83 % 0,72 %
10 ms 0,48 % 0,44 % 0,30 % 0,32 % 0,31 % 0,29 % 0,29 %
50 ms 0,10 % 0,11 % 0,11 % 0,10 % 0,11 % 0,11 % 0,12 %
100 ms 0,07 % 0,07 % 0,07 % 0,07 % 0,06 % 0,06 % 0,05 %
500 ms 0,01 % 0,01 % 0,01 % 0,01 % 0,01 % <1‰ <1‰

Tableau 1. Mesure du surcoût en débit du squelette pipeline

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 Pardo, on mesure l’écart entre le temps d’exécution et le temps


d’exécution d’un code MPI équivalent, pour N fonctions fi en parallèle, avec N com-
pris entre 2 et 10 et des durées d’exécutions τ (fi ) comprises entre 0, 001s et 0, 5s. On
note que, quels que soient la durée des tâches séquentielles et le nombre de proces-
seurs utilisés, le surcoût induit par QUAFF est inférieur à 1 % et devient négligeable
dès que plus de 5 processeurs sont utilisés.
τi N=2 N=3 N=4 N >= 5
1 ms 0,3 % 0,1 % 3‰ <1‰
10 ms 0,1 % 1‰ <1‰ <1‰
50 ms 2‰ <1‰ <1‰ <1‰
> 100 ms <1‰ <1‰ <1‰ <1‰

Tableau 2. Mesure du surcoût du squelette pardo

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‰

Tableau 3. Mesure du surcoût du squelette farm

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

τf N=2 N=3 N=4 N=5 N=6 N=7 N=8 N>9


1 ms 3,35% 2,50% 2,20% 1,87% 1,41% 1,24% 1,10% 1,03%
10 ms 0,87% 0,79% 0,71% 0,63% 0,63% 0,59% 0,57% 0,53%
50 ms 0,19% 0,21% 0,19% 0,18% 0,17% 0,18% 0,17% 0,15%
100 ms 0,11% 0,12% 0,11% 0,10% 0,09% 0,09% 0,08% 0,09%
500 ms 0,05% 0,03% 0,04% 0,02% 0,02% 0,01% <1‰ <1‰

Tableau 4. Mesure du surcoût du squelette scm

5.2. Une application de complexité réaliste

On présente ici l’implantation d’une application parallèle de complexité réaliste


avec QUAFF . Il s’agit d’une part de vérifier que les conclusions sur les performances
du code produit tirées en section précédente restent valables dans le cas d’une ap-
plication mettant en jeu des fonctions séquentielles réalistes mais aussi d’évaluer
l’expressivité de l’outil, c’est-à-dire sa capacité à exprimer des schémas de parallé-
lisation plus complexes que ceux illustrés jusque-là.
L’application choisie concerne la détection et de suivi de personnes dans des sé-
quences d’images stéréoscopiques. Pour ce type d’application, on distingue en géné-
ral les approches basées sur des modèles pré-établis des objets considérés et celles
basées sur un apprentissage des dits objets. On décrit ici une application fondée sur
la deuxième approche et autorisant le suivi de la trajectoire tridimensionnelle d’une
personne en utilisant une formalisation probabiliste du problème. Cette méthode ayant
été par ailleurs décrite en détail dans (Falcou et al., 2006), nous nous contentons ici
de rappeler les principales bases théoriques et nous concentrons sur les aspects liés à
l’implémentation avec QUAFF .

5.2.1. Approche probabiliste du suivi d’objets


Dans cette approche probabiliste, les problèmes de détection et de suivi d’objets
dans une séquence d’images se résument à rechercher la densité de probabilité a poste-
riori P (Xt |Z1:t ) à partir de la densité de probabilité P (Z1:t |Xt ); où Xt représente le
vecteur d’état composé des paramètres du modèle de l’objet que l’on tente de suivre et
Z1:t = Z1 , . . . , Zt est le vecteur d’observation qui regroupe l’historique des mesures
effectuées précédemment. Ce processus récursif met en œuvre deux étapes :
– une étape de prédiction dans laquelle on évalue P (Xt |Z1:t−1 ) à partir de
P (Xt−1 |Z1:t−1 ) et de la densité de transitions P (Xt |Xt−1 ). Pour ce faire, on uti-
lise classiquement l’équation de prédiction de Chapman-Kolmogorov ;
– une phase de mesure et de mise à jour dans laquelle l’utilisation de la règle de
Bayes permet de mettre à jour la valeur de P (Xt |Zt ) en fonction de P (Zt |Xt ).
En théorie, ces équations récurrentes forment la base d’une estimation de Bayes
optimale. Malheureusement, le calcul de ces intégrales est en général impossible ana-
666 RSTI - TSI. Volume 28 – n° 5/2009

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).

5.2.2. Implantation séquentielle


L’algorithme séquentiel du suivi par filtrage particulaire peut être grossièrement
décrit par une succession d’étapes reprenant l’algorithme dit de CONDENSATION pré-
senté dans (Falcou et al., 2006) en utilisant un classifieur issu d’un algorithme de type
ADABOOST (Freund et al., 1995; Schapire, 1999; Tieu et al., 2004; Niculescu-
Mizil et al., 2005; Chateau et al., 2006) pour évaluer le score de vraisemblance de
chaque particule. Le tableau 5 donne les temps d’exécution séquentiels de cet algo-
rithme pour un nombre de particules variant de 1 000 à 10 000, sachant qu’en pratique
le premier nombre consitue un minimum pour obtenir un suivi stable. On se place
dans l’hypothèse du régime permanent et on ne tient pas compte du temps nécessaire
à l’étape d’initialisation. Ces résultats sont obtenus sur une machine POWER PC G5 en
mode monoprocesseur et sans utiliser l’extension ALTIVEC.

Npart Temps d’exécution Débit


1 000 672,3 ms 1,49 images/sec
2 000 1986,2 ms 0,50 images/sec
5 000 11358,4 ms 0,09 images/sec
10 000 21142,7 ms 0,05 images/sec

Tableau 5. Performances séquentielles du suivi de personne par filtrage particulaire

Ces résultats montrent clairement la nécessité d’obtenir un gain substantiel en per-


formance et motivent l’idée d’une implantation parallèle. Dans (Falcou et al., 2006),
on décrit un schéma de parallélisation hybride, s’appuyant à la fois sur les possibili-
tés SIMD de l’unité ALTIVEC de chaque processeur et sur un modèle MIMD inter-
processeurs. QUAFF ne prenant en charge que le second aspect, seul celui-ci est évoqué
ici. Notons au passage que l’algorithme de filtrage particulaire nécessite l’utilisation
d’un nombre de particules qui croît exponentiellement avec la taille du vecteur d’état.
Ainsi, si l’on désire étendre cet algorithme au suivi de plusieurs personnes avec une
Métaprogrammation pour le parallélisme 667

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.

5.2.3. Implantation parallèle


L’analyse de l’implantation séquentielle du suivi par filtrage particulaire est résu-
mée dans le tableau 6.
N Génération Pré-traitement Mesure Mise à jour Estimation Echantillon.
1 000 0,12 % 1,56 % 96,51 % 0,02 % 0,11 % 1,68 %
2 000 0,08 % 0,53 % 97,13 % 0,01 % 0,07 % 2,17 %
5 000 0,04 % 0,09 % 97,47 % 0,01 % 0,04 % 2,35 %
10 000 0,05 % 0,05 % 94,57 % 0,01 % 0,05 % 5,28 %

Tableau 6. Importance relative des étapes de l’algorithme de suivi

Listing 13 – Code QUAFF pour l’algorithme du filtre à particule


# include < quaff / quaff . hpp >
# define NB_PROC 28

typedef vector < particle > mat_t ;


typedef task < sample , void , mat_t > Sample ;
typedef task < gener , mat_t , mat_t > Generate ;
typedef task < mesure , mat_t , mat_t > Mesure ;
typedef task < update , mat_t , mat_t > Update ;
typedef task < display , mat_t , void > Display ;

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é.

nP =1 000 nP =2 000 nP =5 000 nP =10 000


P =2
Temps 409,94ms 1115,84ms 6074,01ms 10898,30ms
Gain 1,64 1,78 1,87 1,94
Efficacité 82 % 89 % 93,5 % 97 %
P =4
Temps 263,65ms 711,90ms 3527,45ms 6110,61ms
Gain 2,55 2,79 3,22 3,46
Efficacité 63,7 % 69,75 % 80,5 % 86,5 %
P =6
Temps 187,79ms 467,34ms 1975,37ms 3645,29ms
Gain 3,58 4,25 5,75 5,80
Efficacité 59,7 % 70,8 % 95,8 % 96,7 %
P = 12
Temps 162,00ms 291,23ms 1046,86ms 1828,95ms
Gain 4,15 6,82 10,85 11,56
Efficacité 34,6 % 56,8 % 90,4 % 96,3 %
P = 28
Temps 108,61ms 170,05ms 512,79ms 786,27ms
Gain 6,19 11,68 22,15 26,89
Efficacité 22,1 % 41,7 % 79,1 % 96,1 %

Tableau 7. Performances temporelles du suivi parallèle

On remarque d’une part que, pour un nombre donné nP de particules, l’efficacité


 = Gain/P décroît avec le nombre P de processeurs, sauf pour les grandes valeurs
de np 6 . Ceci s’explique tout à fait normalement par le coût croissant des communi-
cations et des synchronisations par rapport aux calculs. Le même effet est d’ailleurs
observé avec une version de l’application parallélisée « à la main » avec MPI. L’aug-
mentation de nP permet de limiter cet effet en accroissant la part des calculs.

6. La valeur  obtenue pour nP = 10000 et P = 4 est discordante en ce sens. Nous pensons


qu’il s’agit d’une erreur de mesure.
Métaprogrammation pour le parallélisme 669

L’efficacité dépasse 50 % sauf pour les configurations (nP = 1000, P ≥ 12) et


(nP = 2000, P = 28), pour lesquelles le volume de calcul est insuffisant par rapport
au nombre de processeurs. Pour nP ≥ 5000, cette efficacité ne descend pas en-dessous
de 79 %, et ce quel que soit P , ce qui permet d’atteindre des gains de 2 à 26 avec 2 à
28 processeurs.
Ces résultats sont similaires, à quelques pourcents près, à ceux obtenus avec une
version de l’application parallélisée « à la main » avec MPI, ce qui confirme que le
surcoût du à QUAFF est négligeable dans ce cas.

L’usage de QUAFF a permis à un programmeur non spécialiste du parallélisme


d’obtenir un code opérationnel en moins de deux semaines. Par comparaison, il avait
fallu presque deux mois de travail à un programmeur ayant un profil similaire pour
écrire une version parallèle fondée sur le seul usage de MPI.
La différence sensible de niveau d’abstraction entre les deux approches explique
évidemment ce gain en termes de programmabilité. Y contribue aussi le le découplage
strict rendu possible par QUAFF entre les fonctions de calcul séquentielles et le code
lié à la parallélisation, ce qui simplifie considérablement la phase de mise au point et
de déboguage.

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

Le principal but de ce travail était de démontrer la faisabilité et l’efficacité des tech-


niques d’implémentation de langages orientés domaine (DSL) dans un langage hôte
tel que C++ pour la programmation parallèle. Nous avons présenté une application
de ces techniques en implantant un DSL fondé sur le concept de squelettes algorith-
miques sous la forme d’une bibliothèque active écrite en C++. Cette bibliothèque,
baptisée QUAFF, offre à la fois un haut niveau d’abstraction et des performances très
proches de celles obtenues avec un code parallélisé « à la main » avec des outils de plus
bas niveau comme MPI ou OPENMP. Ceci est rendu possible par la transformation du
code source, faisant appel à des abstractions de haut niveau, en un code résiduel ne
faisant appel qu’à des primitives MPI. Cette transformation est faite entièrement à la
compilation. Elle repose sur une représentation intermédiaire explicite (sous la forme
Métaprogrammation pour le parallélisme 671

de réseau de processus communiquants ici) dérivée systématiquement de la syntaxe


abstraite du DSL considéré.
Notons au passage que le recours à une telle représentation intermédiaire expli-
cite des programmes simplifie l’extension et l’adaptation de QUAFF à d’autres cibles
ou l’ajout de nouveaux squelettes. Des travaux visant à porter QUAFF sur le proces-
seur CELL sont d’ailleurs en cours (Falcou et al., 2008) et donnent d’ores et déjà
des résultats encourageants. Corrollairement, nous travaillons aussi sur la définition et
l’intégration d’autres squelettes (de type DivideAndConquer par exemple) à QUAFF.
Outre ces extensions immédiates, nous envisageons deux pistes de travail à plus
long terme.
Transformations de plus haut niveau. En offrant la possibilité de transformer sta-
tiquement les programmes (au niveau de l’arbre de syntaxe abstrait ou de la réprésen-
tation intermédiaire sous forme de RPSC ici), les techniques de métaprogrammation
sont susceptibles d’apporter des réponses efficaces à certains problèmes classiques liés
aux approches par squelettes (suppression de synchronisations superflues par fusion
de tâches dans un pipeline ou augmentation du grain dans une ferme par exemple).
Cet aspect est clairement sous-exploité dans la version de QUAFF décrite ici (les opti-
misations se bornant à remplacer une représentation sous forme de tableau dynamique
contenant des instances de classes abstraites par un appel direct des fonctions utilisa-
teurs au sein de la structure de gestion des communciations). Il faudrait évaluer dans
ce contexte la difficulté qu’il y a à implémenter des règles de transformation et d’opti-
misation complexes sous la forme de métaprogrammes template en C++ (par rapport,
par exemple, à une implémentation dans un langage tel que MetaOcaml qui offre un
niveau d’expressivité bien supérieur pour cela). En effet, bien que Turing-complet,
le sous-langage offert par les patrons C++ est limité par une syntaxe extrêmement
verbeuse et par des difficultés de mise au point dues aux messages d’erreurs souvent
cryptiques du compilateur. Afin d’évaluer cette voie – et de procéder à une compa-
raison des deux langages hotes dans ce contexte –, nous avons commencé à déve-
lopper une implémentation de SKL reposant sur la même représentation intermédiaire
des programmes, mais en MetaOcaml justement (Sérot et al., 2008). Le cas échéant,
on pourrait d’ailleurs envisager une approche dans laquelle les métaprogrammes se-
raient écrits en MetaOcaml puis réécrits en C++ avec un outil comme MetaGene par
exemple (Maes, 2004).
Preuve de métaprogramme. Le processus de transformation sur lequel s’appuie
QUAFF, bien que formalisé par les règles de production données dans la section 2,
n’a fait l’objet d’aucune preuve. Ce n’était pas l’objet du travail décrit ici. Au de-
meurant, la portée d’une telle preuve serait fortement limitée par l’absence, à l’heure
actuelle, de toute formalisation du métalangage à base de template en C++, qui empê-
cherait pratiquement d’étendre ladite preuve de correction au code résiduel. Il serait
certainement intéressant de voir dans quelle mesure la même approche implantée en
MetaOcaml (comme décrit dans (Sérot et al., 2008) par exemple) pourrait faire l’objet
d’une preuve complète.
672 RSTI - TSI. Volume 28 – n° 5/2009

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

2005, 13-16 September 2005, Department of Computer Architecture, University of Malaga,


Spain, p. 803-810, 2005.
Danelutto M., Teti P., « Lithium: A Structured Parallel Programming Enviroment in Java »,
Proceedings of Computational Science - ICCS 2002, Springer Verlag, p. 844-853, 2002.
Darlington J., Field A. J., Harrison P. G., Kelly P. H. J., Sharp D. W. N., Wu Q., While R. L.,
« Parallel Programming Using Skeleton Functions », Parallel Architectures and Languages
Europe, Springer-Verlag, 1993.
Engler D. R., « Incorporating Application Semantics and Control into Compilation », Procee-
dings of USENIX Conference on Domain-Specific Languages, p. 103-118, 1997.
Falcou J., Chateau T., Sérot J., Lapresté J., « Real Time Parallel Implementation of a Particle
Filter Based Visual Tracking », CIMCV 2006 - Workshop on Computation Intensive Me-
thods for Computer Vision at ECCV 2006, Grazz, 2006.
Falcou J., Saidani T., Lacassagne L., Etiemble D., « Programmation par squelettes algorith-
miques pour le processeur CELL », SYMPA 2008, 2008.
Falcou J., Sérot J., « CamlG4: une bibliothéque de calcul de parallèle pour Objective Caml »,
14es Journées Francophones des Langages Applicatifs, p. 139-152, 2003.
Falcou J., Sérot J., « E.V.E., An Object Oriented SIMD Library », Proceedings of Practical
Aspects of High-level Parallel Programming,ICCS 2004, vol. 3, p. 323-330, 2004.
Fatni A., Houzet D., Basille J.-L., « The C// Data Parallel Language on a Shared Memory
Multiprocessor », in I. C. S. Press (ed.), CAMP’97, Octobre, Los Alamitos, p. 10-15, 1997.
Freund Y., Schapire R. E., « A decision-theoretic generalization of on-line learning and an
application to boosting », European Conference on Computational Learning Theory, p. 23-
37, 1995.
Futamura Y., « Partial Evaluation of Computation Process - An Approach to a Compiler-
Compiler », Higher-Order and Symbolic Computation, vol. 12, n° 4, p. 381-391, 1999.
Ginhac D., Prototypage rapide d’applications paralléles de vision artificielle par squelettes fonc-
tionnels, PhD thesis, Université Blaise Pascal, Clermont-Ferrand, 1999.
Gorlatch S., « Send-receive considered harmful: Myths and realities of message passing », ACM
Trans. Program. Lang. Syst., vol. 26, n° 1, p. 47-56, 2004.
Herrmann C. A., « Generating message-passing programs from abstract specifications by partial
evaluation », Parallel Processing Letters, vol. 15, n° 3, p. 305-320, 2005.
Herrmann C. A., Langhammer T., « Combining partial evaluation and staged interpretation in
the implementation of domain-specific languages », Sci. Comput. Program., vol. 62, n° 1,
p. 47-65, 2006.
Herrmann C. A., Lengauer C., « HDC: A Higher-Order Language for Divide-and-Conquer »,
Parallel Processing Letters, vol. 10, n° 2/3, p. 239-250, 2000.
Hill J. M. D., McColl B., Stefanescu D. C., Goudreau M. W., Lang K., Rao S. B., Suel T., Tsan-
tilas T., Bisseling R. H., « BSPlib: The BSP programming library », Parallel Computing,
vol. 24, n° 14, p. 1947-1980, 1998.
Irigoin F., Jouvelot P., Triolet R., « Semantical interprocedural parallelization: an overview of
the PIPS project », ICS ’91: Proceedings of the 5th international conference on Supercom-
puting, ACM, New York, NY, USA, p. 244-251, 1991.
Isard M., Blake A., « Condensation – conditional density propagation for visual tracking »,
International Journal of Computer Vision, vol. 29, n° 1, p. 5-28, 1998.
674 RSTI - TSI. Volume 28 – n° 5/2009

Isard M., Maccormick J., « BraMBLe: A Bayesian Multiple-Blob Tracker », Proceedings of


International Conference on Computer Vision, vol. 2, p. 34-41, 2001.
Ishikawa Y., Hori A., Sato M., Matsuda M., « Design and implementation of metalevel archi-
tecture in C++ - MPC++ approach », Proceedings of the Reflection ’96 Conference., San
Francisco, USA, p. 153-166, 1996.
Jones N. D., « An introduction to partial evaluation », ACM Comput. Surv., vol. 28, n° 3, p. 480-
503, 1996.
Jones N. D., Glenstrup A. J., « Program generation, termination, and binding-time analysis »,
ICFP ’02: Proceedings of the seventh ACM SIGPLAN international conference on Func-
tional programming, ACM Press, New York, NY, USA, p. 283-283, 2002.
Kalman R. E., « A New Approach to Linear Filtering and Prediction Problems », Transactions
of the ASME–Journal of Basic Engineering, vol. 82, n° Series D, p. 35-45, 1960.
Koebel C., Loveman D., Schreiber R., Jr. G. S., Zosel M., The High Performance Fortran Hand-
book, MIT Press, Cambridge, 1994.
Kuchen H., « A Skeleton Library », Euro-Par ’02: Proceedings of the 8th International Euro-
Par Conference on Parallel Processing, Springer-Verlag, London, UK, p. 620-629, 2002.
Maes F., « Metagene, a C++ meta-program generation tool », Proceedings of the Workshop
on Multiple Paradigm with OO Languages (MPOOL; in conjunction with ECOOP), Oslo,
Norway, June, 2004.
Mark W. R., Glanville R. S., Akeley K., Kilgard M. J., « Cg: a system for programming graphics
hardware in a C-like language », ACM Trans. Graph., vol. 22, n° 3, p. 896-907, 2003.
Niculescu-Mizil A., Caruana R., « Obtaining Calibrated Probabilities from Boosting », Proc.
21st Conference on Uncertainty in Artificial Intelligence (UAI ’05), AUAI Press, 2005.
Schapire R. E., « A Brief Introduction to Boosting », IJCAI, p. 1401-1406, 1999.
Sérot J., Falcou J., « Métaprogrammation fonctionnelle appliquée à la génération d’un DSL
dédié à la programmation parallèle », 19eme Journées Francaises des Langages Applicatifs,
2008.
Sheard T., « Accomplishments and Research Challenges in Meta-programming », Lecture
Notes in Computer Science, vol. 2196, p. 2-45, 2001.
Skillicorn D. B., Talia D., « Models and languages for parallel computation », ACM Computing
Surveys, vol. 30, n° 2, p. 123-169, 1998.
Taha W., « MetaOCaml – A compiled, type-safe multi-stage programming language. », , Avai-
lable online from http://www.metaocaml.org/, March, 2003.
Teulière V., Brun O., « Parallelisation of the particle filtering technique and application
to Doppler-bearing tracking of maneuvering sources », Parallel Comput., vol. 29, n° 8,
p. 1069-1090, 2003.
Tieu K., Viola P., « Boosting Image Retrieval », International Journal of Computer Vision, vol.
56, n° 1, p. 17-36, 2004.
Unruh E., Prime number computation, Technical Report n° ANSI X3J16-94-0075/ISO WG21-
462., C++ Standard Commitee, 1994.
Veldhuizen T. L., « Expression templates », C++ Report, vol. 7, n° 5, p. 26-31, 1995.
Veldhuizen T. L., C++ Templates are Turing Complete, Technical report, 2000.
Métaprogrammation pour le parallélisme 675

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.

Article reçu le 14 mai 2008


Accepté le 18 septembre 2008

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.

You might also like