WK PhDThesis2012
WK PhDThesis2012
WK PhDThesis2012
UFR STMIA
THÈSE
pour l’obtention du
par
Wilfried KIRSCHENMANN
Composition du jury
Président : Jocelyn SEROT Professeur, Université Blaise Pascal, Clermont-Ferrand
Rapporteurs : Denis BARTHOU Professeur, Université de Bordeaux
François BODIN Professeur, IRISA, Rennes
Examinateurs : Pascal BOUVRY Professeur, Université du Luxembourg
André SCHAFF Professeur, Université de Lorraine, Nancy
Stéphane VIALLE Professeur, Supélec campus de Metz
(directeur de thèse)
Laurent PLAGNE Docteur en physique, chercheur à EDF R&D, Clamart
(co-encadrant de la thèse)
Remerciements
Aux randonneurs qui, croisant mon chemin, ont écarté les branches qui sur ma route. . .
Un simple sourire dans le métro – de compassion, de joie ou plus simplement de bonne humeur –,
un encouragement, une discussion animée, un repas partagé ou une présence dans les moments
de doute : autant d’empreintes imperceptibles traçant les sentiers de cette thèse.
Aux guides qui m’ont montré des voies invisibles entre les difficulté et m’ont indiqué le nord
lors de mes égarements. . .
Martine et Raymond, pour tout ce que vous avez fait pour moi au cours de ces douze dernières
années, je vous remercie ! Votre aide n’aurait probablement pas abouti au même résultat sans
la participation active d’Anne-Cécile et Laurent. Cousin, Cousine merci pour tous les moments
que nous avons partagés et pour votre soutien !
Vous avez contribué à façonner le chemin que j’ai emprunté et, si je ne vous dois pas tout, je
vous dois beaucoup.
Aux éclaireurs qui m’ont aidé à dessiner mon chemin sur une carte et à lire celles des autres. . .
Jean-Philippe, nos nombreuses nuits entre gris clair et gris foncé, révisions ponctuées de débats de
société, ont été autant d’occasion pour moi de confronter ma vision du monde et de comprendre
ses fondements culturels. C’est également par ton intermédiaire que j’ai été amené à côtoyer
Alexandre, Élodie, Jean, Kamel, Ludovic, Nicolas et Romain. AG1D, un nom de code, un cercle
ouvert d’échanges et de rencontres, un lieu de traditions, d’engagements. . . D’amitié. Alexandre,
tu m’as montré des chemins de travers et m’a amené à découvrir d’autres mondes.
Continuons à faire tomber les tours de guet de nos citadelles et à bâtir de nouveaux mondes.
Aux voyageurs éclairés qui m’ont montré des passerelles vers des univers différents. . .
Ariane, à cause de toi, je me disperse ! Aidé d’Alexandre, d’Alexis-Michel, d’Antoine, d’Augustin,
de Bruno, de Christelle, de Daria, de Didier, de François, de Françoise, de Jean-Claude, de
Joséphine, de Léa, de Patrice, de Pierre et de Philomène, tu m’a amené à m’intéresser à presque
tous les sujets nécessaires pour se forger un regard sur notre monde.
Aux compagnons qui m’ont aidé à choisir une destination, nouveau point de départ. . .
Alia, Andréas, Angélique, Antoine, Bruno, Christophe, Christian, Daniel, David, Éléonore, Éric,
Évelyne, Fannie, Flora, Frank, Franck, François, Gérald, Guillaume, Guy, Hugues, Ivan, Jean-
Philippe, Laurent, Marc-André, Marie-Agnès, Mark, Matthieu, Maxime, Olivier, Sethy, Solène,
Stéphane, Tanguy, Thierry et Véronique. Grâce à vous, le mot collègue ne signifie pas seulement
co-travailleur : il comprend des notions d’amitié. Nos discussions sur l’égalité entre les vrais gens
– hommes ou femmes – reprendront, je n’en doute pas, autour d’un verre en montagne.
Aux yeux affûtés qui, repassant derrière moi, m’ont aidé à bien indiquer mon chemin. . .
Laurent et Stéphane vous avez largement contribué à rendre ce document compréhensible. Sec-
ondés d’Alexandre, d’Alexis-Michel, de Léa, de Martine et de Raymond, vous m’avez même
convaincu de le rendre lisible !
Aux oreilles attentives qui ont accepté d’entendre mon histoire, parfois en venant de loin. . .
Denis et François, vous avez accepté d’aider André et Pascal à juger, sous la présidence de Joce-
lyn, la qualité scientifique de ce parcours. Vous avez été assisté en cela de Laurent et Stéphane
qui m’ont conseillé tout au long de cette thèse. Si les témoins de cette journée étaient trop
nombreux pour être listés sur cette page, je ne les oublie pas pour autant.
. . . J’adresse mes remerciements les plus sincères.
i
ii
Table des matières
Chapitre 1
Introduction
Chapitre 2
Programmation multicible : une structure de données par cible ?
iii
Table des matières
Chapitre 3
État de l’art des environnements de développement parallèle multicible
Chapitre 4
MTPS : MultiTarget Parallel Skeleton
Chapitre 5
Conception et réalisation d’un démonstrateur multicible de Legolas++
iv
5.1.2 Les matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.1.3 Les solveurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.2 Contraintes pour une version multicible de Legolas++ . . . . . . . . . . . . . . . 107
5.2.1 L’expérience MTPS : rappel des contraintes pour un code multicible . . . 107
5.2.2 Famille de problèmes compatibles avec une implémentation multicible . . 108
5.3 Un démonstrateur de Legolas++ multicible . . . . . . . . . . . . . . . . . . . . . 109
5.3.1 Structures de données Legolas++ et vues parallèles . . . . . . . . . . . . 110
5.3.2 Un démonstrateur capable de cibler les différentes générations de pro-
cesseurs X86_64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5.3.3 Utilisation et limitations du démonstrateur . . . . . . . . . . . . . . . . . 116
Chapitre 6
Analyse des performances du démonstrateur
6.1 Premier cas : les blocs ont une structure bande symétrique . . . . . . . . . . . . . 121
6.1.1 Structure de la matrice A et paramètres du problème . . . . . . . . . . . 121
6.1.2 Performances d’une implémentation idéale . . . . . . . . . . . . . . . . . . 122
6.1.3 Performances de MTPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
6.1.4 Performances du démonstrateur Legolas++ multicible . . . . . . . . . . . 129
6.1.5 Bilan : accélérations obtenues . . . . . . . . . . . . . . . . . . . . . . . . . 130
6.2 Deuxième cas : les blocs ont une structure bande symétrique sur deux niveaux . 132
6.2.1 Structure de la matrice A et paramètres du problème . . . . . . . . . . . 133
6.2.2 Performances du démonstrateur . . . . . . . . . . . . . . . . . . . . . . . . 134
6.3 Bilan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Chapitre 7
Conclusions et perspectives
Annexe
Annexe A
Du polymorphisme dynamique au polymorphisme statique
v
Table des matières
Annexe B
Introduction aux formats de stockage creux Compressed Row Storage
Annexe C
Définitions Legolas++
Glossaire 159
Bibliographie 161
vi
Liste des figures
vii
Liste des figures
2.11 Influence de l’entrelacement des données sur les performances. Machine test :
32
1×4 SandyBridge – exécution séquentielle CPU . . . . . . . . . . . . . . . . . . . . 65
2.12 Influence de l’entrelacement des données sur les performances. Machine test :
32
2×4 Nehalem – exécution parallèle CPU : SSE et OpenMP . . . . . . . . . . . . . 65
2.13 Influence de l’entrelacement des données sur les performances. Machine test :
32
1×4 SandyBridge – exécution parallèle CPU : SSE et OpenMP . . . . . . . . . . . 65
2.14 Influence de l’entrelacement des données sur les performances. Machine test :
32
1×4 SandyBridge – exécution parallèle : AVX et OpenMP . . . . . . . . . . . . . . 66
2.15 Influence de l’entrelacement des données sur les performances. Machine test :
Fermi – exécution parallèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.1 Matrice diagonale à blocs bandes et symétriques. Cette matrice contient 4 blocs
tridiagonaux de taille 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4.2 structure de données de la matrice A et du vecteur X sous formes de classes
appartenant à V<Wn > telles que définies dans le tableau 4.1. . . . . . . . . . . . 88
4.3 La matrice A et le vecteurs X sont regroupés en un vecteur de 2-tuples . . . . 89
4.4 La collection de 2-tuples est transformée en un 2-tuple de structures de
données bidimensionnelles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.5 Un maillage spatial 2D issu de la chaîne COCAGNE. . . . . . . . . . . . . . . . . 94
4.6 Une application enchaîne différents contextes d’exécution. . . . . . . . . . . . . . 95
4.7 L’implémentation du concept de collection s’appuie sur la description des élé-
ments fournie par l’utilisateur et sur les fonctions d’entrelacement fournies par la
classe décrivant l’architecture matérielle afin de générer une structure de données
optimisée. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
4.8 MTPS fournit des vues permettant d’accéder aux différents éléments d’une collection
dont le stockage est optimisé. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
4.9 Les opérateurs parallèles utilisent des vues pour appliquer les fonctions définies
par l’utilisateur aux différents éléments de la collection . . . . . . . . . . . . . . 98
4.10 Vue d’ensemble du fonctionnement de MTPS. . . . . . . . . . . . . . . . . . . . . 98
5.1 Exemple d’une matrice pouvant être représentée par une collection homogène . 110
5.2 Sérialisation des données d’une structure à deux niveau. . . . . . . . . . . . . . . 111
5.3 Transformation du stockage d’un vecteur 3D en structure bidimensionnelle. . . . 112
5.4 Transformation d’une matrice à deux niveaux en vecteur à deux niveaux. . . . . 114
6.3 Intensité arithmétique sur CPU pour différentes configurations du problème. . . . 124
6.4 Comparaison entre ia et les intensités arithmétiques critiques de 2×4 32 Nehalem. . . 125
6.5 Comparaison entre ia et les intensités arithmétiques critiques de 1×4 32 SandyBridge. 125
tielle). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.8 Performances de l’implémentation optimisée sur 1×4 32 SandyBridge (exécution séquen-
tielle). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.9 Performances de l’implémentation optimisée sur 2×4 32 Nehalem (exécution parallèle). 127
viii
6.10 Performances de l’implémentation optimisée sur 1×4 32 SandyBridge (exécution par-
allèle). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.11 Performances de l’implémentation optimisée sur Fermi. . . . . . . . . . . . . . . . 128
6.12 Performances de MTPS sur 2×4 32 Nehalem (parallèle). . . . . . . . . . . . . . . . . 128
6.13 Performances de MTPS sur 1×4 32 SandyBridge (parallèle). . . . . . . . . . . . . . . 128
6.14 Performances de MTPS sur Fermi. . . . . . . . . . . . . . . . . . . . . . . . . . . 129
6.15 Performances du démonstrateur Legolas++ sur 2×4 32 Nehalem (parallèle). . . . . . 130
6.16 Performances du démonstrateur Legolas++ sur 1×4 32 SandyBridge (parallèle). . . . 130
6.17 Accélérations apportées par les différentes approches sur 2×4 32 Nehalem. . . . . . . 131
6.18 Accélérations apportées par les différentes approches sur 1×4 32 SandyBridge. . . . . 131
6.19 Structure de la matrice A utilisée dans le second cas test. . . . . . . . . . . . . . 133
6.20 Performances du démonstrateur Legolas++ sur 2×4 32 Nehalem. . . . . . . . . . . . 134
6.21 Performances du démonstrateur Legolas++ sur 1×4 32 SandyBridge. . . . . . . . . . 134
32
6.22 Accélérations apportées par Legolas++ sur 2×4 Nehalem. . . . . . . . . . . . . . . 135
6.23 Accélérations apportées par Legolas++ sur 1×4 32 SandyBridge. . . . . . . . . . . . 135
ix
Liste des figures
x
Liste des tableaux
xi
Liste des tableaux
xii
Les experts sont formels et unanimes : en règle
générale, l’ordinateur le mieux adapté à vos
besoins est commercialisé environ deux jours
après que vous ayez acheté un autre modèle !
Dave Barry (1947 – présent)
Prix Pulitzer du commentaire en 1988
Chapitre 1
Introduction
L’utilisation de logiciels de calcul scientifique permet aux acteurs industriels comme EDF
de simuler des expériences qui seraient trop coûteuses, trop longues, trop compliquées voire
impossibles à mettre en œuvre dans la pratique [1]. Dans le cas des problèmes trop spécifiques
pour être traités par les logiciels disponibles sur le marché, l’industrie doit développer ses propres
outils de simulation.
La complexité algorithmique et la taille de ces logiciels peuvent entraver la maintenance et
la mise à jour de leurs modèles physiques et mathématiques. Lors de la mise au point d’un
nouveau logiciel de simulation, une grande attention est donc portée aux choix influençant sa
maintenance future. Afin de réduire le coût de cette maintenance, les techniques de génie logiciel
privilégient les stratégies minimisant le nombre de branches dans le code. Cela signifie générale-
ment la mise au point de modules génériques dépendant le moins possible des spécificités des
ordinateurs ou des modèles physiques et mathématiques. Ces modules peuvent alors être utilisés
plusieurs fois dans une ou plusieurs applications, dans des contextes différents. Les modifications
apportées à un module sont alors visibles dans toute les applications. Au contraire, la multiplica-
tion de branches spécialisées pour différents cas particuliers augmente le travail de maintenance
et multiplie les possibilités d’introduction d’erreurs.
Parallèlement à cette problématique de maintenance, l’augmentation des besoins en nombre
et en précision de ces simulations conduit à porter un grand intérêt à la réduction des temps
d’exécution de ces logiciels. Cette optimisation des performances d’un logiciel implique une
spécialisation du code afin de l’adapter aux différents problèmes à résoudre et aux différentes
architectures matérielles utilisées. Afin de pouvoir optimiser les différents problèmes sur les
différentes architectures matérielles ciblées par l’application, différentes branches d’exécution
doivent alors être mises en œuvre.
Les choix d’architecture logicielle et d’implémentation traduisent donc un compromis entre
maintenabilité et performances. Une implémentation favorisant une plus grande réutilisabilité
du code améliorera la maintenabilité mais peut s’opposer à l’optimisation des performances.
Inversement, une implémentation permettant la spécialisation du code favorisera l’obtention de
bonnes performances au détriment de sa maintenabilité.
Dans le cadre de cette thèse, nous cherchons à identifier les approches permettant d’améliorer
la compatibilité entre les activités de maintenance et d’optimisation. Ceci permettra de mieux
spécialiser les applications scientifiques pour différentes générations d’architectures matérielles
sans augmenter les coûts de maintenance.
1
Chapitre 1. Introduction
Sommaire
1.1 Les codes scientifiques : un compromis entre maintenabilité et per-
formances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Recensement des solutions pour le domaine de l’algèbre linéaire . . 6
1.2.1 Limitation des bibliothèques procédurales . . . . . . . . . . . . . . . . . 6
1.2.2 Une plus grande expressivité des opérations d’algèbre linéaire . . . . . . 8
1.2.2.1 Les interfaces procédurales : des interfaces de programmation
peu expressives . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.2.2 La composition de fonctions avec les langages procéduraux . . 11
1.2.2.3 Les langages fonctionnels . . . . . . . . . . . . . . . . . . . . . 12
1.2.2.4 Les langages spécialisés à un domaine . . . . . . . . . . . . . . 13
1.2.2.5 La programmation orientée objet . . . . . . . . . . . . . . . . . 14
1.2.2.6 La programmation générative et les bibliothèques actives . . . 16
1.2.3 Vers une description plus avancée de la structure des matrices . . . . . . 18
1.2.3.1 Les interfaces procédurales : des structures de données peu
évoluées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.3.2 Autres solutions pour exploiter les structures de données . . . 21
1.2.4 Notre objectif : la maintenabilité des langages dédiés et les performances
des bibliothèques optimisées . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.3 Legolas++ : une bibliothèque dédiée aux problèmes d’algèbre linéaire 22
1.4 Objectif de la thèse : conception d’une version multicible de Lego-
las++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.5 Plan de lecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2
1.1. Les codes scientifiques : un compromis entre maintenabilité et performances
3
Chapitre 1. Introduction
méthodes d’approximation sont disponibles dans la chaîne de calcul COCAGNE afin de résoudre
cette équation. À chacune de ces approximations correspond un compromis particulier entre le
temps de résolution et la précision des résultats.
– La méthode SPN [21, 22, 18] correspond à l’approximation la plus grossière disponible
au sein de la chaîne de calcul COCAGNE. Elle permet l’obtention de résultats plus pré-
cis que la méthode de diffusion utilisée dans COCCINELLE tout en nécessitant rela-
tivement peu de temps de calcul. C’est la méthode utilisée pour valider les calculs de
COCCINELLE [17, 16]. La consommation mémoire et les temps de résolution induits par
cette méthode permettent d’effectuer des calculs directement sur station de travail. Il n’est
donc pas envisagé d’utiliser les résultats des différents travaux de parallélisation de cette
méthode sur machine à mémoire distribuée [23, 24].
– La méthode Sn [25, 22, 18] correspond à une approximation plus précise mais plus coûteuse
en temps de calcul.
– La méthode des caractéristiques [26, 22, 18] est en cours de développement. Cette méthode
permettra l’obtention de résultats plus fidèles aux modèles physiques et à la structure
géométrique des réacteurs [26]. Si la consommation mémoire et les temps de résolution
induits limitaient cette méthode aux seuls problèmes 2D [27, 28, 29], l’augmentation au
cours des dernières années de la puissance de calcul des ordinateurs ainsi que de leur
capacité mémoire permet depuis de concevoir des solveurs visant à résoudre des problèmes
3D [30, 31, 32].
Afin d’améliorer les performances de COCAGNE, le groupe Analyse et Modèles Numériques
d’EDF R&D a réalisé différents travaux d’optimisations pour réduire la consommation mémoire
et le temps d’exécution de ces solveurs [33, 34, 35, 23, 36, 37].
Si le temps d’exécution des codes de calcul industriels est une contrainte importante, ces
derniers sont également soumis à un autre impératif : la longueur de leur cycle de vie. Le code
de mécanique Code-Aster a par exemple fêté ses 20 ans en 2009 tandis que la première version de
COCCINELLE a été lancée en 1983 [38]. Durant toute cette période, le code doit être maintenu.
Ce travail de maintenance inclut trois types de travaux :
– la correction des erreurs,
– l’ajout de nouvelles fonctionnalités afin de répondre aux nouveaux besoins,
– et l’adaptation du code aux nouvelles architectures matérielles.
Par exemple, en 2010, une nouvelle version de COCCINELLE est sortie afin de permettre de
faire des calculs de cœur pour l’EPR [39].
Face à des durées de vie aussi longues, l’évolution des architectures matérielles des ordinateurs
est extrêmement rapide. Deux ans s’écoulent en moyenne entre deux évolutions technologiques
majeures des micro-processeurs centraux (Central Processing Unit ou CPU) comme l’illustre le
tableau 1.1 représentant les évolutions des CPU X86 1 . Ce tableau, qui ne concerne ni les autres
familles de processeurs généralistes (PowerPC, ARM), ni les accélérateurs de calcul (Cell, GPUs),
ne fait apparaître que les évolutions matérielles ayant un impact sur les méthodes d’optimisation
des performances d’un code. Nous reviendrons plus longuement au chapitre 2 sur ces éléments
ainsi que sur les différentes technologies présentées dans ce tableau. Nous verrons alors que,
depuis 2002, l’augmentation du nombre d’opérations de calcul effectuées en une seconde s’appuie
uniquement sur le parallélisme. En conséquence, un code non parallèle ne peut exploiter qu’une
proportion toujours plus faible de la puissance de calcul disponible. Ainsi, sur notre machine de
1. Les processeurs de la famille X86 correspondent aux processeurs « INTEL et compatibles ». Les autres
fabricants de processeurs X86 sont AMD et VIA.
4
1.1. Les codes scientifiques : un compromis entre maintenabilité et performances
Degré de parallélisme
Apporté Cumulé
Date Technologie Conditions d’utilisation
Précision Précision
simple double simple double
Utilisation de fonctions très bas niveau
1997 3DNow ! 2 - 2 -
Modification des structures de données
Utilisation de fonctions très bas niveau
1999 SSE 4 - 4 -
Modification des structures de données
Utilisation de fonctions très bas niveau
2001 SSE2 4 2 4 2
Modification des structures de données
Modification des tailles des types
2003 X86-64 -
Recompilation des bibliothèques
2005 Multicœur1 nc 4nc 2nc Écriture de code multithreadé
Disparition Temps d’accès mémoire non uniformes
2005 -
du FSB2 Modification des structures de données
2010 Cœurs GPU Variable Prise en compte de l’hétérogénéité
Utilisation de fonctions très bas niveau
2011 AVX 8 4 8nc 4nc
Modification des structures de données
1
nc définit le nombre de cœurs d’un processeur multicœur. nc est typiquement compris entre 2
et 16.
2
En 2005, AMD remplace le Front Side Bus par sa technologie Direct Connect (DC)afin de sup-
porter les accès mémoire non uniformes. INTEL attendra le lancement de l’architecture Nehalem
en 2008 pour introduire un changement équivalent en introduisant la technologie QuickPath In-
terconnect (QPI). En 2011, QPI permet l’obtention de meilleurs débits de données que DC.
test 2 2×4
32 Nehalem comportant 2 processeurs quadri-cœurs Xeon E5504 (2.0 GHz) embarquant
la technologie SSE (cf. section 2.3), un code purement séquentiel (ni parallélisé, ni vectorisé) ne
peut exploiter plus de 1/32è de la puissance de calcul disponible.
L’optimisation d’un code de calcul pour une génération de processeurs ne peut donc garantir
l’obtention d’un code capable d’exploiter efficacement les générations suivantes. Les coûts de
mise en œuvre pour le développement, la maintenance et la validation étant très importants,
il n’est pas envisageable de ré-optimiser les différents codes de calcul pour chaque génération
d’ordinateurs. Un compromis doit donc être trouvé entre les coûts de maintenance d’un côté et
l’optimisation des performances de l’autre.
Bien que cette recherche de compromis soit commune à tous les grands codes, nous nous
intéresserons dans cette thèse aux solutions permettant de réduire les coûts d’optimisation des
performances de la chaîne de calcul COCAGNE. Dans ce but, nous allons nous focaliser sur
les solutions permettant d’améliorer l’adaptabilité du solveur SPN aux nouvelles architectures.
Ce solveur résout un problème d’algèbre linéaire et différentes approches existent aujourd’hui
afin de mettre au point des applications d’algèbre linéaire pouvant exploiter les évolutions tech-
nologiques des processeurs dès leur apparition. Nous étudierons ces solutions dans la prochaine
section.
32
2. 2×4 Nehalem se lit : machine disposant de 2 processeurs d’architecture Nehalem comportant 4 cœurs chacun.
Chaque cœur comporte des unités SIMD à 4 voies. Le degré de parallélisme explicite disponible (multicœur ×
SIMD) sur cette machine est donc de 32.
5
Chapitre 1. Introduction
6
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
Grâce à cette standardisation, les applications construites autour de l’interface BLAS devi-
ennent portables sur les différentes plate-formes matérielles. Afin de gagner des parts de marché,
les constructeurs d’ordinateurs et de processeurs cherchent à fournir à leurs clients les meilleures
performances possibles. Pour y parvenir, ces derniers proposent donc leurs propres implémenta-
tions des BLAS. Actuellement, la majorité des constructeurs proposent leur implémentation des
BLAS, comme par exemple :
– la Math Kernel Library (MKL) [50], fournie par INTEL ;
– la AMD Core Math Library (ACML) [51], fournie par AMD ;
– la Engineering and Scientific Subroutine Library (ESSL) [52], fournie par IBM.
Outre ces implémentations fournies par les constructeurs, différents travaux portés par la
communauté du calcul intensif existent, comme par exemple les projets ouverts ATLAS et
GOTO :
– ATLAS [53] (Automatically Tuned Linear Algebra Software) est une bibliothèque dont le
processus d’installation effectue des mesures de performances afin de générer une implé-
mentation optimisée pour l’ordinateur ;
– GOTO [54, 55] est une bibliothèque mise au point en assembleur par Kazushige Goto et
qui est réputée pour être une des implémentations les plus performantes pour processeurs
X86.
Des standards de facto existent également pour d’autres domaines scientifiques. Par exemple,
les bibliothèques actuelles de transformées de Fourier implémentent la même interface que la
version 3 de Fastest Fourier Transform in the West (FFTW) [56]. Depuis 2001, la GNU Scientific
Library (GSL) [57] propose d’unifier les interfaces des bibliothèques de calcul utilisées dans de
nombreux domaines scientifiques (algèbre linéaire, transformée de Fourier, interpolations, . . . ).
Une application dont la plus grande partie du temps d’exécution correspond à l’appel à
de telles bibliothèques de calcul bénéficie automatiquement des performances apportées par les
nouvelles architectures en changeant de version de bibliothèque. Le compromis entre maintenance
et performance est alors trivial : le développeur de l’application scientifique prend en charge sa
maintenance tandis que l’optimisation des performances est externalisée vers les bibliothèques
scientifiques.
Cependant, il n’est pas toujours possible d’utiliser efficacement les bibliothèques disponibles.
En effet, la majorité des bibliothèques orientées performances fournissent un jeu de fonctions
fixe et fini. Ce jeu de fonctions peut s’avérer être trop rigide pour répondre efficacement aux
besoins des applications clientes. Ainsi, dans son analyse de l’optimisation des performances sur
le CRAY I, Jack Dongarra montre que l’utilisation des BLAS1 conduit à deux limitations :
– BLAS1 ne permet pas d’exprimer des opérations présentant deux niveaux de parallélisme ;
– BLAS1 ne définit pas de matrices.
Ceci illustre les deux catégories de limitations inhérentes à ce type de bibliothèque : un manque
d’expressivité des opérations et un manque de moyen de description pour les objets mathé-
matiques complexes. L’ajout des interfaces BLAS2 et BLAS3 n’a fait que repousser ces deux
limitations. Ces limitations, qui existent toujours en 2011 comme nous allons le montrer, sont
inhérentes à la nature des interfaces BLAS. Ces interfaces définissent des fonctions ou procé-
dures, nous parlerons donc d’interfaces procédurales. Nous illustrerons dans la prochaine section
l’impact des limitations des bibliothèques à interfaces procédurales sur les performances des
codes.
7
Chapitre 1. Introduction
Sur notre machine de tests 2×4 32 Nehalem (cf. section 2.3), cette implémentation séquentielle ne
permet pas d’obtenir des performances optimales. En effet, afin d’exploiter efficacement le pro-
cesseur, cette implémentation doit être parallélisée. Le degré et le type de parallélisme dépendent
de l’architecture matérielle ciblée, comme nous le verrons au chapitre 2.
Nous souhaitons externaliser cette charge d’optimisation vers une bibliothèque externe. Afin
de juger l’efficacité des différentes approches sur le plan des performances, nous allons comparer
les performances obtenues par ces dernières avec les performances obtenues avec l’implémenta-
tion de référence ci-dessus ainsi qu’avec une implémentation optimisée en C.
La figure 1.1 représente le nombre d’opérations flottantes effectuées en une seconde en fonc-
tion de la taille des vecteurs. Les essais sont effectués sur notre machine de tests 2×432 Nehalem 4 .
La courbe « C » montre les performances du code C présenté précédemment. Notons qu’elle est
automatiquement vectorisée par le compilateur INTEL icc 12.0.2. Il est en outre possible de
paralléliser cette implémentation à l’aide d’OpenMP[59]. Pour cela, il suffit d’ajouter la ligne
# pragma omp parallel for reduction (+: norm )
au dessus de la boucle for (ligne 4). La courbe « C optimisé » montre les performances alors
obtenue 5 .
Nous pouvons remarquer que pour les vecteurs de plus de trois millions d’éléments, le nombre
d’opérations effectuées en une seconde est constant pour les deux implémentations qui atteignent
respectivement 3,6 et seulement 6,3 GFlops. Les performances de ce type d’opérations sont
limitées par la vitesse des accès à la mémoire RAM [60] : le processeur ne peut effectuer des
opérations de calcul que si les données sont disponibles en registre. L’émergence des processeurs
parallèles tend à augmenter la proportion de logiciels qui sont limités par la bande passante
de la RAM [61]. En effet, un processeur capable d’effectuer deux opérations simultanément a
3. Cette implémentation a été choisi à titre d’exemple pour sa simplicité. Différentes stratégies permettant de
minimiser la perte de précision des résultats existent, comme celle utilisée par netlib dans l’implémentation de
référence [58].
4. Deux processeurs quadri-cœur INTEL Xeon E5504 (2.0 GHz) et 12 Go de RAM DDR3 1066.
5. Une vectorisation manuelle a été mise au point et permet d’obtenir les mêmes performances.
8
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
Puissance de calcul
6
(GFLOPS)
5
4 C
3 C optimisé
2
1
0
0 5 10 15 20 25 30
Taille des vecteurs (millions d’éléments)
Figure 1.1 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem.
7
Puissance de calcul (GFLOPS)
C
6
4
C optimisé
3
Figure 1.2 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
besoin d’un débit de données deux fois supérieur. Or, ces dernières années, la puissance de calcul
des processeurs a cru plus rapidement que la bande passante. Lors de nos analyses des autres
approches, nous présenterons les performances moyennes observées pour des tailles de vecteurs
comprises entre 3 et 30 millions d’éléments en complétant le graphique de la figure 1.2.
Nous allons utiliser les BLAS pour effectuer l’opération 1.1. Les performances de notre implé-
mentation dépendront alors des performances de l’implémentation BLAS utilisée, celle d’INTEL
dans notre cas. Comme l’interface BLAS ne fournit pas de fonction permettant de calculer la
norme d’une expression vectorielle quelconque, nous devons créer un vecteur temporaire con-
tenant cette expression vectorielle puis en calculer la norme. Les différentes étapes de notre
implémentation seront donc :
1. allouer de la mémoire pour un vecteur temporaire Z ;
2. calculer Z = aW + bX + cY ;
3. calculer la norme de Z ;
4. désallouer Z.
La décomposition de notre problème en opérations prises en charge par les BLAS n’est pas
achevée. En effet, la seconde opération, calculant Z, n’est également pas prise en charge. Cette
opération doit, elle aussi, être décomposée. Les fonctions correspondant aux opérations élé-
9
Chapitre 1. Introduction
mentaires nous permettant d’implémenter cette opération sont listées ci-dessous ainsi que leur
descriptions mathématiques :
cblas_scopy(N, X, INCX, Y, INCY) :
∀i ∈ J0; N − 1K, Y[i × INCY] ← X[i × INCX] ;
cblas_sscal(N, ALPHA, X, INCX) :
∀i ∈ J0; N − 1K, X[i × INCX] ← ALPHA × X[i × INCX] ;
cblas_saxpy(N, ALPHA, X, INCX, Y, INCY) :
∀i ∈ J0; N − 1K, Y[i × INCY] ← ALPHA × X[i × INCX] + Y[i × INCY].
À l’aide de ces fonctions, il est possible de calculer Z. L’implémentation avec les BLAS est donc :
float *W , *X , * Y ;
...
float norm = 0;
float * Z = malloc ( n * sizeof ( float ) ) ; // allocation memoire pour Z
cblas_scopy (n , W , 1 , Z , 1) ; // Z=W
cblas_sscal (n , a , Z , 1) ; // Z= a *Z
cblas_saxpy (n , b , X , 1 , Z , 1) ; // Z= b *X+Z
cblas_saxpy (n , c , Y , 1 , Z , 1) ; // Z= c *Y+Z
norm = cblas_snrm2 (n , Z , 1) ; // calcul de la norme
free ( Z ) ; // desallocation memoire
Selon la version de la bibliothèque choisie, cette implémentation pourra être parallèle et vec-
torisée. L’utilisation des BLAS permet donc l’exploitation des spécificités des processeurs.
Cependant, deux problèmes limitent les performances de cette implémentation. Première-
ment, l’allocation de mémoire et sa désallocation sont des opérations coûteuses, en particulier
pour des vecteurs de grande taille. Deuxièmement, la transformation de la boucle for présentée
(page 8) en quatre appels de fonctions distincts revient à couper cette boucle en quatre, ce qui
conduit à augmenter le nombre d’accès à la mémoire. L’implémentation à l’aide des BLAS est
en effet équivalente de ce point de vue aux boucles suivantes :
float *X , *Y , * Z ;
...
float norm = 0;
float * Z = malloc ( n * sizeof ( float ) ) ; // allocation memoire pour Z
for ( int i =0 ; i < n ; ++ i ) Z [ i ]= W [ i ]; // Z=W
for ( int i =0 ; i < n ; ++ i ) Z [ i ]= a * Z [ i ]; // Z= a *Z
for ( int i =0 ; i < n ; ++ i ) Z [ i ]= b * X [ i ]+ Z [ i ]; // Z= b *X+Z
for ( int i =0 ; i < n ; ++ i ) Z [ i ]= c * Y [ i ]+ Z [ i ]; // Z= c *Y+Z
for ( int i =0 ; i < n ; ++ i ) norm += Z [ i ]* Z [ i ]; // calcul de la norme
norm = sqrtf ( norm ) ;
free ( Z ) ; // desallocation memoire
Le nombre d’accès mémoire est passé de 3.n avec l’implémentation en C à 11.n en utilisant
les BLAS. Les performances de ce type d’opérations étant dominées par le nombre d’accès
à la mémoire, nous pouvons donc nous attendre à ce que les performances soient dégradées
d’un facteur 11/3. Dans ce cas, l’utilisation des BLAS devrait donc être contre-productive.
Nous avons utilisé la bibliothèque MKL développée par INTEL et implémentant l’interface
BLAS. Conformément à nos attentes, cette implémentation permet d’obtenir 1,0 GFlops en
utilisant la version séquentielle de la MKL et 1,6 GFlops en utilisant la version parallèle de cette
bibliothèque. Dans les deux cas, l’exécutable obtenu est environ quatre fois moins performant
que celui optimisé en C comme l’illustre la figure 1.3.
Dans son analyse de l’optimisation des performances sur le CRAY I [43], Jack Dongarra
décrit une étude analogue. Le problème auquel il s’intéressait pouvait mathématiquement se
10
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
4
C optimisé
3 MKL Parallèle
2
Figure 1.3 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
généraliser en regroupant les vecteurs dans une matrice. Il a donc étendu l’interface BLAS pour
prendre en compte les matrices et ainsi résoudre son problème.
Dans notre cas de figure, l’extension de l’interface pour prendre en compte la norme de toutes
les expressions vectorielles est impossible. Afin de comprendre cette impossibilité, supposons
que l’on étende l’interface des BLAS avec les fonctions fictives cblas_s2nm2 et cblas_s3nm2
calculant la norme des expressions vectorielles comportant respectivement deux et trois vecteurs.
L’extension de l’interface BLAS pour prendre en charge la fonction cblas_s3nm2 permet de
résoudre le problème que nous nous étions posé : calculer ||aW + bX + cY || en mutualisant les
travaux d’optimisation et de portage sur les nouvelles architectures. Cependant, si l’application
évolue et nécessite l’introduction d’un quatrième vecteur, nous nous retrouverons face au même
problème : il faudra de nouveau étendre l’interface BLAS. La prise en compte de toutes les
expressions vectorielles est donc impossible car cet ensemble est infini 6 .
Afin de répondre de manière plus pérenne aux problèmes soulevés par notre exemple, il
conviendrait de pouvoir composer les fonctions entre elles. Le rôle du vecteur Z est d’établir
un lien entre les différents appels BLAS. La composition des fonctions entre elles permettrait
de supprimer ce besoin et donc de conserver une seule boucle comme dans l’implémentation
en C. La seule possibilité offerte par les langages procéduraux pour composer des fonctions
entre elles passe par l’utilisation de fonctions d’ordre supérieur acceptant comme argument des
pointeurs sur fonction. Outre la complexité d’utilisation induite par l’utilisation des pointeurs
sur fonction, ces derniers introduisent un surcoût du fait de l’indirection et de l’impossibilité
d’effectuer des optimisations interprocédurales. Sur notre 2×4 32 Nehalem, ce surcoût est d’environ
30 cycles par appel de fonction. Lorsque le corps de cette fonction est suffisamment important, le
surcoût peut être négligé. Ce n’est plus le cas lorsque le corps de la fonction de rappel est petit.
Dans notre exemple, le corps de ces fonctions de rappel correspondrait à l’itération élémentaire
des boucles équivalentes à l’implémentation BLAS et contiendrait donc très peu d’instructions.
Par exemple, le corps de la fonction équivalent à la fonction cblas_saxpy contiendrait deux
instructions flottantes (y=a*x+y), traitées en un cycle. Le surcoût d’une fonction de rappel étant
6. En C, l’utilisation de fonctions variadiques permet une implémentation de la norme des expressions vec-
torielles. Cependant, aucune solution équivalente n’existe dans les autres langages procéduraux comme le FOR-
TRAN.
11
Chapitre 1. Introduction
d’environ 30 cycles, cette approche conduirait à une implémentation 30 fois plus lente que la
version en C.
En résumé, dans le contexte du calcul numérique avec un langage procédural, le seul com-
promis entre maintenabilité et optimisation des performances est obtenu via l’utilisation de
bibliothèques externes. Cependant, dans les cas où il n’existe pas de bibliothèque répondant
exactement aux besoins de l’application, l’adaptation de l’application pour utiliser les biblio-
thèques existantes peut être contre-productive comme dans notre exemple. Il convient donc de
trouver d’autres approches permettant de dépasser cette limite dans l’expressivité des opérations
à réaliser.
L’expressivité limitée des bibliothèques classiques ne permet pas de trouver un compromis in-
téressant entre maintenabilité et performances pour les solveurs d’algèbre linéaire de COCAGNE.
En effet, de nombreuses opérations nécessaires à la mise au point de ces solveurs ne sont pas
fournies par les bibliothèques d’algèbre linéaire. Outre les opérations de normes d’expressions
vectorielles, nous pouvons par exemple citer les permutations d’éléments de vecteur 7 ou la
résolution de systèmes linéaires dont le membre de droite est une expression vectorielle. Ces
opérations doivent donc soit être implémentées et optimisées par les développeurs des applica-
tions scientifiques (COCAGNE dans notre cas), soit être implémentées de manière sous-optimale
en utilisant l’interface BLAS. Dans ce dernier cas, nous avons vu que la principale limitation
réside dans l’impossibilité de composer entre elles les différentes fonctions. Une façon de ré-
soudre notre problème d’expressivité consiste donc à permettre les compositions de fonctions.
Les langages fonctionnels reposent sur ce principe et permettent l’écriture de fonctions d’ordre
supérieur acceptant d’autres fonctions comme argument. Par exemple, le code suivant corre-
spond à l’implémentation de notre boucle avec le langage fonctionnel Haskell [62] et son module
Repa (Regular Parallel Arrays) [63], qui est le module de référence concernant les opérations sur
les tableaux :
import Data . Array . Repa as R
...
norm <- sqrt (
R . foldAll
(+)
0
( R . map
(\ x - > x * x )
( R . map ( a *) w +^ R . map ( b *) x +^ R . map ( c *) y )
)
)
La fonction R.map prend deux arguments. Le premier argument est une fonction unaire
tandis que le second est un tableau. La fonction R.map applique alors la fonction unaire à
tous les éléments du tableau. Dans l’exemple ci-dessus, \x->x*x définit la fonction unaire qui
à chaque nombre, associe son carré. L’opérateur +ˆ effectue une addition élément par élément
de deux tableaux. La fonction R.foldAll effectue une accumulation et prend trois arguments.
Le premier argument est la fonction binaire d’accumulation, le second argument est la valeur
initiale de l’accumulateur et le troisième est le tableau dont les éléments doivent être accumulés.
7. Ces opérations correspondent à la multiplication d’un vecteur avec une matrice de passage orthonormée.
L’interface BLAS fournit des fonctions pour effectuer ce type d’opérations dans certains cas très particuliers.
12
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
4
C optimisé
3 MKL Parallèle
Haskell Parallèle
2
Figure 1.4 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
L’extension pour prendre en charge un troisième vecteur est simple : il suffit d’utiliser l’opéra-
teur +ˆ ainsi que la fonction R.map. Cependant, cela ne permet pas l’obtention de bonnes per-
formances comme l’illustre la figure 1.4. En effet, les versions séquentielles et parallèles de ce
programme atteignent 0,40 GFlops. Notons que les performances parallèles sont identiques aux
performances séquentielles bien que plusieurs threads soient actifs. D’après Ben Lippmeier, co-
auteur de Repa, cela est dû à un manque de maturité des réductions dans le module Repa 8 . En
Avril 2011, l’implémentation Haskell est environ 15 fois plus lente que notre version C optimisée.
Les performances obtenues avec le langage fonctionnel OCaml [64] (0,60 GFlops) sont égale-
ment présentées sur la figure 1.4. Notons cependant que ce langage ne prend pas en charge le
type flottant simple précision et que ces mesures sont donc effectuées sur des tableaux d’élé-
ments double précision. Cette implémentation est environ dix fois plus lente que notre version
C optimisée utilisant des nombres flottants simple précision.
En 2011, l’utilisation des langages fonctionnels actuels ne permet donc pas aisément de
trouver directement un compromis intéressant entre performance et maintenabilité pour les
applications qui nous intéressent.
Afin de permettre la définition de telles opérations, ces langages prennent en charge le concept
de vecteur. Les opérateurs + et * ont dans ces langages une sémantique bien adaptée, directement
importée du langage mathématique. De la même manière, MATLAB et Scilab sont des langages
restreints, efficaces pour implémenter des algorithmes mathématiques, mais mal adaptés à la
mise au point d’autres applications. On parle d’un langage spécialisé à un domaine (Domain
Specific Language (DSL) dans la littérature [67]). Généralement, la mise au point d’un DSL
requiert la conception d’un environnement de développement relativement coûteux. En effet,
cela implique le plus souvent le développement d’un compilateur ou d’un interpréteur, d’une
bibliothèque d’entrées-sorties et d’outils de débogage.
8. http://www.haskell.org/pipermail/haskell-cafe/2011-April/090998.html
13
Chapitre 1. Introduction
Figure 1.5 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
Cependant, la mise au point d’un environnement offrant de bonnes performances est com-
plexe. Dans le cas de MATLAB, le code présenté ci-dessus offre de très mauvaises performances,
comme l’illustre la figure 1.5. La version MATLAB plafonne à 0.28 GFlops, soit près de treize fois
moins que la version C séquentielle (3.6 GFlops). Une implémentation sous MATLAB qui décom-
pose cette opération en opérations élémentaires BLAS conduit aux mêmes performances. Nous
supposons donc que la version de MATLAB disponible sur notre machine de tests (R2009b) ne
fusionne pas les différentes boucles et n’utilise pas toutes les possibilités d’optimisation des pro-
cesseurs modernes. Notons que nous ne disposons pas sur cette machine d’une version parallèle
de MATLAB.
La proximité entre la spécification mathématique et son implémentation avec les DSL dédiés
à l’algèbre linéaire permet de bénéficier d’une maintenabilité très importante. Cependant, les
DSLs disponibles en 2011 ne fournissent pas des implémentations suffisamment performantes
pour convenir aux besoins du calcul intensif.
La Programmation Orientée Objet (POO) est une autre approche permettant d’augmenter
l’expressivité d’une bibliothèque. En effet, la possibilité de définir des objets possédant des
fonctions membres et de passer ces objets en argument d’autres fonctions permet in fine d’écrire
des fonctions qui prennent d’autres fonctions comme arguments. Un objet est une structure de
données valuées et cachées qui répond à un ensemble de messages. Cette structure de données
définit son état tandis que l’ensemble des messages qu’il accepte décrit son interface. Nous nous
intéresserons aux langages orientés objet qui permettent la modification des données en place,
ce qui permettrait d’implémenter efficacement les opérations d’algèbre linéaire.
Des objets de natures différentes (instances de classes différentes) peuvent présenter la
même interface mais se comporter différemment. En effet, deux objets peuvent répondre aux
mêmes sollicitations mais avec des implémentations différentes dépendant éventuellement de
leur état interne. Du point de vue de l’utilisateur d’une Bibliothèque Orientée Objet (BOO),
un même extrait de code conduira donc à exécuter des instructions différentes selon l’état in-
terne des différents objets manipulés. Les détails de la conception d’une Bibliothèque Orientée
Objet (BOO) C++ pour répondre à notre problème sont détaillées en annexe A.1 (page 143).
Le principe de cette conception consiste à identifier les fonctionnalités communes aux dif-
férentes expressions vectorielles. Dans le cadre de la BOO présentée en annexe A.1, il existe trois
types d’expression vectorielles :
14
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
- r_ {const &}
VExpression
- x_ {const &} 1
+ size() : int - l_ {const &}
1 + get(in i : int) : float
1
Selon que ve représente un simple vecteur ou une expression plus complexe, l’expression
ve.get(i) prendra un sens différent. On dit de la fonction
Source 1.1 : Implémentation du calcul de la norme d’une expression vectorielle avec une BOO
Vector w ( n ) ,x ( n ) ,y ( n ) ;
float a ,b , c ;
...
float result = norm ( a * w + b * x + c * y ) ;
Du point de vue de l’utilisateur, l’utilisation de cette BOO permet d’obtenir une expressivité
et une maintenabilité comparables à ce que propose MATLAB pour les vecteurs. En effet,
en surchargeant les opérateurs + et * des expressions vectorielles, nous leur avons ajouté des
informations sémantiques et défini une grammaire. Cette BOO fournit donc un DSL. Ce DSL
héritant de la grammaire du C++, son langage hôte, il est qualifié de langage enfoui spécialisé
à un domaine (Domain Specific Embedded Languages ou DSELs dans la littérature) [68, 69, 70].
Selon les possibilités du langage hôte, du compilateur choisi et des techniques employées, cette
approche peut fournir les mêmes possibilités d’optimisation que les langages dédiés possédants
15
Chapitre 1. Introduction
C optimisé
3 MKL Parallèle
Haskell Parallèle
2
1 BOO optimisée
Figure 1.7 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
leurs propres compilateurs. Par conséquent, la mise au point d’un DSEL ne nécessite pas toujours
la mise au point d’un nouvel environnement de développement.
Comme les langages orientés objet, les langages fonctionnels permettent aussi de définir
des DSEL. Cependant, l’obtention d’implémentations efficaces requiert alors généralement deux
étapes dans la génération du programme. Lors de l’exécution du programme écrit avec le DSEL,
un arbre syntaxique abstrait (Abstract Syntax Tree ou AST) est construit. Cet AST est alors
transformé, pour générer un exécutable optimisé. Le traitement de l’AST lors de l’exécution
permet d’utiliser des informations qui ne sont pas toujours disponibles lors de la compilation
comme la taille des jeux de données ou le problème à résoudre. Certaines optimisations impossi-
bles à réaliser lors de la compilations sont alors permises. Cependant, si l’exécutable devient trop
spécialisé, cela peut devenir contreproductif : optimiser un exécutable peut prendre un temps
non-négligeable. Cette approche n’est efficace que pour les transformations apportant un gain
suffisamment important pour en amortir le coût. Dans le cas où toutes les informations sont
connues à la compilation, il est possible de générer et d’optimiser l’AST lors de la génération du
programme [71, 72].
La figure 1.7 illustre les performances obtenues en utilisant le DSEL fournit par la BOO
présentée dans l’annexe A.1 (0,17 GFlops pour la version séquentielle et 3,6 GFlops pour la
version parallélisée et vectorisée). Si elles sont meilleures que celles fournies par la MKL, elles
restent cependant très inférieures à la version optimisée en C (6.3 GFlops). Ceci est dû au coût
des fonctions virtuelles. En effet, les fonctions virtuelles sont implémentées avec des pointeurs
de fonction. L’utilisation d’une BOO implique les mêmes limitations de performances qu’une
bibliothèque utilisant les pointeurs de fonction. Cette technique est donc à réserver pour les cas où
le corps des fonctions virtuelles est suffisamment gros. De plus, l’utilisation de fonctions virtuelles
limite les optimisations interprocédurales effectuées par le compilateur. Dans cet exemple, cela
se traduit par le fait que le compilateur a été incapable de vectoriser automatiquement cette
implémentation.
Dans l’exemple précédent, toutes les fonctions utilisées peuvent être déterminées à la com-
pilation, il est donc regrettable d’utiliser des pointeurs de fonction, un mécanisme dynamique
impliquant un surcoût important lors de l’exécution. Un outil capable d’analyser le code et d’-
effectuer la composition des opérations lors du processus de compilation permettrait d’obtenir
l’expressivité souhaitée sans impliquer de surcoût sur les performances lors de l’exécution. En
16
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
Une bibliothèque active est une bibliothèque qui fournit à la fois des abstractions
propres à un domaine et les connaissances requises pour les optimiser et vérifier
leurs exigences sémantiques. En outre, les bibliothèques actives sont composables :
un même fichier source peut combiner l’utilisation de plusieurs d’entre elles.
17
Chapitre 1. Introduction
1 BOO optimisée
BA optimisé
0
Figure 1.8 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
pable de vectoriser cette implémentation, ce qui montre bien que les fonctions ont pu être
inlinées pour donner un code équivalent au code C introduit en début de chapitre. Les perfor-
mances obtenues sont identiques à celles obtenues pour les implémentations en C, en séquen-
tiel (3,6 GFlops) comme en parallèle (6.5 GFlops).
Afin de faciliter la maintenance d’une application scientifique, l’utilisation d’outils externes
est une solution intéressante. Cependant, la plupart de ces bibliothèques fournissent des in-
terfaces fixes qui ne permettent pas d’exprimer tous les problèmes de manière pertinente. Nous
avons présenté un certain nombre d’approches permettant de surmonter les limitations dans l’ex-
pressivité des bibliothèques procédurales. Parmi ces approches, l’utilisation des langages dédiés
à la problématique ciblée nous parait la plus prometteuse. En effet, cette approche permet de
concilier à la fois une grande expressivité des opérations (cf. source 1.1) et des implémenta-
tions efficaces (cf. figure 1.8). Afin de ne pas devoir mettre au point un nouvel environnement
de développement, nous préférerons leur implémentation sous forme de bibliothèques actives
permettant d’utiliser les outils de développement associés au langage hôte.
Dans la prochaine section, nous allons nous intéresser aux méthodes de description des struc-
tures de matrices, second point de limitation des bibliothèques procédurales d’algèbre linéaire.
Nous verrons alors que le peu d’expressivité des langages procéduraux limite la définition des
structures de données tout comme elle limite la définition des opérations de calcul. Nous présen-
terons alors différentes approches permettant de dépasser cette nouvelle limitation et d’exprimer
des structures de données complexes.
L’interface BLAS permet de transmettre des matrices denses stockées par lignes ou par
colonnes en passant un pointeur et les dimensions du tableau en mémoire. La description du
stockage d’une matrice dense pour les BLAS nécessite déjà cinq arguments [83]. Par exemple,
pour définir le stockage d’une matrice A, les éléments suivants doivent être définis :
– TRANS définit si A est stockée par ligne ou par colonne,
– M définit le nombre de lignes de A,
– N définit le nombre de colonnes de A,
– LDA définit la distance en mémoire entre deux lignes consécutives de A,
– A définit l’emplacement en mémoire de A.
18
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
Les stockages plus complexes, comme le stockage de Morton [84] qui offre une bonne localité
de données [85] ne sont pas pris en charge. Il serait envisageable d’étendre l’interface BLAS
pour prendre en compte ce format de stockage mais le problème serait simplement reporté : les
autres formats de stockage [86] ne seraient toujours pas supportés. Nous avons vu précédemment
que l’extension des BLAS pour prendre en charge le calcul des normes de toutes les expressions
vectorielles est impossible car il faudrait ajouter une infinité de fonctions. Ce constat s’étend
aux formats de stockage des matrices denses.
Les matrices denses sont des objets mathématiques relativement simples. Pourtant, leur
description est déjà complexe et ne nécessite pas moins de cinq arguments pour les formats les
plus classiques [83]. Avec les matrices creuses qui contiennent un grand nombre d’éléments nuls,
cette description se complique encore (cf. annexe B).
Les formats de stockage CRS (Compressed Row Storage) et CCS (Compressed Column
Storage) sont les plus généraux et conviennent aux cas où l’on ne dispose pas d’information
concernant la structure des matrices creuses. Dans le cas contraire, ces formats ne permettent
pas l’implémentation d’algorithmes efficaces [87] car elles introduisent des indirections lors de
l’accès à chaque élément. De plus, leur empreinte mémoire n’est pas négligeable car il faut
stocker l’emplacement des éléments non nuls. Par exemple pour une matrice de taille m × n
contenant k éléments et stockée selon les formats CRS ou CCS, la taille cumulée des différents
tableaux mis en œuvre est de 2k + n, soit un surcoût de k + n par rapport au nombre d’éléments.
Lorsque les éléments de la matrice sont répartis selon une structure connue à l’avance, il est
possible de définir des formats de stockage moins onéreux. Pour des raisons que nous avons déjà
évoqué, il est impossible de prendre en charge toutes les structures de matrices : il en existe
une infinité. Seules les structures de matrice les plus courantes bénéficient donc de formats de
stockage adaptés.
Dans Templates for the solution of algebraic eigenvalue problems : a practical guide [88],
Jack Dongarra explique comment stocker efficacement quelques structures de matrices récur-
rentes [87] :
matrices creuses générales (cf. figure 1.9(a)) : nous l’avons vu, toutes les matrices creuses
peuvent être stockées aux formats CRS (Compressed Row Storage) ou CCS (Compressed
Column Storage) ;
matrices multidiagonales (cf. figure 1.9(b)) : ces matrices sont nulles à l’exception de quelques
diagonales. Ces matrices pourront être stockées aux formats CDS (Compressed Diagonal
Storage) ou JDS (Jagged Diagonal Storage) ;
matrices à profil (cf. figure 1.9(c)) : ces matrices, aussi appelées matrices à bande variable,
sont des matrices dont les éléments sont répartis dans une bande de largeur variable autour
de la diagonale. Ces matrices peuvent être stockées au format SKS (SKyline Storage) ;
matrices creuses avec blocs denses (cf. figure 1.9(d)) : les éléments non-nuls de ces ma-
trices sont regroupés dans des zones de forme rectangulaires. Ces matrices peuvent être
stockées aux formats BCRS (Block Compressed Row Storage) ou BCCS (Block Compressed
Column Storage).
L’utilisation des formats spécialisés permet de diminuer l’empreinte mémoire et facilite l’util-
isation d’algorithmes plus efficaces. Par exemple, la résolution d’un système à profil est grande-
ment simplifiée et devient naturellement parallélisable si l’on sait que la matrice est en réalité
diagonale par blocs. Si ces blocs sont de même taille, une vectorisation des différentes opérations
peut également être envisagée (cf. chapitre 2).
Les matrices des solveurs SN et SPN ne peuvent être représentées efficacement par les formats
de stockage existants. La figure 1.10 représente un exemple de matrice issue du SPN . La structure
19
Chapitre 1. Introduction
20
1.2. Recensement des solutions pour le domaine de l’algèbre linéaire
de cette matrice ne correspond pas aux structures habituellement prises en charges par des
formats de stockage spécifiques disponibles. Comme l’emplacement de tous les éléments non
nuls de cette matrice est connu lors de l’écriture du code, nous devons utiliser un autre type
d’approche permettant d’exprimer l’ensemble des connaissances dont nous disposons concernant
la structure de cette matrice. Cela permettra de limiter, voire de supprimer les indirections.
Dans la section précédente, nous avons vu qu’il est impossible de décrire précisément les
structure de toutes les matrices creuses avec les langages procéduraux. Avec ces langages, la seule
solution permettant d’exploiter la structure d’une matrice consiste à implémenter les différentes
opérations souhaitées manuellement.
Pour contourner cette limitation, différentes approches ont été explorées. Nous allons en
détailler quelques-unes ici.
Certains algorithmes peuvent être optimisés en modifiant la structure des matrices. Cepen-
dant, cette restructuration des données a un coût important qui doit être amorti par le gain
en performance induit. Les solveurs creux directs comme PaStiX [89] utilisent par exemple des
outils d’analyse de la structure de la matrice comme Scotch [90] ou Metis [91]. Ces outils perme-
ttent de déterminer les échanges de lignes et de colonnes permettant d’aboutir à une structure
plus adaptée pour l’algorithme de résolution utilisé.
Dans les cas où un tel réordonnancement ne permet pas d’améliorer les performances, la
question de l’exploitation de la structure de la matrice se pose. Une approche intéressante con-
siste à laisser l’utilisateur implémenter les opérations élémentaires sur ses matrices (ex : produit,
21
Chapitre 1. Introduction
somme, accès aux éléments) sous la forme de fonctions de rappel 9 (callbacks). En s’appuyant
sur ces fonctions de rappel, il est possible de mettre au point une couche logicielle implémentant
des algorithmes de plus haut niveau. Le solveur PetSc [92, 93, 94] propose ce type de fonction-
nalité. Cela peut également permettre de mettre en place des matrices non stockées dont les
éléments sont calculés « à la volée ». Cette approche permet à l’utilisateur de choisir de manière
générique parmi un catalogue d’algorithmes tout en exploitant une partie des spécificités des
structures de ses matrices. Cependant, cela suppose encore une fois de disposer, pour chaque
opération élémentaire, d’une implémentation optimisée pour chaque type de matrice et pour
chaque architecture matérielle ciblée. Tout ce travail est laissé à la charge de l’utilisateur de la
bibliothèque et ne permet pas toujours d’aboutir à un compromis satisfaisant entre performances
et maintenabilité des codes.
Enfin, une dernière solution consiste, comme pour le problème des expressions vectorielles
étudié précédemment, à définir un langage. En effet, la définition d’un langage permettant de
décrire de manière abstraite la structure des matrices (denses, bande, ...) devrait permettre
de dissocier les algorithmes de haut niveau, les structures de matrices et leur stockage. Cette
structure pourrait donc être utilisée pour déterminer les structures de données et donc les formats
de stockage optimaux.
9. Une fonction de rappel est une fonction qui est passée en argument à une autre fonction. Cette dernière
peut alors faire usage de cette fonction de rappel comme de n’importe quelle autre fonction, alors qu’elle ne la
connaît pas par avance.
22
1.3. Legolas++ : une bibliothèque dédiée aux problèmes d’algèbre linéaire
La décomposition d’une matrice en blocs est une opération classique en algèbre linéaire. En
effet, de nombreuses méthodes de résolution s’expriment sous la forme de matrices blocs. Par
exemple, la matrice bloc suivante : !
A B
BT 0
est utilisée pour exprimer les problèmes de point-selle [95]. Nous avons par ailleurs présenté dans
la section précédente le format de stockage (BCSR) pour les matrices dont la structure creuse
contient des blocs denses.
Legolas++ propose de réunifier et de généraliser ces approches. En effet, Legolas++ permet
de préciser la structure des blocs sous-jacents. La structure d’une matrice est décrite comme la
combinaison de différentes sous-structures de matrices. Par exemple, la figure 1.11(a) représente
une matrice à deux niveaux : un premier niveau de structure diagonale avec des blocs tridi-
agonaux. Lorsqu’un bloc peut de nouveau être décomposé en blocs, on parle de matrice bloc
multiniveaux. La figure 1.11(b) présente une matrice à trois niveaux : un premier niveau de
structure diagonale, un second niveau de structure tridiagonale, un troisième niveau triangu-
laire inférieur. Dans le reste de ce chapitre, nous nommerons ces matrices M2 et M3 en référence
à leur nombre de niveaux.
Legolas++ est une bibliothèque active. L’exemple de la section 1.2.2 utilisait un AST des
opérations vectorielles afin d’optimiser le calcul de norme correspondant à l’équation 1.1. Le
domaine d’application de Legolas++ est bien plus vaste et nécessite la construction de deux
AST différents qui interagissent entre eux. Le premier AST est dédié à la description de la
structure des matrices tandis que le second est dédié aux opérations de calcul.
Legolas++ permet de définir de manière récursive des « matrices structurées de matrices
structurées » 10 . L’AST d’une structure de matrice est un arbre dont les nœuds sont des blocs
et les feuilles des données scalaires. Le type de M3 est :
Diagonal < Tridiagonal < LowerTriangular < float > > >.
Dans le cas général, la taille des différents niveaux n’est pas connue à la compilation. L’AST
ne peut donc être complètement défini à la compilation. Il convient donc de dissocier la partie
10. L’expression « matrice de matrice » est employée ici de manière abusive car les éléments d’une matrice
doivent être dans un corps, ce qui est trop limitant pour décrire les matrices issues du solveur SPN .
23
Chapitre 1. Introduction
Figure 1.13 : Structure d’une matrice à cinq niveaux décrite avec Legolas++ dans le solveur SPN [33].
La densité du gris indique le niveau dans la structure ; les éléments scalaires non-nuls sont représentés
en rouge.
24
1.3. Legolas++ : une bibliothèque dédiée aux problèmes d’algèbre linéaire
las++ fournit l’opérateur crochets []. L’utilisation des opérateurs parenthèses () et crochets []
permet de différencier la nature mathématique de ces deux types de structures. En effet, l’accès
à un élément de matrice nécessite deux coordonnées tandis que l’accès à un élément de vecteur
ne nécessite qu’une seule coordonnée. L’expression X[i][j][k] permet ainsi d’accéder à un
élément d’un vecteur à trois niveaux.
Concrètement, Legolas++ généralise le mécanisme d’Expression Templates introduit précédem-
ment pour prendre en charge les matrices et les vecteurs à plusieurs niveaux. Legolas++ permet
ainsi de spécialiser l’implémentation des différentes opérations pour chaque structure. Prenons
par exemple l’expression matricielle Y = A × X + Y où A est une matrice et où X et Y sont
des vecteurs. Ci-dessous se trouvent les implémentations pour trois types de matrices :
A est une matrice diagonale
Il est aisé d’allonger cette liste et de fournir un opérateur de multiplication pour chaque
structure élémentaire. Si A est une matrice par bloc, les différentes implémentations spécifiques
doivent être composées afin de fournir une implémentation spécialisée pour la nouvelle structure.
Prenons l’exemple de M2 représentée sur la figure 1.11(a). Cette matrice est diagonale par blocs
et ses blocs sont tridiagonaux. Analysons les étapes de compilation qui transforment le produit
matrice-vecteur Legolas++ suivant :
Y2 += M2 * X2 ;
en pseudo-code C équivalent.
Les opérateurs += et * permettent de construire un AST correspondant aux opérations de
calcul. Cet arbre donne ensuite lieu à une implémentation selon le procédé suivant : pour chaque
niveau de la matrice, l’implémentation spécialisée est mise en œuvre et conduit à la construction
de nouveaux AST correspondant aux opérations des niveaux inférieurs. Dans notre exemple, la
structure de plus haut niveau de M2 est la structure diagonale. Le compilateur remplacera donc
Y2+=M2*X2 par l’implémentation ci-dessous :
25
Chapitre 1. Introduction
Les blocs M2(i,i) ont une structure tridiagonale. La compilateur remplacera donc l’ex-
pression de la ligne 3 par le code spécialisé pour cette structure et générera l’implémentation
suivante :
int nrows = M2 . nrows () ;
for ( int i =0 ; i < nrows ; ++ i ) {
int nrows2 = M2 (i , i ) . nrows () ;
for ( int j =0 ; j <2 ; ++ j )
Y2 [ i ][0]+= M2 (i , i ) (0 , j ) * X2 [ i ][ j ];
for ( int j =1 ; j < nrows2 -1 ; ++ j ) {
Y2 [ i ][ j ]+= M2 (i , i ) (j ,j -1) * X2 [ i ][ j -1];
Y2 [ i ][ j ]+= M2 (i , i ) (j , j ) * X2 [ i ][ j ];
Y2 [ i ][ j ]+= M2 (i , i ) (j , j +1) * X2 [ i ][ j +1];
}
for ( int j = nrows2 -1 ; j < nrows2 ; ++ j )
Y2 [ i ][ nrows2 -1]+= M2 (i , i ) ( nrows2 -1 , j ) * X2 [ i ][ j ];
}
26
1.4. Objectif de la thèse : conception d’une version multicible de Legolas++
do {
répéter
| f o r ( int i =0; i <n ; i ++){
pour i = 0 a n − 1 faire
S = Bi | | s=B [ i ] ;
pour j = 0 a i − 1 faire | | f o r ( int j =0; j <i ; j ++)
S = S − Aij Xj | | | s−=A( i , j ) ∗X[ j ] ;
pour j = i + 1 a n − 1 faire | | f o r ( int j=i +1; j <n ; j ++)
S = S − Aij Xj | | | s−=A( i , j ) ∗X[ j ] ;
Résoudre : Aii Xi = S | | X[ i ]= s /A( i , i ) ; }
} while ( ! i t e r . end (X) ) ;
jusqu’à convergence atteinte;
}
}
27
Chapitre 1. Introduction
différents cœurs présents dans les processeurs, elle ne permet ni d’utiliser les unités vectorielles
intégrées au processeur (cf. tableau 1.1), ni les accélérateurs de calcul GPU de plus en plus
courants dans nos stations de travail.
Cette limitation de Legolas++ ne permet donc pas d’utiliser toutes les ressources matérielles
disponibles pour nos simulations. En effet, une étude préliminaire menée en 2008 de parallélisa-
tion du solveur SPN sur GPU, présentée dans [34], a permis d’atteindre de très bonnes perfor-
mances (facteur d’accélération de 30 comparé à une exécution séquentielle sur le CPU). Cette
parallélisation utilisait la structure des matrices mais ne s’appuyait pas pour autant sur Lego-
las++ : les noyaux de calculs ont été écrits et optimisés manuellement. Cependant, les coûts
de maintenance liés à l’intégration de ces travaux dans le code industriel sont prohibitifs. Nous
souhaitons étudier la faisabilité d’une extension de Legolas++ pour utiliser automatiquement
les différentes architectures spécialisées pour le calcul scientifique.
L’objectif de cette thèse est d’identifier les verrous à lever pour mettre au point une biblio-
thèque permettant d’exploiter efficacement différentes familles de processeurs de manière trans-
parente à l’utilisateur. Nous proposerons une solution pour lever ces verrous et validerons l’ap-
proche ainsi définie avec un démonstrateur multicible de Legolas++.
28
La fonction de la mémoire est aussi importante
que celle du calcul
Jacques Le Goff (1924 – présent)
Historien médiéviste français
Chapitre 2
Dans l’introduction, nous avons vu que le suivi des évolutions des architectures de calcul est
un enjeu important pour les codes industriels de calcul scientifique. Afin de faciliter ce suivi et
de minimiser le coût d’adaptation aux nouvelles architectures, nous proposons de concentrer au
sein de Legolas++ l’adaptation et l’optimisation des codes pour les différentes architectures.
Afin de définir les stratégies d’optimisation adaptées à chaque architecture matérielle, nous
allons présenter les évolutions architecturales subies par les processeurs au cours des quinze
dernières années. Nous nous intéresserons tout particulièrement aux différentes architectures
que nous souhaitons cibler avec Legolas++.
Après avoir proposé des implémentations optimisées pour une opération simple sur ces dif-
férentes cibles matérielles, nous étudierons comment leur architecture impacte l’optimisation
des codes. Cette étude nous permettra de mettre en avant la nécessité d’utiliser une couche
d’abstraction chargée d’unifier l’expression du parallélisme présent dans l’application.
Nous définirons ensuite un cas test que nous utiliserons dans la suite du document pour
mesurer l’efficacité des différentes approches. Ce cas test, issu du solveur neutronique SPN , est
un exemple simple d’utilisation de Legolas++. Nous nous appuierons sur ce cas test pour mettre
en avant l’importance du format de stockage des matrices et la nécessité de l’adapter à chaque
architecture ciblée.
Nous conclurons ce chapitre en spécifiant le besoin d’une couche logicielle intermédiaire
entre les différentes cibles et Legolas++. Cette couche doit permettre une implémentation de
Legolas++ n’imposant pas de structures de données et exprimant le parallélisme d’une manière
unifiée.
29
Chapitre 2. Programmation multicible : une structure de données par cible ?
Sommaire
2.1 Présentation des différentes architectures cibles . . . . . . . . . . . . 31
2.1.1 Les processeurs : des machines parallèles . . . . . . . . . . . . . . . . . . 31
2.1.2 Du processeur au processeur assisté : les accélérateurs de calcul . . . . . 33
2.1.3 Introduction à l’architecture des processeurs X86_64 et à leur program-
mation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.1.3.1 Les unités SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.1.3.2 Les processeurs multicœurs . . . . . . . . . . . . . . . . . . . . 37
2.1.3.3 Optimisation de l’implémentation du calcul d’une expression
vectorielle ||aW + bX + cY || . . . . . . . . . . . . . . . . . . . 38
2.1.4 Introduction à l’optimisation pour GPUs NVIDIA . . . . . . . . . . . . 40
2.1.4.1 Historique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.1.4.2 Introduction à l’architecture des GPUs NVIDIA . . . . . . . . 41
2.1.4.3 Implémentation optimisée du calcul d’une expression vecto-
rielle ||aW + bX + cY || avec CUDA . . . . . . . . . . . . . . . 45
2.1.5 Comparaison des stratégies d’optimisation CPU et GPU . . . . . . . . . 49
2.1.5.1 Comparaison des implémentations de l’opération vectorielle . . 50
2.1.5.2 Comparaison des implémentations de l’opération de réduction 50
2.2 Optimisation pour CPU et pour GPU d’un exemple plus complexe 51
2.2.1 Présentation du problème . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.2.2 Implémentations optimisées . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.2.2.1 Principe général . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.2.2.2 Différents formats de stockage : entrelacement des deux niveaux
de la structure . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.2.2.3 Implémentation pour processeurs X86_64 . . . . . . . . . . . . 58
2.2.2.4 Implémentation pour GPUs . . . . . . . . . . . . . . . . . . . . 61
2.3 La plate-forme de tests : les configurations matérielles . . . . . . . . 62
2.4 Analyse des performances : à chaque architecture sa structure de
données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.5 Programmation multicible : un code source unique, différents exé-
cutables optimisés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
30
2.1. Présentation des différentes architectures cibles
Core 2 Core i7
100
Pentium 4
Fréquence (GHz)
Pentium pro
Pentium
1
i486
0.1 Simple precision (GFLOPS)
Double precision (GFLOPS)
Fréquence (GHz)
0.01 Parallélisme SIMD
Parallélisme cœurs
Débit d’instruction > 1
Débit d’instruction < 1
0.001
1990 1995 2000 2005 2010
Figure 2.1 : Approches permettant l’augmentation de la puissance de calcul des processeurs INTEL
31
Chapitre 2. Programmation multicible : une structure de données par cible ?
32
2.1. Présentation des différentes architectures cibles
Au final, nous pouvons définir la puissance de calcul Pc d’un processeur par la formule
suivante :
Pc = f Di Nv Nc .
Comme Nv dépend du type des données manipulées (flottants simple ou double précision),
Pc peut également en dépendre. C’est le cas sur les processeurs X86_64. Les différentes zones
de couleur de la figure 2.1 représentent les contributions de ces différentes composantes dans
l’obtention de la puissance de calcul pour les nombre flottants simple et double précision. Rap-
pelons que le débit d’instructions flottantes est de 0,2 pour le proceseur i486 12 , ce qui justifie
que les courbes de performances soient sous la courbe de fréquence pour ce processeur. Notons
par ailleurs que les instructions SSE s’exécutent en deux cycles sur les processeurs Pentium
III et Pentium 4 12 . L’utilisation simultanée des deux unités SSE disponibles ne permet donc
d’obtenir qu’un débit d’instructions flottantes égal à 1. À partir de la génération core 2, ces
instructions s’exécutent en 1 cycle 12 . Les deux unités de calcul étant toujours disponibles, le
débit d’instruction passe donc à 2.
Parmi ces quatre approches permettant l’amélioration des performances des processeurs,
deux n’impactent pas la génération des exécutables (ni le code, ni la chaîne de compilation) :
l’augmentation de la fréquence f ou du débit d’instructions Di . Ces améliorations sont to-
talement transparentes pour l’application et permettent donc une accélération « gratuite » des
applications lors d’un changement de processeur. Les deux autres approches imposent une trans-
formation du code de l’application afin d’exploiter le parallélisme disponible. Dans le cas des
unités vectorielles, on parle de parallélisme SIMD ou de parallélisme « à grain fin ». Pour le
développeur d’une application, cela se traduit par l’utilisation de fonctions « intrinsèques » cor-
respondant à une instruction assembleur. Nous verrons dans la section 2.1.3.3 un exemple de
parallélisation de code mettant en œuvre les unités SSE. Enfin, l’augmentation du nombre de
cœurs se traduit pour le développeur par la nécessité d’exposer des fils d’exécutions parallèles
s’exécutant sur chacun des cœurs. Ces fils d’exécutions peuvent prendre la forme de processus
ou de threads (processus légers).
L’introduction du parallélisme explicite nécessitant une intervention du développeur, les
fabricants de processeurs ont longtemps favorisé l’augmentation de la puissance de calcul au
travers de l’augmentation de la fréquence. Cependant, l’augmentation de la fréquence implique
une très forte augmentation de la consommation énergétique et donc de la chaleur à dissiper.
Ces problématiques énergétiques ont finalement conduit les fabricants de processeurs à favoriser
les autres approches après avoir atteint la limite des 4,0 GHz. Depuis 2006, les processeurs les
plus puissants ont une fréquence comprise entre 2,5 et 3,5 GHz.
Entre 1989 et 2002, les performances des applications ont ainsi été améliorées par la simple
augmentation du débit d’instruction et de la fréquence, alors que depuis 2002, la fréquence des
processeurs a tendance à baisser. Les performances des applications ne s’améliorent donc plus
avec la sortie des nouveaux processeurs. Pour être exploitées efficacement, les dernières évolutions
des processeurs nécessitent une modification logicielle mettant en œuvre leurs différents niveaux
de parallélisme comme nous le verrons dans la section 2.1.3.3.
33
Chapitre 2. Programmation multicible : une structure de données par cible ?
applications continuent à justifier l’existence de puces dédiées. Ainsi, estil très commun d’u-
tiliser des DSPs (Digital Signal Processor) pour traiter des signaux numériques. Dans les années
1970 les cartes graphiques sont apparues pour faire de l’affichage en temps réel [106]. À cette
époque où les processeurs étaient relativement simples, de nombreux accélérateurs ont fait leur
apparition. Une grande partie de ces accélérateurs a été absorbée par d’autres processeurs, à
l’instar du FPU, intégré dans les processeurs X86 depuis la sortie du 486 par INTEL en 1989.
Les processeurs de rendu graphique ont suivi une évolution parallèle et sont également formés
de différents accélérateurs : en 1995, ATI commercialise la carte Ati Rage 3D, la première carte
graphique intégrant un accélérateur de rendu 3D 13 .
Plus récemment, des accélérateurs dédiés aux calculs physiques (Physics Processing Units ou
PPU) ont commencé à apparaître. Disponible depuis 2000, le système GRAPE-6 [107] permet
d’accélérer la résolution du problème des N-corps en astrophysique. SPARTA [108] puis HEL-
LAS [109] sont des architectures de processeurs dédiés à accélérer les calculs de déformations de
matériaux. En 2006, Ageia propose les premières cartes PhysiX, comprenant des accélérateurs
physiques à destination du grand public afin d’accélérer les traitements physiques dans les jeux
vidéos. Depuis l’achat d’Ageia par NVIDIA et l’intégration de PhysX à leurs produits, les GPU
NVIDIA sont également des PPUs.
L’évolution des techniques de conception et de production des processeurs ainsi que l’évolu-
tion des besoins ont rendu ces différents accélérateurs de plus en plus programmables. En effet,
les coûts de conception d’un accélérateur performant sont aujourd’hui tellement élevés qu’il n’est
plus économiquement possible de concevoir et de fabriquer une puce pour chaque application.
Rendre les accélérateurs plus génériques et plus programmables permet de réutiliser le même
accélérateur pour différentes familles de problèmes.
Aujourd’hui, plusieurs types d’accélérateurs de calcul sont couramment disponibles pour
effectuer des calculs scientifiques.
Les FPGAs (Field-Programmable Gate Array) [110] sont des puces comportant un cir-
cuit logique programmable. Les FPGA permettent de programmer une architecture et
d’obtenir, ainsi, une puce spécialisée pour le problème que l’on veut traiter. Certains PPU,
comme SPARTA, sont construits autour d’un FPGA. Les FPGA sont couramment utilisés
pour le décodage de séquences ADN [111]. Mais pour les calculs nécessitant l’utilisation
de nombres flottants, les FPGA semblent aujourd’hui peu adaptés car l’implémentation
d’unités de calcul flottant sur FPGA ne laisse pas beaucoup de place pour ajouter d’autres
unités de calcul. Par la suite, nous ne nous intéresserons donc plus à cette catégorie d’ac-
célérateurs.
Le processeur IBM Cell [112, 113], conçu comme une puce multicœur hétérogène, est com-
posé d’un cœur de type PowerPC 970 chargé de contrôler 8 cœurs SIMD, les SPEs (Syn-
ergistic Processing Elements). Ces 8 cœurs sont des cœurs contenant une unité vectorielle
Altivec (équivalent IBM du SSE) et embarquant très peu de mémoire locale. Dix fois
plus puissant que les processeurs classiques disponibles à la même époque [114], le Cell
est réputé très difficile à programmer. En effet, l’absence de mémoire cache, les fortes
contraintes d’alignement mémoire et la sensibilité des SPEs à l’ordre des instructions im-
pose généralement une programmation proche de l’assembleur et une gestion manuelle des
transferts de données entre SPEs. De ce fait, la prise en main du Cell est difficile. No-
13. http://en.wikipedia.org/wiki/ATI_Rage
34
2.1. Présentation des différentes architectures cibles
tons qu’en 2009, IBM a annoncé l’arrêt du développement du processeur Cell 14 , sans pour
autant renoncer à sortir de nouveaux processeurs dans cette gamme 15
Les GPUs (Graphic Processing Unit) [115, 116, 117] reprennent les grandes lignes des pro-
cesseurs vectoriels. Conçus à l’origine pour appliquer le même traitement informatique à
chaque pixel d’un écran, ils sont de moins en moins spécialisés afin de pouvoir répondre
aux besoins de plus en plus variés de l’industrie vidéoludique. Aujourd’hui, ce sont des
processeurs comportant un grand nombre d’unités de calcul (jusqu’à 512 dans les GPUs
NVIDIA).
Dans la suite de ce document, nous ne nous intéresserons qu’aux processeurs couramment
disponibles dans les station de travail, c’est-à-dire aux processeurs X86_64 multicœurs avec
unités SSE ou AVX commercialisés par INTEL, AMD et VIA 16 et aux GPUs commercialisés
par NVIDIA. En effet, ces processeurs ont l’avantage d’être déjà bien répandus dans les envi-
ronnements de calcul intensif.
L’étude de ces architectures permettra de déterminer les stratégies à employer afin de réduire
les temps d’exécution des codes de calcul scientifiques.
35
Chapitre 2. Programmation multicible : une structure de données par cible ?
ni = i(nd + nu ).
Dans un processeur SIMD à i voies, les i calculs sont identiques, il est possible de mutualiser le
décodeur d’instructions entre les i unités de calcul :
ni = nd + inu .
36
2.1. Présentation des différentes architectures cibles
Processeur Processeur
Mémoire cache L3 Mémoire cache L3
Cœur Cœur Cœur Cœur
Mémoire cache L2 Mémoire cache L2 Mémoire cache L2 Mémoire cache L2
Mémoire cache L1 Mémoire cache L1 Mémoire cache L1 Mémoire cache L1
Unité SSE Unité SSE Unité SSE Unité SSE Unité SSE Unité SSE Unité SSE Unité SSE
Figure 2.2 : Architecture d’une station de calcul comportant deux processeurs quadri-cœurs.
la version 12.0.2 du compilateur INTEL, la version 4.5.3 de g++ et la version 2.9 de clang
montrent que les compilateurs actuels ne sont capables de vectoriser que dans le cas où les
instructions à vectoriser sont dans un nid de boucle contenant moins de trois niveaux. Dans le
cas général, ces instructions doivent explicitement être appelées par le développeur à l’aide de
fonctions intrinsèques du compilateur 17 [102, 103]. La prochaine section (page suivante) présente
un exemple de code utilisant ces fonctions intrinsèques. Nous verrons également section 2.4 que
l’utilisation de ces instructions suppose d’avoir des structures de données adaptées.
les processus systèmes, ce chiffre est encore plus important. Amortir ces coûts nécessite donc
que chaque thread exécute une tâche suffisamment longue à traiter.
17. Une fonction intrinsèque est une fonction dont l’implémentation est assurée par le compilateur même.
Typiquement, une séquence d’instructions générées automatiquement remplace l’appel de fonction original un
peu à la manière d’une fonction inline. Cependant, à la différence d’une fonction inline, le compilateur a une
connaissance approfondie de la fonction intrinsèque, et par conséquent peut mieux intégrer celle-ci et l’optimiser
pour la situation donnée. Les fonctions intrinsèques sont aussi appelées built-in functions lorsqu’elle font partie
de la définition d’un langage. http://fr.wikipedia.org/wiki/Fonction_intrinsèque
37
Chapitre 2. Programmation multicible : une structure de données par cible ?
La multiplication du nombre de cœurs au sein des processeurs se traduit par la mise en place
d’une hiérarchie du parallélisme. La figure 2.2 représente de manière simplifiée l’architecture de
32 Nehalem. Celle-ci dispose de deux processeurs (en violet sur la figure),
notre machine de tests 2×4
comportant chacun quatre cœurs (en vert sur la figure). Chacun de ces huit cœurs dispose de
deux unités SSE (en bleu sur la figure). Dans l’écriture des codes, cela se traduit par la nécessité
de mettre en place différents niveaux de parallélisme.
Cette opération est trivialement parallélisable : la boucle for correspond à une opération
de réduction classique. Nous allons utiliser OpenMP [59] pour effectuer le premier niveau de
parallélisation et les fonctions intrinsèques SSE [102] pour la vectorisation.
Paralléliser cette boucle avec OpenMP est trivial : il suffit d’ajouter une directive de par-
allélisation en précisant que la boucle for correspond à une réduction. Le code résultant est
donc :
1 float *W , *X , * Y ;
2 ...
3 float norm = 0;
4 # pragma omp parallel for reduction (+: result )
5 for ( int i =0; i < n ; ++ i ) {
6 float tmp = a * W [ i ]+ b * X [ i ]+ c * Y [ i ];
7 result += tmp * tmp ;
8 }
9 norm = sqrtf ( norm ) ;
Le mot clé parallel de la ligne 4 préconise le lancement automatique d’un thread par
cœur vu par l’environnement d’exécution OpenMP 18 . Ces threads seront terminés à la sortie du
bloc for, ligne 8. Le mot clé for, ligne 4, précise que chacun de ces threads devra pendre en
charge une partie des itérations de la boucle for de la ligne suivante (ligne 5). Enfin, le mot clé
reduction, ligne 4 précise que la boucle for définit une opération de réduction dont l’opérateur
est l’addition (+) et la variable d’accumulation result.
L’utilisation des fonctions intrinsèques est plus complexe :
– les pointeurs W, X et Y doivent respecter des contraintes d’alignement ;
18. Ce nombre peut ne pas correspondre au nombre de cœurs. Par exemple, un cœur hyperthreadé [122, 104]
expose deux cœurs au lieu de un. Il est également possible d’influencer le nombre de threads vu par l’environnement
d’exécution via des variables d’environnement [59].
38
2.1. Présentation des différentes architectures cibles
Ce code est bien moins explicite que le code non vectorisé ci-contre. Un code complet devrait
en outre contenir différentes branches pour les cas suivants :
– W, X ou Y ne sont pas correctement alignés,
– n n’est pas un multiple de 4,
– le processeur ciblé ne supporte pas l’instruction DPPS 19 apparue avec le jeu d’instruction
SSE 4.1 ; par exemple, les processeurs INTEL Atom ne supportent pas cette instruction.
Il semble donc naturel de déléguer ce travail d’optimisation et de spécialisation à des outils dédiés.
Dans les cas simples comme celui-ci, le compilateur génère des instructions SSE vectorisées à
partir du code non vectorisé. Cependant, comme nous le verrons dans le chapitre 6, aucun
compilateur disponible ne parvient à vectoriser les boucles plus complexes. Notre expérience
sur différents cas montre en particulier que lorsqu’il y a plus de deux niveaux de boucle, aucun
compilateur ne parvient à générer des opérations vectorielles automatiquement.
39
Chapitre 2. Programmation multicible : une structure de données par cible ?
2.1.4.1 Historique
Dans cette section, nous allons présenter l’évolution de l’architecture des accélérateurs graphiques
afin de comprendre l’émergence des architectures actuelles. Le lecteur intéressé pourra également
se référer à [123].
En 1988 le studio d’animation Pixar publie RenderMan Interface Specification [124, 125],
une interface de bibliothèque permettant d’unifier l’interface de programmation pour les dif-
férentes bibliothèques de rendu photoréaliste de scènes 3D. RenderMan permet de dissocier les
activités de modélisation et de rendu graphique. Afin de combler le manque d’expressivité des
bibliothèques procédurales, RenderMan inclut un DSL dédié au rendu photoréaliste : Render-
Man Shading Language [126]. L’utilisation de ce DSL permet aux utilisateurs de définir leurs
propres effets visuels. RenderMan est une interface de rendu graphique photoréaliste et ne cible
pas les applications de rendu en temps-réel : le rendu des 77 minutes du film d’animation Toy
Story a pris près de 4 mois sur 300 processeurs [127].
En 1992, Silicon Graphic Inc (SGI) propose le standard OpenGL [128] afin de faciliter le
développement et la portabilité des applications scientifiques et industrielles nécessitant un rendu
de scènes 3D en temps réel. Afin de permettre un rendu des images en temps réel, OpenGL ne
peut pas prendre en charge toutes les fonctionnalités de RenderMan. En particulier, OpenGL
ne propose pas de DSL. Les différents types de traitement sont prédéfinis par le standard. Ceci
permet en contrepartie des implémentations très efficaces capables d’afficher des scènes 2D en
temps réel sur les stations de travail disponibles à cette époque.
À partir de 1995, le rendu de scène 3D en temps réel trouve un débouché auprès du grand pub-
lic grâce aux jeux vidéos. L’ouverture de ce marché, très important en volume, permet de financer
le développement d’accélérateurs matériels dédiés au support d’OpenGL. Afin d’améliorer la
qualité graphique et les performances des jeux vidéo, des accélérateurs 3D supportant matérielle-
ment une partie des traitements OpenGL sont alors intégrés aux consoles de salon et aux ordi-
nateurs grand public [129, 130].
En 1999, NVIDIA commercialise la première carte graphique comportant une « unité de
calcul graphique » (Graphic Processing Unit ou GPU) : la GeForce 256 20 est la première carte
graphique capable d’accélérer matériellement l’ensemble des fonctionnalités d’OpenGL 1.2. Les
GPUs sont alors des processeurs vectoriels avec des décodeurs d’instructions très pauvres : les
traitements à appliquer sur chaque élément de donnée sont prédéterminés. OpenGL laisse en
20. http://en.wikipedia.org/wiki/GeForce_256
40
2.1. Présentation des différentes architectures cibles
effet peu de liberté aux développeurs d’applications. En particulier, il ne permet pas la définition
de nouveaux effets de rendu.
En 2000, une équipe de SGI montre comment réaliser une implémentation de RenderMan
basée sur OpenGL [131]. Dans ces travaux, OpenGL est considéré comme une machine virtuelle
SIMD : les fonctions OpenGL faisaient office d’assembleur pour la machine virtuelle. Afin de
faciliter ce travail et d’étendre les possibilités de cette machine virtuelle, un assembleur minimal
a été proposé comme extension à OpenGL. Bien que les performances de l’implémentation
proposée aient été limitées 21 , ces travaux ont permis de montrer que les GPUs pouvaient être
utilisés comme des accélérateur SIMD pour mettre en œuvre des effets de rendu qui ne sont pas
proposés nativement par OpenGL.
En 2002, L’OpenGL Architecture Review Board 22 (ARB) publie ARB assembly, un assem-
bleur standard pour programmer les GPU. Si cet assembleur est très limité (il ne propose par
exemple pas d’instructions permettant de contrôler le flot d’exécution), il permet cependant aux
développeurs de concevoir et d’ajouter de nouveaux effets de rendu. Afin d’accélérer l’applica-
tion de ces effets, les fabricants de GPUs vont peu à peu augmenter la programmabilité de leurs
processeurs.
En 2004, la version 2.0 de OpenGL introduit OpenGL Shading Language (GLSL), un DSL
dédié au rendu de scènes 3D. Les GPU deviennent alors des accélérateurs graphiques facilement
programmables. Les développeurs de codes de calcul intensif commencent alors à exploiter la
puissance de calcul disponible sur les GPUs pour effectuer des calculs généralistes (General-
Purpose computing on Graphics Processing Units ou GPGPU) [132]. En effet, la puissance de
calcul affichée des GPUs était bien supérieure à celle des CPUs. Comme l’illustre la figure 2.3(a),
en valeur absolue, cet écart n’a cessé de croître depuis.
En 2011, le GPU AMD Radeon HD 6970, le GPU le plus puissant disponible sur le marché,
dispose d’une puissance de calcul de 2703 GFlops, contre 316 GFlops pour le CPU le plus
puissant : le processeur INTEL Core i7-3960X. Dans la version 4.0 du CUDA Programming
guide [133], NVIDIA l’explique par le fait qu’une part plus grande des transistors est consacrée
aux unités de calcul comme l’illustre la figure 2.3(b). Héritiers d’accélérateurs très spécialisés, les
GPUs modernes ont vu leur programmabilité s’améliorer avec le temps au fur et à mesure que
leur puissance de calcul et les besoins de l’industrie vidéo-ludique ont augmenté. Les premiers
GPUs étaient des processeurs dont tous les transistors étaient consacrés à l’exécution de fonctions
OpenGL pré-câblées. Les GPUs modernes héritent de cette historique de larges unités vectorielles
et un très petit cache. À ces éléments se sont ajoutées des unités d’instruction plus évoluées
permettant de contrôler plus finement les unités de calcul.
Dans cette section, nous allons présenter les principes de l’architecture des GPUs GF100 com-
mercialisée par NVIDIA en 2011 et utilisée dans les produits dédiés au calcul scientifique (ligne
de produit Tesla 20 23 ). Issus de l’architecture Fermi [117], les GPUS GF100 peuvent dans une
première approche être assimilés à des processeurs multicœurs disposant d’unités SIMD à 32
voies. Cette section a pour objectif de donner au lecteur les éléments permettant de mieux
21. D’après les auteurs, les faibles performances de cette implémentation sont largement dûes au manque de
maturité du compilateur mis au point pour la prise en charge du RenderMan Shading Language.
22. L’ARB est le consortium qui encadre alors les spécifications d’OpenGL. Depuis 2006, c’est le Kronos Group
(http://www.khronos.org/) qui est chargé de faire évoluer OpenGL.
23. Les principes exposés ici s’étendent à tous les GPU NVIDIA GeForce 4XX et 5XX. Cependant, les données
chiffrées peuvent varier d’un modèle à l’autre.
41
Chapitre 2. Programmation multicible : une structure de données par cible ?
comprendre les stratégies d’optimisation et les analyses de performances effectuées dans la suite
du document. Le lecteur intéressé par les détails de l’architecture des GPUs Tesla 20 pourra
se référer au CUDA Programming guide [133], à la documentation technique de Fermi [117]
ou à la thèse de Sylvain Collange [134] qui effectue également une comparaison de différentes
architectures GPU.
Les GPUs GF100 sont commercialisés sous forme de cartes périphériques embarquant de la
mémoire RAM et un GPU GF100 comme l’illustre la figure 2.4. La carte est reliée au système
hôte par un lien PCI Express permettant de transférer des données et de lancer des calculs sur
la carte. Bien que les GPUs de la série Tesla 20 ne disposent que de 448 cœurs, ils exposent
21 504 threads. En effet, sur un GF100, plusieurs threads partagent les mêmes unités de calcul. Ce
principe est appelé Simultaneous Multi Threading (SMT) [135] dans la littérature. Dans les pro-
cesseurs INTEL, l’implémentation du SMT est appelée Hyper-Threading. La gestion matérielle
des threads est effectuée de manière hiérarchique. Les trois niveaux de hiérarchie existant sont
le warp 24 , le bloc et la grille.
Un warp est un regroupement de 32 threads.
Un bloc est un regroupement de warps. Un bloc peut contenir entre 1 et 32 warps.
La grille regroupe l’ensemble des blocs. Le nombre maximal de blocs pris en charge est de
65535.
24. Warp (chaîne en français) est un terme technique du tissage. Sur un métier à tisser, la chaîne correspond à
l’ensemble des fils (threads en anglais) tendus qui servent de support à la trame. Sur un métier à tisser, tous les
fils d’une même chaîne montent ou descendent simultanément ; ce qui correspond au comportement des threads
d’un même warp.
42
2.1. Présentation des différentes architectures cibles
Appels RPC
CPUs GF100
Lien
Système hôte Carte périphérique
PCI Express
Figure 2.4 : Les GPUs NVIDIA dédiés au calcul intensif sont commercialisés sous forme de cartes
périphériques reliées au système hôte par un lien PCI Express.
À cette hiérarchie logique correspond une hiérarchie matérielle : les cœurs sont assemblés par
groupes de 32 au sein de multiprocesseurs de flux (Streaming Multiprocessors ou SM), comme
l’illustre la figure 2.5. Un GF100 possède 14 SM.
La grille est ordonnancée au niveau du GPU : un ordonnanceur (GigaThread dans la docu-
mentation technique de Fermi [117]) est chargé de distribuer les blocs aux différents SM. Chaque
SM peut prendre en charge simultanément jusqu’à huit blocs correspondant à un total maximal
de 1536 threads. Les autres blocs sont stockés dans une file d’attente.
Au sein d’un SM, deux warps peuvent s’exécuter simultanément. Les deux ordonnanceurs
de warps (Warp Scheduler sur la figure 2.5) et les deux décodeurs d’instruction (Instruction
Dispatch Unit sur la figure 2.5) sont chargés de décoder deux instructions : une pour chacun des
deux warps actifs. Chaque warp pourra exécuter son instruction sur une des unités fonctionnelles
de calcul suivante :
– deux unités fonctionnelles regroupant chacune 16 cœurs de calcul,
– une unité fonctionnelle regroupant 16 unités d’accès mémoire,
– une unité fonctionnelle regroupant 4 unités de calcul dédiées aux fonctions transcendantes
(sinus, cosinus, exponentielle, logarithme) et représentées par les quatre SFU (pour Special
Function Unit) sur la figure 2.5.
La description du fonctionnement SMT d’un SM est décrite en détails par Matsushita
dans [136]. La principale différence est la gestion par warps des unités fonctionnelles dans un
SM alors que l’architecture de Matsushita traite de simples threads. La gestion des unités fonc-
tionnelles par warp implique qu’à chaque cycle, tous les threads d’un même warp exécutent la
même instruction. Si les 32 threads d’un warp doivent exécuter la même instruction de calcul non
transcendante, cette instruction sera exécutée en un cycle par les 16 cœurs du groupe fonction-
nel 25 . Au contraire, si les 32 threads doivent exécuter des instructions différentes, ces dernières
ne pourront être décodées simultanément et leur exécution sera sérialisée. Un SM est une unité
de calcul dite Single Instruction Multiple Threads (SIMT) [133]. Si un processeur SIMT est
plus souple d’utilisation qu’un processeur SIMD, les performances maximales ne pourront être
obtenues que dans le cas où la même instruction est exécutée par tous les threads d’un warp.
Pour qu’un algorithme puisse s’exécuter de manière optimale sur un GF100, il faut donc qu’il
puisse être implémenté pour un processeur multicœur avec des unités de calcul SIMD à 32 voies.
Sur une carte Tesla C2050, le ratio entre la puissance de calcul théorique et la bande passante
théorique implique qu’il faut un minimum de 57 opérations de calcul flottant simple précision
par accès mémoire pour exploiter au mieux la puissance de calcul disponible. Une attention par-
25. Les cœurs de calcul ayant une fréquence deux fois supérieure à celle du décodeur d’instructions, chaque
cœur peut exécuter deux fois la même instruction par cycle de ce décodeur d’instructions.
43
Chapitre 2. Programmation multicible : une structure de données par cible ?
Instruction Cache
LD/ST
Core Core Core Core
LD/ST
SFU
LD/ST
Core Core Core Core
LD/ST
LD/ST
Core Core Core Core
LD/ST
SFU
LD/ST
Core Core Core Core
LD/ST
Interconnect Network
Uniform Cache
44
2.1. Présentation des différentes architectures cibles
ticulière doit donc être portée à la réutilisation des données. Afin de faciliter ce travail, NVIDIA
a inclus une hiérarchie de mémoire caches dans le GF100. Comme pour le CPU, l’exploitation de
ce cache repose sur la localité spatio-temporelle des données. Cependant, comparé au CPU, le
GF100 dispose de très peu de cache : le cache de niveau L2, partagé entre tous les SM, n’est que
de 768 Ko pour 21 504 threads (1024 threads pour chacun des quatorze SMs), soit en moyenne
36 octets par threads. Ce chiffre est à comparer aux plusieurs Mo disponibles par threads sur
un CPU. Pour exploiter efficacement un GF100, il faut donc que les jeux de données réutilisés
soient petits ou communs à plusieurs threads afin de rester dans les différents niveaux de cache.
CUDA C/C++ est une extension des langages C et C++ proposée par NVIDIA afin de
programmer les GPU en ajoutant un certain nombre de mots-clés, de fonctions et de variables.
Nous allons utiliser CUDA C/C++ [133] pour paralléliser le calcul de la norme. Nous allons dans
cette section présenter uniquement les concepts nécessaires à la compréhension des optimisations
effectuées dans la suite du document. Le lecteur plus intéressé pourra se référer au CUDA
Programming guide [133].
Avec CUDA C/C++, l’implémentation de ce calcul se divise en quatre parties.
1. Nous devons tout d’abord implémenter le noyau de calcul CUDA qui sera exécuté par
chaque thread GPU,
2. Nous devons ensuite nous assurer que les vecteurs sont présents dans la mémoire GPU en
implémentant les transferts de données entre le système hôte et le GPU,
3. Nous devons implémenter l’appel permettant de lancer l’exécution du noyau de calcul
CUDA sur le GPU. Cet appel est asynchrone, ce qui signifie que CUDA permet au pro-
gramme CPU de continuer son déroulement pendant que le noyau de calcul s’exécute sur
GPU.
4. Nous devons enfin synchroniser le CPU sur le GPU avant d’effectuer une copie du résultat
depuis le GPU vers le système hôte. Nous utiliserons pour cela une fonction de recopie
contenant une synchronisation.
NVIDIA préconise d’utiliser les techniques de méta-programmation C++ pour générer les
versions du noyau optimisées pour différentes tailles de blocs sur ce type d’application [137].
Afin de simplifier l’écriture du noyau de calcul de notre exemple, nous allons nous limiter au cas
où les blocs contiennent 512 threads ; chaque bloc contient alors 16 warps.
45
Chapitre 2. Programmation multicible : une structure de données par cible ?
26. L’utilisation d’une opération atomique permet de garantir qu’il n’y a pas eu d’accès à ce nombre entre la
lecture de la valeur en mémoire et l’écriture du résultat.
27. Ces objets contiennent d’autres champs (y et z) utilisés lorsque la grille ou les blocs possèdent plus d’une
dimension.
46
2.1. Présentation des différentes architectures cibles
21 float partialNorm = 0. f ;
22 // Les threads d ’ un warp accedent a des donnees spatialement proches
23 for ( int i = globalThreadIndex ; i < n ; i += globalNbThreads ) {
24 float tmp = a * W [ i ]+ b * X [ i ]+ c * Y [ i ];
25 partialNorm += tmp * tmp ;
26 }
27
28 // chaque thread du bloc partage son resultat partiel
29 scratchpad [ threadIdx . x ]= partialNorm ;
30 __syncthreads () ;
31
32 // Les threads effectuent une reduction binaire :
33 // a chaque etape , le nombre de threads actifs est divise par 2
34 if ( threadIdx .x <256) // la moitie des 512 threads actifs continuent
35 scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +256];
36 else return ; // Les threads inactifs ne doivent plus etre synchronises
37 __syncthreads () ;
38 if ( threadIdx .x <128) // la moitie des 256 threads actifs continuent
39 scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +128];
40 else return ; // Les threads inactifs ne doivent plus etre synchronises
41 __syncthreads () ;
42 if ( threadIdx .x <64) ) // la moitie des 128 threads actifs continuent
43 scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +64];
44 else return ; // Les threads inactifs ne doivent plus etre synchronises
45 __syncthreads () ;
46 // la moitie des 64 threads actifs continuent
47 // Il n ’y a plus qu ’ un warp actif (32 threads ) :
48 // execution synchrone des threads
49 if ( threadIdx .x <32) {
50 // Pour empecher le compilateur de supprimer les acces a la memoire
51 volatile float * scratchpad_ = scratchpad ; // on utilise volatile
52 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +32];
53 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +16];
54 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +8];
55 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +4];
56 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +2];
57 scratchpad_ [ threadIdx . x ]+= scratchpad_ [ threadIdx . x +1];
58 }
59
60 // le thread 0 du bloc possede le resultat partiel du bloc
61 if ( threadIdx . x ==0) {
62 // le resultat du bloc est accumule dans l ’ accumulateur global
63 atomicAdd (& result , scratchpad [0]) ;
64
65 // si tous les resultats partiel ont ete accumules
66 if ( atomicInc (& count , gridDim . x ) == gridDim .x -1) {
67 // le noyau retourne le resultat
68 * returnResult = sqrtf ( result ) ;
69 // les variables globales sont reinitialises pour
70 // les prochains appels
71 count =0; result =0. f ;
72 }
73 }
74 }
47
Chapitre 2. Programmation multicible : une structure de données par cible ?
données. En effet, sur GPU, lorsqu’un thread accède à un élément en mémoire, une ligne de
cache de seize octets (quatre nombres flottants) est mise en cache. Sur CPU, il faut minimiser
le nombre de threads accédant à une même ligne de cache afin de limiter les false sharing [138].
Cependant, une telle approche nécessite que le cache puisse contenir quatre itérations de calcul
pour chaque thread. Une itération de calcul nécessite douze octets de données correspondant à
W[i], X[i] et Y[i]. Sur GPU, stocker les données pour quatre itérations, soit 48 octets par
thread en mémoire cache est impossible : les caches L1 et L2 sont trop petits et ne peuvent
stocker que 32 octets par thread. Cette implémentation utilise une particularité du cache L1 du
GF100 : il est partagé au sein d’un SM. Il n’y a donc pas de protocole de cohérence mis en
œuvre si deux threads appartenant à un même bloc accèdent à la même ligne de cache. Cette
solution permet donc de minimiser le nombre d’accès à la RAM en garantissant que toutes les
données mises en cache sont immédiatement utilisées.
La portion de code comprise entre les lignes 32 et 58 (page précédente) correspond à l’opéra-
tion de réduction effectuée au sein de chaque bloc. Cette opération est effectuée selon un arbre
binaire comme illustré sur la figure 2.6. À chaque itération, le nombre de threads actifs est divisé
par deux. Ces threads vont ajouter à leur norme partielle la norme partielle des autres threads.
Ces échanges de données se font en mémoire partagée : il s’agit d’un espace mémoire accessible
par tous les threads d’un bloc. Afin de garantir la validité des valeurs lues, l’exécution des threads
est synchronisée à chaque itération, après les écritures. Ces synchronisations étant coûteuses,
leur nombre doit être minimisé. À partir du moment où seuls 32 threads sont actifs (ligne 49),
ils appartiennent tous au même warp et sont donc exécutés par le SM de manière synchrone. Il
est alors contreproductif d’ajouter des synchronisations. Les itérations restantes de la réduction
sont donc simplement enchaînées.
Le compilateur CUDA suppose cependant que les échanges de données entre threads donnent
toujours lieu à des synchronisations entre les threads. Dans le cas contraire, il peut supprimer
certaines écritures en mémoire et conserver les données correspondantes en registre. Les lignes :
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +32];
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +16];
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +8];
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +4];
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +2];
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +1];
sont en effet considérées par le compilateur comme équivalentes à
scratchpad [ threadIdx . x ]+= scratchpad [ threadIdx . x +32]
+ scratchpad [ threadIdx . x +16]
+ scratchpad [ threadIdx . x +8]
+ scratchpad [ threadIdx . x +4]
+ scratchpad [ threadIdx . x +2]
+ scratchpad [ threadIdx . x +1];
L’utilisation d’un pointeur qualifié de volatile permet de prévenir la suppression des accès en
mémoire partagée par le compilateur. Nous redéfinissons donc ligne 51 un pointeur scratchpad_.
Enfin, les résultats partiels de chaque bloc sont accumulés dans une variable globale result
initialisée à 0. Le dernier bloc à avoir effectué son accumulation écrit alors le résultat dans
l’espace mémoire donné par l’utilisateur et réinitialise les variables globales.
Nous venons de voir comment implémenter efficacement un noyau permettant de calculer
la norme d’une expression vectorielle. Afin d’utiliser ce noyau, nous devons maintenant copier
les tableaux W, X et Y depuis la mémoire du système vers la mémoire de la carte périphérique.
Nous pourrons ensuite lancer l’exécution du noyau de calcul normKernel avant de transférer le
résultat depuis la mémoire de la carte vers la mémoire système.
48
2.1. Présentation des différentes architectures cibles
Step 1 Thread
Stride 8 IDs
0 1 2 3 4 5 6 7
Values 8 -2 10 6 0 9 3 7 -2 -3 2 7 0 11 0 2
Step 2 Thread
Stride 4 IDs 0 1 2 3
Values 8 7 13 13 0 9 3 7 -2 -3 2 7 0 11 0 2
Step 3 Thread
Stride 2 IDs 0 1
Values 21 20 13 13 0 9 3 7 -2 -3 2 7 0 11 0 2
Step 4 Thread
IDs 0
Stride 1
Values 41 20 13 13 0 9 3 7 -2 -3 2 7 0 11 0 2
Figure 2.6 : Arbre de réduction binaire d’une réduction efficace sur processeur GF100.
Source : tutoriel d’optimisation CUDA présenté par Mark Harris à SuperComputing 2007 [137]
1 float *W , *X , * Y ;
2 ...
3 float * W_gpu , * X_gpu , * Y_gpu ;
4 /* Allocation sur le GPU a l ’ aide de CudaMalloc */
5 ...
6 float norm ;
7 const uint memSize = n *4; // 4 = sizeof ( float )
8 // Les donnees sont transferees depuis la memoire systeme vers le GPU
9 cudaMemcpy (( void *) W_gpu , ( void *) W , memSize , c ud aM em cpy Ho st To Dev ic e ) ;
10 cudaMemcpy (( void *) X_gpu , ( void *) X , memSize , c ud aM em cpy Ho st To Dev ic e ) ;
11 cudaMemcpy (( void *) Y_gpu , ( void *) Y , memSize , c ud aM em cpy Ho st To Dev ic e ) ;
12 dim3 Dg ( n /(512*10) ,1 ,1) ; // chaque thread traitera 10 elements de vecteur
13 dim3 Db (512) ;
14 // Le noyau de calcul est parametre avec Dg et Db puis lance sur le GPU
15 normKernel <<< Dg , Db >>>(a , W_gpu , b , X_gpu , c , Y_gpu , n , result_gpu ) ;
16 // Le resultat est transfere depuis le GPU vers la memoire systeme
17 cudaMemcpy (( void *) & norm , ( void *) result_gpu , 4 , c ud aM em cpy De vi ce ToH os t ) ;
18 }
49
Chapitre 2. Programmation multicible : une structure de données par cible ?
tion, nous allons tout d’abord décomposer cette opération en deux opérations mettant en œuvre
deux types de parallélisme différents.
1. La première est une opération vectorielle correspondant au calcul des valeurs de l’expres-
sion vectorielle aX + bW + cZ et où toutes les itérations sont indépendantes les unes des
autres.
float * tmp ;
for ( int i =0; i < n ; ++ i ) {
tmp [ i ] = a * W [ i ]+ b * X [ i ]+ c * Y [ i ];
}
2. La seconde est une opération de réduction pour calculer la norme de cette expression.
float norm = 0;
for ( int i =0; i < n ; ++ i ) {
norm += tmp [ i ]* tmp [ i ];
}
norm = sqrtf ( norm ) ;
La fusion de ces deux boucles permet de réduire le tableau tmp à une simple variable et donc
d’obtenir le code présenté page 8.
Nous allons maintenant comparer l’expression de ces deux types de parallélisme dans les
deux implémentations optimisées (page 39 et 46).
tmp[i] = a*W[i]+b*X[i]+c*Y[i]
50
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
L’ensemble des éléments à réduire afin de calculer le résultat de l’opération souhaitée. Dans
notre cas, il s’agit de l’ensemble des résultats de l’opération vectorielle.
Dans le code, (page 39), optimisé pour les architectures X86_64, la fonction de réduction
n’est pas exprimée de la même manière pour les deux niveaux de parallélisme. Pour le paral-
lélisme multicœur, cette opération est définie en rajoutant la directive OpenMP reduction(+:
result) à la ligne 6. L’élément neutre est simplement utilisé lors de l’initialisation de result. La
fonction __mm_dp_ps (ligne 29) effectue à la fois la dernière opération vectorielle et l’opération
de réduction. L’expression de cette fonction n’est pas directement apparente dans les fonctions
intrinsèques SSE. Cette opération de réduction pourrait tout de même être exprimée de manière
isolée en utilisant une autre fonction intrinsèque (__mm_hadd_ps). Aux deux niveaux de paral-
lélisme sur CPU correspondent donc, ici aussi, des paradigmes de programmation différents.
Dans le code optimisé pour le GF100, l’opération de réduction est effectuée entre les lignes 32
et 73 (page 47). Nous pouvons dans ce cas distinguer les deux niveaux de parallélisme :
– au sein des blocs de threads, cette implémentation de la réduction accède aux résultats
partiels calculés par les différents threads en utilisant la mémoire partagée disponible sur
chaque SM (lignes 32 à 58) ;
– entre les différents blocs, cette réduction utilise un accumulateur en mémoire RAM et des
opérations atomiques pour synchroniser les communications (lignes 61 à 73).
L’élément neutre de la réduction est précisé pour chacun de ces niveaux (lignes 4 et 21). L’implé-
mentation de chacun de ces deux niveaux pourrait être abstrait dans une fonction équivalente
à une fonction intrinsèque SIMD comme __mm_hadd_ps. Pour un développeur, le GF100, et
plus généralement tout processeur SIMT, peut donc aussi être considéré comme un processeur
vectoriel pour les opérations de réduction.
Les deux opérations parallèles que nous avons identifiées se programment différemment sur
les deux architectures cibles qui nous intéressent : les CPUs X86_64 multicœurs et les GPU
GF100. Le tableau 2.1 récapitule les différentes approches utilisées pour exprimer l’opération
vectorielle et l’opération de réduction sur ces deux architectures. Nous pouvons constater que la
gestion des différentes cibles matérielles a un impact très important sur la maintenance des codes.
En effet, le code C à l’origine de notre problème (page 8) comporte six lignes. Si une application
utilisant ce code devait disposer de différentes branches pour différentes architectures cibles, il
faudrait ajouter à ces 6 lignes 30 lignes pour la version X86_64 optimisée et 82 lignes pour la
version CUDA. Le nombre de lignes serait donc multiplié par 25 malgré la simplicité de cette
fonction. Cette augmentation du nombre de lignes de code s’ajoute aux autres problèmes que
nous avons listés dans le chapitre d’introduction. Cette étude montre donc une nouvelle fois la
nécessité d’utiliser des outils permettant d’abstraire l’architecture matérielle et en particulier
l’expression du parallélisme.
Dans la prochaine section, nous allons étudier l’optimisation d’un exemple plus complexe,
mettant en œuvre des structures de données également plus complexes que le simple vecteur util-
isé jusqu’à présent. Nous constaterons alors que selon l’architecture matérielle ciblée, le format
de stockage optimal diffère.
51
Chapitre 2. Programmation multicible : une structure de données par cible ?
mémoires sur GPU. Dans cette section, nous allons étudier la parallélisation et l’optimisation
d’opérations plus complexes. Nous nous intéresserons tout particulièrement aux adaptations qui
doivent être apportées aux structures de données afin de permettre l’utilisation efficace des unités
SIMD et SIMT.
Notre expérience d’utilisation de Legolas++ [34] pour mettre au point des solveurs d’algèbre
nous a conduit à identifier quatre familles d’opérations récurrentes dans les solveurs d’algèbre
linéaire creux et structurés :
Les deux premières familles d’opérations sont relativement simples à traiter et correspondent
au « Hello world » de toutes les approches de programmation parallèles. Elle font en effet partie
des exemples classique de programmation sur toutes les architectures matérielles [137, 139,
140, 141, 142]. Tous les exemples de parallélisations que nous avons présentés jusqu’à présent
correspondent à ces deux familles d’opérations.
La troisième famille d’opérations correspond à la généralisation de la transposition de tableaux
bidimensionnels pour les tableaux comportant plus de deux dimensions et dispose également
d’une abondante littérature sur les différentes architectures matérielles [143, 144, 145, 146, 137,
147]. Les opérations de permutations se traitent d’une manière relativement semblable sur les
différentes architectures : l’idée principale est de décomposer l’opération globale en un ensemble
de permutations de tuiles qui seront transposées dans une mémoire « proche » (mémoire locale
ou mémoire cache). L’article de Michael Bader [147] explique très clairement cette idée et montre
son implémentation pour GPU.
La dernière famille d’opérations est en revanche moins bien décrite dans la littérature. Dans
cette section, nous présenterons un exemple appartenant à cette famille d’opérations ainsi que
des implémentations optimisées pour CPU et GPU. Nous étudierons ensuite l’impact de ces
optimisations sur les structures de données multidimensionnelles. Nous déduirons de cette étude
les transformations à apporter aux structures de données afin d’exploiter efficacement les unités
SIMD et SIMT.
52
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
Figure 2.7 : Matrice diagonale à blocs bandes et symétriques. Cette matrice contient 4 blocs
tridiagonaux de taille 8×8
53
Chapitre 2. Programmation multicible : une structure de données par cible ?
Données
A : une matrice bande, symmetrique et définie positive
Ad : la matrice A décomposée sous la forme LDLT
D : une matrice diagonale
L : une matrice bande inférieure avec une diagonale unitaire
LT : la matrice transposée de L
hbw : la demi largeur de bande (Half BandWidth) de A, correspond au nombre
de diagonales de L
n : nombre de lignes de A
B : le vecteur membre de droite
X : le vecteur inconnu
/* Étape 1 : LX = B */
// Coin supérieur // Descente
pour i ← 0 a hbw faire // Coin supérieur
pour j ← 0 a i faire pour i ← 0 a hbw faire
Ad ij ← Aij X i ← Bi
pour k ← 0 a j faire pour j ← 0 a i faire
Ad ij ← Ad ij − Ad ik Ad jk X i ← X i − Ad ij B j
54
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
La classe Vector dispose d’une interface classique : les opérateurs crochets [] permettent
d’accéder aux éléments du vecteur. Nous verrons lors de l’implémentation SSE qu’il n’est pas pos-
sible de laisser le type des données float dans l’interface. Ce type est donc défini par RealType.
La macro INLINE correspondra à des mots-clé différents selon les implémentations. Dans
les implémentations X86_64, elles définiront le mot-clé C++ inline. Dans l’implémentation
CUDA, elle définira les mots-clés __device__, __host__ et inline afin que ces fonctions soient
aussi compilées pour le GPU. Notons qu’une instance de la classe Vector peut être construite
sur CPU ou sur GPU.
Nous allons maintenant présenter la classe Vector2D. Cette classe ne dispose pas de con-
structeur, ce qui est relativement inhabituel pour une classe C++. Il s’agit d’une des contraintes
à respecter pour permettre aux instances de cette classe d’être copiées comme des structures
C (avec memcpy par exemple). En effet, nous devrons passer une instance de cette classe aux
noyaux de calcul CUDA. Les contraintes imposées par CUDA pour ce type de classe sont as-
sez proches des contraintes du C++ pour la définition des POD 28 (Plain Old Data) [74]. À
notre connaissance, ces contraintes ne sont pas complètement formalisées dans la documenta-
tion CUDA 29 . Notre expérience nous permet cependant de définir les contraintes suivantes :
– la classe ne doit pas définir de constructeur,
– la classe ne doit pas contenir de données membres statiques 30 ,
– la classe ne doit pas contenir de fonctions virtuelles,
– tous les membres données de la classe doivent respecter ces mêmes conditions ou être des
types scalaires au sens de la norme du C++ [74].
La classe Vector2D comportera donc deux fonctions initialize et copy qui remplacent
les constructeurs et allouent la mémoire GPU tandis qu’une fonction finalize fait office de
destructeur et désalloue la mémoire. Notons que ces trois fonctions n’ont pas vocation à être
exécutées sur le GPU et ne prennent que le mot-clé inline (et pas INLINE). L’opérateur crochets
de la ligne 13 construit à la volée les instances de la classe Vector représentant un bloc Xi de
vecteur. Les instances de cette classe ne font l’objet d’aucun transfert entre la mémoire système
et la mémoire du GPU. C’est ce qui permet à la classe Vector de ne pas être soumise à ces
contraintes.
28. Les POD sont des types compatibles avec C. Il s’agit soit de types scalaires au sens de la norme, soit de
classes POD. Une classe POD ne contient pas de constructeurs, n’a pas de données non statiques privées et ces
dernières doivent être des POD ou des types scalaires au sens de la norme.
29. Seules les contraintes pesant sur les classes pouvant être utilisées dans les noyaux CUDA sont spécifiées
dans l’appendice D du CUDA programming guide [133]
30. Les POD peuvent contenir des données statiques. C’est une des différences entre les contraintes POD et les
contraintes CUDA.
55
Chapitre 2. Programmation multicible : une structure de données par cible ?
1 class Vector2D {
2 public :
3 // initialize() remplace le constructeur
4 // alloue l ’ espace memoire pour tous les vecteurs 1 D
5 inline void initialize ( int nbBlocks , int sizeOfBlocks ) ;
6 // finalize() remplace le destructeur
7 // desalloue la memoire
8 inline void finalize () ;
9 // copy() remplace le constructeur par recopie
10 inline void copy ( const Vector2D & rhs ) ;
11 // operator[] cree a la volee une instance de Vector
12 // cette instance dispose d ’ un espace memoire propre fonction de son indice
13 INLINE Vector operator []( int block ) ;
14 INLINE int nbBlocks () const ;
15 };
Nous pouvons utiliser ce jeu de classes pour implémenter la fonction effectuant la descente
remontée en place (c.f. Alg. 2.2). Cette fonction prend en argument un bloc de matrice Adii
préalablement factorisée et un bloc de vecteur Xi préalablement initialisé pour être égal à Bi .
En sortie de la fonction, Xi contient la solution du système Adii Xi = Bi .
1 INLINE void solveProblemBlock (
2 const B a ndSymmetricMatrix & A_factorized
3 , Vector & X )
4 {
5 const int hbw = A_factorized . hbw () ;
56
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
Avant de nous focaliser sur les implémentations optimisées de ces classes, nous allons nous
pencher le temps de cette section sur les formats de stockage des données.
Les données de la matrice A et du vecteur X peuvent être représentées sous la forme d’un
tableau à deux dimensions tel que représenté sur la figure 2.8 : chaque ligne de couleur contient
alors les données d’un bloc Aii ou Xi . Nous avons vu dans la section 1.2.3.1 qu’il existe différents
formats de stockage pour les tableaux bidimensionnels : par ligne, par colonne, selon un ordre
de Morton, etc. La figure 2.9 représente différents formats de stockage pour le tableau de la
figure 2.8. Pour chaque sous-figure, nous avons représenté l’ordre de parcours des éléments
dans le tableau et représenté l’espace mémoire occupé par ce tableau. Nous avons représenté
57
Chapitre 2. Programmation multicible : une structure de données par cible ?
différents formats de stockage entrelaçant les lignes et les colonnes. La figure 2.9(a) représente
le cas où il n’y a pas d’entrelacement : les données sont stockées lignes par lignes. À l’opposé, la
figure 2.9(e) représente le cas où l’entrelacement est complet : les données sont stockées colonne
par colonne. Entre les deux, les figures 2.9(b) à 2.9(d) représentent des situations intermédiaires.
Nous appellerons facteur d’entrelacement le nombre de lignes dont les éléments sont entrelacés.
L’entrelacement de facteur quatre (ou entrelacement quatre par quatre) représenté sur la fig-
ure 2.9(d) est particulièrement intéressant. En effet, dans ce cas, il n’est pas possible d’entrelacer
parfaitement les lignes et les colonnes car le nombre de lignes n’est pas multiple de quatre. Dans
ce cas, il convient d’ajouter des élément de remplissage (padding), représentés en rouge dans le
parcours. Cela implique qu’une partie de l’espace mémoire utilisé est perdue (les espaces blancs).
Un facteur d’entrelacement trop important peut donc avoir un coût élevé sur la consommation
mémoire d’une application.
Nous souhaitons vectoriser la descente-remontée du problème AX = B sur une unité SIMD
à deux voies. Les sous problèmes Aii Xi = Bi étant indépendants, une solution serait que chaque
voie de l’unité SIMD effectue la descente-remontée d’un bloc. L’utilisation des unités SIMD
suppose de travailler avec des « paquets » de données scalaires. Prenons l’exemple de la ligne 11
(page précédente) :
x [ i ] -= A_factorized (i , j ) * X [ j ];
Afin de pouvoir effectuer simultanément cette opération sur les deux voies de l’unité SIMD, il
faut que l’élément x[i] appartenant à un vecteur bloc X2k et celui appartenant au vecteur bloc
X2k+1 soient adjacents en mémoire. De même, les éléments A_factorized(i,j) des bloc 2k et
2k + 1 doivent être stockés de manière contiguë. Ce qui suppose que le facteur d’entrelacement
soit un multiple de deux.
Nous allons maintenant proposer des implémentations parallèles des classes de vecteurs et
de matrices. Nous ferons varier le facteur d’entrelacement de ces différentes implémentations et
mesurerons l’impact de ce paramètre sur les performances. Nous constaterons alors que pour
chaque architecture, un facteur d’entrelacement optimal permet d’obtenir les meilleures perfor-
mances.
Dans cette section, nous allons détailler l’implémentation de la version X86_64 des classes
de vecteurs et de matrices. Comme nous l’avons vu dans la section 2.1.3, deux niveaux de
parallélisme sont disponibles dans ces processeurs. Le niveau le plus bas correspond aux unités
SIMD tandis que le niveau le plus haut correspond aux différents cœurs disponibles. Nous
nous intéresserons dans un premier temps à l’utilisation des unités SIMD. Nous verrons alors
comment il est possible de masquer l’utilisation des fonctions intrinsèques SSE en utilisant des
classes adéquates. Dans un second temps, nous nous intéresserons à l’exploitation du parallélisme
multicœur.
58
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
Figure 2.9 : Différents formats de stockage pour une structure de données rectangulaire.
59
Chapitre 2. Programmation multicible : une structure de données par cible ?
Parallélisation SIMD L’utilisation de classes C++ permet d’alléger l’usage des fonctions in-
trinsèques. Il s’agit pour cela de définir des types adaptés permettant par exemple de
masquer les fonctions intrinsèques dans les opérateurs de la classe. Différentes implémen-
tations de telles classes C++ existent [150, 151]. Nous allons ici simplement en rappeler le
principe et montrer l’impact que l’utilisation de telles classes peut avoir sur l’implémenta-
tion de notre problème.
La classe SSEData définie ci-après représente un paquet de quatre valeurs flottantes simple
précision (float) dont les opérateurs sont surchargés pour utiliser les unités SSE.
struct SSEData {
// le type __m128 possede la meme representation que float[4]
union {
__m128 data_ ; // type « paquet » defini pour les unites SSE
float f_ [4]; // type equivalent pour l ’ acces aux scalaires
};
...
inline const SSEData & operator += ( const SSEData & b ) {
return * this = _mm_add_ps ( data_ , b . data_ ) ;
}
...
};
// d ’ autres surcharge d ’ operateurs
inline SSEData operator *( const float & a , const SSEData & b ) ;
...
L’union anonyme définie entre les lignes 3 et 6 représente le paquet de données scalaires. Le
type de données __m128 est utilisé par les fonctions intrinsèques et sa définition varie selon
les compilateurs. Afin d’accéder aux différents éléments scalaires, nous devons réinterpréter
ce type comme un tableau de quatre nombres flottants (float f_[4]). C’est ce qui nous
permet par exemple d’initialiser les matrices et vecteurs.
Afin de vectoriser la descente-remontée (cf. Alg. 2.2) et de traiter plusieurs blocs simul-
tanément, nous allons définir le type de réel (RealType) comme étant SSEData :
1 // code defini selon l ’ architecture ciblee
2 inline int interleaving ( int I , int i , int J , int j ) {
3 return i * J + j ; // pas d ’ entrelacement
4 // return (( i / N ) * J + j ) * N + i % N ; // entrelacement de facteur N
5 // return j * I + i ; // entrelacement complet
6 }
7
8
9 class B andSymmetricMatrix {
10 public :
11 // RealType correspond a SSEData afin
12 // de beneficier de la surcharge des operateurs
13 typedef SSEData RealType ;
14
15 // le troisieme argument (data) est l ’ adresse de la zone
16 // memoire deja allouee par BlocDiagonalMatrix_BSM
17 inline BandSymmetricMatrix ( int size , int hbw , RealType * data ,
18 int nbBlocks , int block
19 );
20 ... // definition de hbw_, block_, size_, data_ et nbBlocks_
21 inline RealType & operator () ( int i , int j ) {
22 // conversion des indices 2 D (i,j) en un indice 1 D I
23 const int I = i *( hbw_ ) +i - j ;
24 const int J = block_ ;
60
2.2. Optimisation pour CPU et pour GPU d’un exemple plus complexe
61
Chapitre 2. Programmation multicible : une structure de données par cible ?
Dans les sections précédentes, nous avons exposé un problème d’algèbre linéaire dont l’op-
timisation est plus intrusive que pour les opérations vectorielles présentées section 2.1. Nous
avons décrit deux implémentations optimisées de la résolution de ce problème : une pour CPU
X86_64 et l’autre pour GPU GF100. Nous allons dans les deux prochaines sections introduire
les différentes configurations matérielles dont nous disposons puis les performances obtenues sur
ces différentes architectures. Nous déduirons de cette étude le format de stockage des données
optimal pour chaque architecture.
62
2.4. Analyse des performances : à chaque architecture sa structure de données
GFlops Go/s
Nom Processeur Date de sortie Mémoire
th. obs. th. obs.
Fermi Tesla C2050 04/2010 4 Go 959,6 608,4 134,1 90,0
Développé par John D. McCalpin, STREAM utilise trois opérations mettant en œuvre deux
ou trois vecteurs de données. Nous avons étendu ce test en ajoutant des opérations compor-
tant jusqu’à huit vecteurs. Notons que selon la configuration matérielle, ce n’est pas la même
opération qui offre le meilleur débit de données.
Notre machine de référence, utilisée pour la grande majorité des mesures, est la machine
32 Nehalem. Celle-ci comporte deux processeurs quadri-cœurs INTEL Xeon E5504 de type
2×4
Nehalem EP [153, 154, 155]. Il s’agit de la configuration par défaut des postes scientifiques
déployés à EDF R&D en 2010 et 2011. La machine 1×4 32 SandyBridge comporte un processeur
quadri-cœurs INTEL Core i5-2500 de génération Sandy-Bridge [156, 154, 155]. Il s’agit de notre
seule configuration disposant d’unité AVX.
Nous avons testé différents compilateurs pour effectuer nos mesures (INTEL icpc 12.0.2,
clang 2.9, g++ 4.3 à 4.6). Le compilateur offrant les meilleures performances sur la majorité de
nos cas tests est g++ 4.6. Nous utiliserons donc ce compilateur pour toutes nos mesures.
Le tableau 2.3 décrit les caractéristiques de la carte GPU NVIDIA TESLA C2050 que nous
avons utilisée. La puissance de calcul a été mesurée à l’aide de la bibliothèque CUBLAS [157],
l’implémentation des BLAS fournie par NVIDIA. Le débit de données maximal a été mesuré à
l’aide du test bandwidthTest du kit de développement logiciel CUDA. Nous utilisons la version
4.0 du compilateur CUDA nvcc.
63
Chapitre 2. Programmation multicible : une structure de données par cible ?
Paramètre Valeurs
Nombre de blocs 10 000, 20 000, 30 000, 40 000, 50 000, 75 000, 100 000
Taille des blocs 25, 50, 100, 200, 500
hbw 2, 3, 4, 6, 8, 10, 15
Entrelacements 11, 41, 8, 16, 32, complet
1
Valeurs utilisées si compatibles avec l’architecture ciblée.
0.8
0.4
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
Le tableau 2.4 indique pour chaque paramètre l’ensemble des valeurs utilisées pour effectuer
nos mesures. Plusieurs de nos machines (1×4 32 SandyBridge et Fermi) ne disposent que de 4 Go
de mémoire RAM ce qui limite les combinaisons de paramètres utilisés dans nos tests de perfor-
mances.
Nous nous intéresserons uniquement au temps nécessaire pour effectuer l’étape de descente-
remontée et ignorons celui nécessaire à la factorisation. En effet, il est classique que la matrice
factorisée soit utilisée plusieurs fois afin de résoudre des systèmes correspondant à des seconds
membres B différents. Dans le solveur SPN , le résultat de la factorisation peut être réutilisé une
fois par itération, soit environ 70 fois dans les résultats présentés dans notre article [36]. Les
performances seront analysées et comparées de manière détaillée dans le chapitre 6. Nous verrons
alors que le nombre de blocs a relativement peu d’influence sur les performances obtenues. Pour
chaque combinaison des trois autres paramètres, nous décrirons donc les performances moyennes
observées quelque soit le nombre de blocs.
Les figures 2.10 à 2.15 représentent pour chaque configuration de bloc le nombre d’opérations
réalisées par seconde pour différents formats de stockage.
Les figures 2.10 et 2.11 présentent les performances obtenues lors d’exécutions séquentielles
32 Nehalem et
sur 2×4 32
1×4 SandyBridge. Ces performances correspondent à ce que nous pouvons
attendre de machines dotées d’un processeur mono-cœur et ne disposant pas d’unités vectorielles.
Dans cette configuration, le format de stockage ligne par ligne offre les meilleures performances
dans presque tous les cas. Ce format de stockage parait donc le mieux adapté pour une utilisation
purement séquentielle des processeurs X86_64 dotés de mémoire cache. Ce résultat n’est pas
surprenant : c’est le format qui conserve le mieux la localité spatiale des données. En effet, plus
le facteur d’entrelacement est petit, plus le stockage d’un bloc est compact, c’est-à-dire qu’il
réduit l’espace compris entre le premier élément du bloc et le dernier. De ce fait, les données
nécessaires à la descente-remontée peuvent être stockées dans les plus petits niveaux de cache.
Lors de la « remontée », les données sont donc directement accessibles. Les unités de calcul
64
2.4. Analyse des performances : à chaque architecture sa structure de données
1.6
0.8
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
peuvent alors commencer la remontée sans attendre que les données nécessaires retraversent les
différents niveaux de cache. À l’opposé, le stockage colonne par colonne augmente la probabilité
d’obtenir des défauts de cache et offre les plus mauvaises performances.
Les figures 2.12 et 2.13 présentent les performances obtenues lors d’exécutions parallèles
à deux niveaux utilisant les unités SSE de tous les cœurs du processeur sur les machines
32 32
2×4 Nehalem et 1×4 SandyBridge. Comme nous l’avons vu dans la section 2.2.2.2, l’entrelacement
minimal pour utiliser les unités SSE est alors de 4. Nous pouvons constater que les formats
correspondant à des facteurs d’entrelacement de 4 et de 8 offrent les meilleures performances
sur les différentes machines. Les performances sont en moyenne améliorées d’un facteur 17 sur
32 32
2×4 Nehalem et d’un facteur 7 sur 1×4 SandyBridge. Ce qui est inférieur au facteur d’accélération
32 attendu sur les deux machines.
10
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
65
Chapitre 2. Programmation multicible : une structure de données par cible ?
10
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
16
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
L’analyse des performances parallèles qui sera effectuée au chapitre 6 nous montrera que les
performances parallèles de cette opération sont ici limitées par la vitesse de transfert des données
entre le processeur et la mémoire RAM. Les formats de stockage permettant de minimiser le
nombre d’accès à la mémoire et la latence de ces derniers offrent alors les meilleures performances.
Le seul format de stockage qui ne permet pas à un problème Aii Xi = Xi de tenir dans le cache
L3 et qui génère donc plus d’accès mémoire que les autres est le format de stockage colonne
par colonne. Pour tous les autres, les données nécessaires à la résolution d’un problème tiennent
dans ce cache. Cependant, lorsque le facteur d’entrelacement dépasse 8, les données nécessaires à
la résolution d’un problème Aii Xi = Xi ne tiennent plus dans le cache L2. En effet, une analyse
de l’exécution du code à l’aide du logiciel INTEL VTune Amplifier XE 2011 [158] montre que
si le nombre de requêtes d’accès à la mémoire RAM reste comparable, le nombre de défauts du
cache L2 augmente fortement lorsque le facteur d’entrelacement dépasse 8. La latence des accès
mémoire est donc augmentée, grevant ainsi les performances.
La figure 2.14 présente les performances obtenues sur la machine 1×4 32 SandyBridge lorsque
l’on utilise les unités AVX et OpenMP. Nous pouvons observer sans surprise que les meilleures
performances sont obtenues avec le facteur d’entrelacement 8 : lorsque le facteur d’entrelacement
est plus grand, les données nécessaires à la résolution d’un problème Aii Xi = Xi ne tiennent plus
dans le cache L2. L’utilisation d’instructions AVX plutôt que SSE multiplie par 2 la puissance de
calculs disponible. Ces instructions n’ont cependant pas d’effet sur la bande passante disponible
et ne permettent pas d’améliorer les performances des codes dont les performances sont limitées
par la vitesse des accès à la mémoire RAM. Les performances obtenues avec les instructions
AVX sont très proches de celles obtenues avec les instructions SSE (figure 2.14), ce qui confirme
que la descente-remontée est limitée par la vitesse des accès à la mémoire RAM.
66
2.5. Programmation multicible : un code source unique, différents exécutables optimisés
Tableau 2.5 : Format de stockage optimal pour chaque type d’unité de calcul.
Enfin, la figure 2.15 présente les performances obtenues sur notre GPU Tesla C2050 (ma-
chine Fermi). Sur cette machine, il est possible d’utiliser tous les formats de stockage qui nous
intéressent : le fonctionnement SIMT des SM n’impose pas d’entrelacement minimal pour fonc-
tionner. Cependant, le principe SIMT implique aussi que le respect de l’entrelacement de l’unité
SIMD équivalente permet de minimiser la séquentialisation de l’exécution des différents threads.
Il est donc logique que les formats entrelacés 32 par 32 et en colonne permettent l’obtention des
meilleures performances. Notre expérience sur d’autres exemples montre que selon les cas, l’un ou
l’autre de ces deux formats permet l’obtention des meilleures performances. Nous privilégierons
dans la suite le format de stockage colonne par colonne car il a l’avantage de ne pas nécessiter
de remplissage supplémentaire des structures de données (padding), ce qui est important compte
tenu de la relativement faible capacité de la mémoire RAM du GPU (4 Go).
Finalement, nous pourrons retenir de cette expérience que pour chaque architecture, un
format de stockage particulier permet d’obtenir les meilleures performances. Le tableau 2.5
récapitule les formats de stockage optimaux pour les différentes architectures.
67
Chapitre 2. Programmation multicible : une structure de données par cible ?
Pour parvenir à cet objectif, nous souhaitons ajouter une couche logicielle intermédiaire entre
Legolas++ et les interfaces proposées par les constructeurs de matériel. Ceci permettra la mise
au point de Legolas++ et de ses extensions sans se soucier des différences entre les architectures
matérielles. En particulier, cette couche intermédiaire devra se charger d’interfacer les différents
environnements de développement (OpenMP, CUDA, etc.) et d’adapter le format de stockage
des données.
Dans le prochain chapitre, nous étudierons les différents environnements de développement
multicible pour machines parallèles. Nous nous intéresserons tout particulièrement aux abstrac-
tions offertes concernant les structures de données afin de les adapter à l’architecture matérielle.
À l’issue de cette étude, nous présenterons dans le chapitre 4, la conception d’une couche
logicielle prenant en charge le réordonnancement des données et le parallélisme multicible. Nous
déduirons de cette étude les conditions permettant l’adaptation automatique du format de stock-
age des données. Nous en déduirons plus généralement les contraintes d’écriture des codes mul-
ticibles.
Enfin, le chapitre 5 présentera la conception d’un démonstrateur montrant comment adapter
Legolas++ afin de mettre au point une version multicible.
68
La bibliographie se fait après et non avant
d’aborder un sujet de recherche.
Jean Baptiste Perrin (1870 – 1942)
Prix Nobel de physique de 1926
Chapitre 3
69
Chapitre 3. État de l’art des environnements de développement parallèle multicible
Sommaire
3.1 Vers un niveau d’abstraction plus élevé . . . . . . . . . . . . . . . . . 71
3.2 Éléments de discrimination . . . . . . . . . . . . . . . . . . . . . . . . 72
3.2.1 Type de conception de l’environnement . . . . . . . . . . . . . . . . . . 72
3.2.2 Niveau d’abstraction du parallélisme . . . . . . . . . . . . . . . . . . . . 72
3.2.3 Support des accélérateurs de calcul . . . . . . . . . . . . . . . . . . . . . 73
3.2.4 Adaptation du stockage à l’architecture matérielle . . . . . . . . . . . . 73
3.3 Différentes approches pour permettre le développement d’applica-
tions parallèles multicibles . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.3.1 Les approches basées sur la programmation événementielle . . . . . . . 73
3.3.2 Les approches basées sur les patrons parallèles . . . . . . . . . . . . . . 74
3.3.2.1 Différentes écoles concurrentes conduisent aux mêmes patrons 74
3.3.2.2 Exemples de squelettes algorithmiques . . . . . . . . . . . . . . 75
3.3.2.3 Exemples de mise en œuvre de patrons de conception parallèles 75
3.3.2.4 Les patrons parallèles par annotation de code source . . . . . . 76
3.3.3 Les approches basées sur la programmation par tableaux . . . . . . . . 77
3.3.4 Les autres approches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.3.5 Synthèse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.4 Choix stratégique : positionnement de MTPS . . . . . . . . . . . . . 81
70
3.1. Vers un niveau d’abstraction plus élevé
31. http://blogs.msdn.com/b/lukeh/archive/2007/10.aspx
71
Chapitre 3. État de l’art des environnements de développement parallèle multicible
Notons que l’objectif de cette thèse est exactement de répondre à cette problématique. En
proposant un DSL restreint à la mise au point de solveurs pour les problèmes d’algèbre linéaires
creux structurés, l’idée est bien entendu d’utiliser les propriétés de ce domaine afin de permettre
une exécution la plus performante possible sur différentes architectures matérielles.
72
3.3. Différentes approches pour permettre le développement d’applications parallèles multicibles
73
Chapitre 3. État de l’art des environnements de développement parallèle multicible
Erlang [177, 178] est un langage fonctionnel mis au point par la société Ericson en 1987 afin
de faciliter la mise au point des applications temps-réel dans le monde des télécommunications.
Il supporte les environnements à mémoire partagée ou distribuée et s’exécute sur les nombreuses
architectures matérielles disponibles dans ce contexte. Cependant, il ne s’exécute pas sur les
accélérateurs de calcul habituels (Cell, GPU). En effet, la mémoire disponible pour le code des
SPE dans le Cell est trop petite pour héberger un environnement d’exécution Erlang tandis que
l’architecture SIMD des GPU ne permet pas une bonne utilisation de ceux-ci avec Erlang.
Charm++ [179, 180] est une extension de C++ disponible depuis 1993 et permettant de
développer une application parallèle basée sur l’envoi de messages asynchrones. Avec Charm++,
l’utilisateur définit des composants (appelés chares) dont les fonctions sont activées par l’arrivée
de messages envoyés par d’autres chares. Charm++ permet de cibler les architectures en mémoire
partagée et en mémoire distribuée. Charm++ permet aussi de cibler le processeur Cell [181, 182]
ou d’encapsuler des chares s’exécutant sur GPU [183]. Dans ce dernier cas, l’utilisateur doit
fournir les noyaux de calcul optimisés pour le GPU.
Finalement, cette approche permet à l’utilisateur de décrire le graphe de dépendances entre
les tâches : à chaque message correspond un arc du graphe. Ce graphe peut ensuite être utilisé
pour obtenir une exécution parallèle de l’application.
74
3.3. Différentes approches pour permettre le développement d’applications parallèles multicibles
75
Chapitre 3. État de l’art des environnements de développement parallèle multicible
STL de paralléliser son code simplement en échangeant ses algorithmes et ses conteneurs avec
leurs équivalents parallèles.
Par exemple, STAPL [197, 198, 199] est une bibliothèque conçue en 1998 comme un sur-
ensemble de la STL. Lorsque les algorithmes de STAPL sont utilisés, l’environnement d’exécu-
tion choisit automatiquement l’implémentation en fonction des performances obtenues sur des
cas précédents [200]. Cet environnement permet aussi à STAPL de cibler les architectures à
mémoire partagée et à mémoire distribuée. Outre ce niveau « utilisateur », deux autres niveaux
d’utilisation de la bibliothèque permettant une plus grande liberté d’utilisation et d’optimisation
sont proposés. Le niveau « développeur » permet l’extension des fonctionnalités de STAPL en
ajoutant de nouveaux conteneurs et de nouveaux algorithmes tandis que le niveau « spécialiste »
permet de modifier l’environnement d’exécution.
STAPL introduit les concepts de pView [201] et de pRange qui généralisent les itérateurs de la
STL et permettent de manipuler les données indépendamment de leur stockage effectif. Un espace
mémoire correspondant à une matrice pourra ainsi être vu comme une matrice par colonnes ou
comme une matrice par lignes. Cependant, STAPL ne prend pas en charge le réordonnancement
automatique des données en mémoire pour permettre par exemple la vectorisation automatique
des algorithmes.
Mise au point par INTEL en 2006, la bibliothèque INTEL TBB [139] cible les architectures
programmables avec des threads (processeurs X86, X86_64, PowerPC) sous différents OS. Tout
comme STAPL, INTEL TBB utilise un concept d’itérateur généralisé. D’une certaine manière,
la bibliothèque INTEL TBB peut être considérée comme une restriction de STAPL aux seules
architectures à mémoire partagée.
D’autres projets, comme Thrust [202], s’inspirent de la STL et ciblent directement les GPU
NVIDIA. SkePU [203] va plus loin et propose une solution multicible personnalisable : outre les
algorithmes déjà proposés, l’utilisateur peut en définir de nouveaux qui pourront être exécutés
sur CPU, sur GPU avec CUDA, ou sur une cible OpenCL 32 . Pour parvenir à ce résultat, SkePU
combine l’utilisation des macros et des templates afin de générer les différentes implémenta-
tions. La principale limite de ces approches est le manque de support pour les structures de
données complexes et pour les fonctions définies par l’utilisateur. La limitation aux structures
de données unidimensionnelle équivalentes aux std::vector, permet de s’affranchir du besoin
de réordonnancement exposé dans la section 2.4.
Enfin, certains travaux se concentrent sur l’abstraction du matériel au service d’un outil de
plus haut niveau. Ainsi, le module TPetra de Trilinos [81] s’appuie sur une couche dédiée à la
parallélisation sur CPU à l’aide d’INTEL TBB ou sur GPU avec CUDA [82]. Contrairement aux
travaux précédents, cet outil est extrêmement restreint. Afin de répondre aux besoins de paralléli-
sation de TPetra, seuls deux constructions sont nécessaires : parallel_for et parallel_reduce.
Ces constructions sont également prises en charge dans la couche de parallélisation de la biblio-
thèque HONEI [204].
76
3.3. Différentes approches pour permettre le développement d’applications parallèles multicibles
Une première phase de développement séquentiel est effectuée par un spécialiste du domaine ap-
plicatif. Dans un second temps, un spécialiste du parallélisme peut mettre en place des patrons
parallèles par simple annotation des sources. Notons que cette famille d’approche n’est perti-
nente que pour les cas où la parallélisation de l’application ne nécessite pas des modifications
algorithmiques importantes.
L’outil s’appuyant sur des annotations de code source le plus connu est OpenMP [59]. Défini
pour le langage FORTRAN en 1997 et pour les langages C et C++ en 1998, il permet à l’u-
tilisateur de paralléliser des nids de boucles ou des sections complètes de code. Si OpenMP est
bien adapté aux cas simples comme celui présenté au chapitre 2, différentes limitations de ce
standard font que son adoption n’a pas été aussi large qu’attendue :
– la parallélisation d’un algorithme non restreint à des boucles parallèles peut être com-
pliquée et introduire de lourds changements algorithmiques (ex : tri [205]) ;
– si OpenMP offre la portabilité du parallélisme sur différentes plates-formes à mémoire
partagée, il ne permet de cibler efficacement ni les architectures à mémoire distribuée,
malgré un certain nombre de tentatives [206, 207, 208, 209, 210], ni les accélérateurs de
calcul, même si des travaux en cours tentent de combler ce vide [211, 212, 213] ;
– pour les architectures à mémoire distribuée, la nécessité d’utiliser MPI et donc d’exprimer
deux niveaux de parallélisme a découragé une partie des utilisateurs qui ont préféré utiliser
uniquement MPI. Ce choix est d’autant plus légitime qu’OpenMP ne permet pas toujours
d’obtenir des performances meilleures que MPI sur une machine multicœur [214, 215].
OpenHMPP (HMPP pour Hybrid Multicore Parallel Programming) [216], généralisation
d’OpenMP aux architecture hybrides, est développé depuis 2007 par CAPS entreprise. Ici, les
boucles sont généralisées aux « codelets » : des portions de code dont les données d’entrée et
de sortie sont bien identifiées. Une fois les codelets extraits, OpenHMPP permet d’exécuter ces
derniers sur une architecture matérielle donnée (par exemple un GPU) après avoir automa-
tiquement effectué les transferts de données requis. Afin de permettre l’optimisation du code,
OpenHMPP incorpore un certain nombre de directives permettant de guider la génération de
code pour l’architecture cible et permet en outre la modification du code généré. Cependant,
le manque de standardisation et donc de visibilité concernant sa pérennité, ne favorise pour
l’instant pas l’adoption d’OpenHMPP. Un processus de standardisation est alors initié [217]
et conduit en novembre 2011 à l’émergence du standard OpenACC [218]. Des travaux sont
actuellement en cours afin d’unifier ce standard avec OpenMP.
Au final, les solutions par annotation de sources se veulent peu intrusives. Cependant, cela
n’est possible qu’au détriment d’une élévation du niveau d’abstraction pour le réordonnancement
des données. En effet, ne pas s’introduire dans le code existant ne permet pas de s’affranchir
du pointeur C qui correspond à la fois aux adresses mémoire et aux structures de données. Dès
lors, il est impossible de masquer à l’utilisateur des modifications dans le stockage des données
en mémoire.
77
Chapitre 3. État de l’art des environnements de développement parallèle multicible
fonctionnels comme Haskell où l’analyse est pourtant bien simplifiée grâce à l’absence d’effets
de bord [219, 220, 221].
Ainsi, NESL [222, 223] est un langage de programmation fonctionnel conçu en 1993 pour
traiter les opérations en parallèle sur des listes. Le principe consiste à fournir à NESL une fonction
à appliquer sur tous les éléments d’une liste. NESL supporte les structures multidimensionnelles
et plusieurs niveaux de parallélisme imbriqués. NESL permet à l’utilisateur de ne pas se soucier
de la manière dont les opérations sont effectuées, mais simplement de déclarer quelles sont les
opérations souhaitées. De ce fait, NESL permet une parallélisation implicite, y compris sur
des unités de calcul SIMD. NESL adapte de fait les structures de données pour permettre une
vectorisation efficace. Cependant, NESL n’adapte pas le stockage en fonction de l’architecture :
seul le stockage vectoriel (correspondant à un entrelacement complet dans notre exemple du
chapitre 2) est possible.
Héritier de NESL, Data Parallel Haskell (DPH) [224, 219] intègre depuis 2007 les fonction-
nalités de NESL au langage Haskell sous forme de DSEL. Accelerate [225] est un autre DSEL de
programmation par tableaux en Haskell dont le but est d’ajouter le support des accélérateurs de
calcul ; aujourd’hui, seules les cartes graphiques de NVIDIA sont ciblées au travers de CUDA.
Outre les langages fonctionnels, NESL a également eu des héritiers dans les langages im-
pératifs : Brook [226, 227] et RapidMind [228, 229] sont deux exemples d’intégration de telles
fonctionnalités dans des extensions du langage C.
Entre 2002 et 2007, les GPUs n’étaient pas programmables au sens actuel du terme. Cepen-
dant, il était possible en passant par des fonctionnalités de traitement graphique d’effectuer un
certain nombre d’opérations de calcul. Brook [226, 227] fut mis au point à l’université de Virginie
entre 2002 et 2007 afin de permettre l’utilisation des cartes graphiques en évitant cette forme de
programmation extrêmement compliquée. Brook était une extension de C permettant de prendre
en compte le parallélisme de donnée. Brook ciblait le GPU en s’appuyant sur les bibliothèques
graphiques DirectX9 et OpenGL. Brook a été peu utilisé en comparaison avec CUDA car son
modèle de programmation était plus contraint. Brook a fini par disparaître avec l’annonce de la
sortie d’OpenCL.
RapidMind [228, 229] est une évolution commerciale de Sh [230]. RapidMind permet à l’util-
isateur de cibler les processeurs multicœurs, les GPU [229] et le processeur Cell [228]. RapidMind
est une extension du C basée sur un mécanisme de macros et de templates C++ : le compilateur
C++ est donc utilisé pour compiler le « C étendu » défini par RapidMind. Afin de pouvoir mas-
quer le stockage des données à l’utilisateur, RapidMind introduit de nouveaux types de valeurs
et de tableaux dont les données ne sont accessibles qu’à travers l’interface fournie. Le principe
est relativement proche de celui des pViews de STAPL puisque l’utilisateur n’a aucun moyen de
savoir comment les données sont effectivement stockées. Afin d’être parallélisées, les fonctions
devant s’appliquer à ces nouveaux types de données subissent un certain nombre de contraintes.
La plus notable de ces contraintes est l’absence d’effets de bords puisque les données qui ne sont
pas définies comme valeur de retour ne peuvent être modifiées et que les données non déclarées
localement ne sont pas accessibles. Ceci permet entre autre l’utilisation de la compilation à la
volée par l’environnement de développement afin de s’adapter à l’architecture matérielle. Au
final, si RapidMind arrive à apporter les fonctionnalités de NESL dans un langage impératif,
c’est en utilisant un modèle de programmation proche de la programmation fonctionnelle.
Relativement proche de RapidMind sur les principes, INTEL Ct [231] se voulait être une
plate-forme d’essai d’INTEL pour mettre au point un environnement de programmation per-
mettant de cibler aussi bien ses processeurs que ses cartes graphiques Larabee [232]. Suite au
rachat de RapidMind par INTEL en 2009, les deux projets ont fusionné pour donner naissance
à INTEL Array Building Blocks (ArBB) [233]. ArBB se compose de quatre éléments :
78
3.3. Différentes approches pour permettre le développement d’applications parallèles multicibles
3.3.5 Synthèse
Le tableau 3.1 liste l’ensemble des environnements cités dans ce chapitre en rappelant leur
situation par rapport aux critères sélectionnés en section 3.2.
Parmi tous ceux que nous avons étudié, seuls les outils basés sur la programmation par
tableau proposent un niveau d’abstraction de parallélisme élevé. De surcroît, cette approche
a l’avantage de permettre l’expression de la localité des données : en deux dimensions avec la
convention C, les éléments x[0][0] et x[0][1] sont plus proches que les éléments x[0][0]
33. http://software.intel.com/en-us/forums/showthread.php?t=105738&o=a&s=lr
79
Chapitre 3. État de l’art des environnements de développement parallèle multicible
STAPL partiel
non
INTEL TBB non
patrons TPetra partiel
de HONEI bibliothèque moyen non
conception Thrust oui non
CuPP non
SkePU non
OpenMP annot. de code non non
annotations moyen
HMPP annot. de code oui non
NESL langage théorique
DPH ext. de lang. non non
INTEL Ct ext. de lang. non
tableaux INTEL ArBB bibliothèque élevé initialement prévu théorique
accelerate bibliothèque non
RapidMind bibliothèque non théorique
Brook ext. de lang. non
CUDA C ext. de lang. faible non non
autres
OpenCL ext. de lang. faible non non
80
3.4. Choix stratégique : positionnement de MTPS
et x[1][0]. Lorsque le stockage des données est pris en charge par l’outil (c’est le cas pour
Nesl, ArBB et RapidMind), il est potentiellement possible d’adapter le format de stockage des
données.
L’approche qui nous semble la plus pertinente pour utiliser les accélérateurs matériels est l’ap-
proche par patrons de conception. Cette famille d’approches unifie l’expression du parallélisme
et adapte le code généré pour les différentes architectures matérielles. D’autre part, l’exemple
de STAPL montre qu’il est alors possible de bénéficier des avantages de la programmation par
tableaux. En effet, afin de pouvoir prendre en charge l’adaptation des structures de données,
STAPL s’appuie sur la notion de pview. C’est l’utilisation de ce concept qui permet à ArBB de
prendre en charge le format de stockage des données.
81
Chapitre 3. État de l’art des environnements de développement parallèle multicible
82
Un langage de programmation est censé être une
façon conventionnelle de donner des ordres à un
ordinateur. Il n’est pas censé être obscur,
bizarre et plein de pièges subtils (ça ce sont les
attributs de la magie).
David Small (1958 – présent)
Concepteur de Spectre GCR, un émulateur
MAC pour atari ST.
Chapitre 4
Dans le premier chapitre, nous avons montré l’importance de la mutualisation de code entre
les applications ou bibliothèques. Afin de mutualiser les travaux d’optimisation de plusieurs
solveurs d’algèbre linéaire, une première version de Legolas++ a été mise au point. Nous
souhaitons étendre Legolas++ afin d’utiliser les architectures matérielles disponibles actuelle-
ment. Pour mutualiser le développement des fonctionnalités et des algorithmes de Legolas++,
nous souhaitons mettre au point une couche logicielle chargée d’abstraire l’architecture matérielle
et sur laquelle pourra s’appuyer une version multicible de Legolas++.
Dans le second chapitre, nous avons présenté les architectures matérielles que nous voulons
utiliser. Nous avons ensuite introduit les optimisations adaptées à ces architectures. Nous avons
alors identifié deux verrous à lever pour mettre au point une application multicible exploitant
efficacement ces architectures. Le premier verrou concerne l’hétérogénéité de l’expression du
parallélisme. En effet, les différentes plate-formes matérielles dédiées au calcul scientifiques sont
livrées avec des outils de parallélisation correspondant à des paradigmes généralement incom-
patibles comme CUDA et les fonctions intrinsèques SSE. La mise au point d’un code capable
de s’adapter à ces paradigmes suppose donc d’abstraire l’expression du parallélisme. Cette ab-
straction devra être composée de constructions dont une implémentation peut être fournie pour
chaque architecture. Le second verrou concerne l’expression des structures de données. Nous
avons en effet montré que le format de stockage des données influe fortement sur les perfor-
mances de l’application et peut même interdire l’utilisation de certaines unités de calcul.
Dans le chapitre 3, nous avons comparé plusieurs approches permettant de mettre au point
des applications parallèles et portables sur différentes architectures matérielles. Si la majorité
des approches étudiées permettent de lever le premier verrou, à ce jour, aucune n’adapte la
structure de données selon l’architecture cible.
Nous présentons donc dans ce chapitre notre conception de MTPS [241, 242], une bib-
liothèque prenant en charge l’adaptation des structures de données bidimensionnelles et du
parallélisme pour les CPUs X86_64 et pour les GPUs compatibles avec CUDA.
À l’issue de cette présentation, nous analyserons les diverses contraintes d’utilisation de
MTPS. Dans le prochain chapitre, nous présenterons comment l’implémentation de Legolas++
a été étendue pour respecter une partie de ces contraintes.
83
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
Sommaire
4.1 Modèle de programmation : introduction aux contextes vectoriels . 85
4.1.1 Modèle d’architecture matérielle . . . . . . . . . . . . . . . . . . . . . . 85
4.1.2 Rappel du cas d’application : résolution du système AX = B . . . . . . 85
4.1.3 Introduction au modèle de données . . . . . . . . . . . . . . . . . . . . . 86
4.1.4 Un parallélisme restreint aux opérations vectorisables . . . . . . . . 88
4.1.4.1 Les collections . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.1.4.2 Le squelette parallèle vectorized_for . . . . . . . . . . . . . 90
4.1.4.3 Le squelette parallèle vectorized_reduce . . . . . . . . . . . 91
4.1.5 Extension de la structure de données . . . . . . . . . . . . . . . . . . . . 93
4.1.6 Les contextes vectoriels . . . . . . . . . . . . . . . . . . . . . . . . 93
4.2 Principes de fonctionnement de MTPS . . . . . . . . . . . . . . . . . 96
4.2.1 Choix technologiques d’implémentation . . . . . . . . . . . . . . . . . . 96
4.2.2 Définition et instanciation de la structure de données . . . . . . . . . . . 96
4.2.3 Abstraction du parallélisme et accès aux données . . . . . . . . . . . . . 97
4.2.4 Vue d’ensemble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.3 Contraintes d’utilisation et architectures matérielles . . . . . . . . . 98
84
4.1. Modèle de programmation : introduction aux contextes vectoriels
85
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
Figure 4.1 : Matrice diagonale à blocs bandes et symétriques. Cette matrice contient 4 blocs
tridiagonaux de taille 8
A : une matrice diagonale par blocs avec des blocs symétriques et définis positifs
Ad : La forme factorisée de la matrice A
B : le vecteur membre de droite
X : le vecteur inconnu, initialisé pour être égal à B
86
4.1. Modèle de programmation : introduction aux contextes vectoriels
Definition 1. Un vecteur définit une bijection entre un ensemble d’éléments distincts de même
type et un intervalle J0; nKn∈N .
Soit V l’ensemble des classes définissant des vecteurs . Soit V<T> ∈ V une classe de vecteur
dont les éléments sont de type T. Soit vT une instance de la classe V<T>. Nous noterons vT [i]
le iè élément de vT et vT .size() le nombre d’éléments. Par exemple, un bloc Aii de matrice ou
Xi de vecteur peut stocker ses données dans des V<float>. Le nombre d’éléments peut varier
entre deux instances vT1 et vT2 de la même classe V<T>. Nous dirons que le nombre d’éléments
d’un vecteur est défini dynamiquement.
Soit Sn l’ensemble des classes définissant des n-séquences . Soit S<T> ∈ Sn une classe de
n-séquences dont les éléments sont de type T.
Soit Wn l’ensemble des classes dont les membres données peuvent être implémentés comme
une unique n-séquence de vecteurs de flottants en simple précision (float). Prenons par
exemple W, une classe élément de Wn :
struct W {
V < float > fields_ [ n ];
};
87
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
A
A0 A1 A2 A3
A0 .fields_[0] A1 .fields_[0] A2 .fields_[0] A3 .fields_[0]
X
X0 X1 X2 X3
X0 .fields_[0] X1 .fields_[0] X2 .fields_[0] X3 .fields_[0]
Figure 4.2 : structure de données de la matrice A et du vecteur X sous formes de classes appartenant
à V<Wn > telles que définies dans le tableau 4.1.
Definition 3. Le profil d’une classe W est l’ensemble des données permettant de calculer le
nombre d’éléments de chacun des vecteurs . La seconde colonne du tableau 4.1 expose pour
chaque exemple les données définissant le profil .
Soit V<Wn > l’ensemble des classes de vecteurs dont les éléments sont des instances de
classes de Wn . Par exemple, les objets mathématiques A et X peuvent être implémentés à l’aide
de classes appartenant à V<Wn >.
La figure 4.2 montre comment ces concepts se traduisent dans l’exemple que nous avons in-
troduit. Les rectangles jaunes représentent des vecteurs de flottants simple précision. Les rect-
angles verts représentent des n-séquences de vecteurs . Enfin, les rectangles bleus représentent
des vecteurs de n-séquences .
Les deux rectangles bleus représentent A et X. Les rectangles verts représentent les blocs
Aii et Xi . La matrice A représentée par le rectangle bleu du haut est un vecteur contenant
quatre blocs Aii représentés par quatre rectangles verts. Chaque bloc Aii est une 1-séquence
contenant un vecteur de float représenté par un rectangles jaunes.
88
4.1. Modèle de programmation : introduction aux contextes vectoriels
A
A0 A1 A2 A3
A0 .fields_[0] A1 .fields_[0] A2 .fields_[0] A3 .fields_[0]
X
X0 X1 X2 X3
X0 .fields_[0] X1 .fields_[0] X2 .fields_[0] X3 .fields_[0]
La figure 4.3 montre comment la matrice A et le vecteur X sont transformés pour construire
quatre 2-tuples (représentés en roses). Ces 2-tuples forment les arguments de la fonction
descente_remontee_bloc. Chacun de ces 2-tuples contient deux vecteurs (représentés en
jaune) issus respectivement de A et X. Ces quatre 2-tuples sont rassemblés au sein d’un
vecteur représentée en bleu.
Definition 5. Une collection cf est un vecteur instance d’une classe appartenant à V<Wn >
et dont tous les éléments sont indépendants vis à vis d’une fonction f , c’est-à-dire que l’ensemble
des résultats de la fonction f appliquée aux éléments de la collection cf ne dépend pas de
l’ordre dans lequel le vecteur cf est parcouru.
Le fait qu’un vecteur soit ou non une collection dépend donc à la fois de la
représentation des données et de la fonction qui lui sera appliquée. Considérons par
exemple le cas d’une matrice dense. Elle peut être considérée comme un vecteur de lignes ou
comme un vecteur de colonnes. Supposons que la fonction f que l’on souhaite appliquer à cette
matrice consiste à calculer, pour chaque ligne, la somme de tous les éléments. Dans ce cas, la
matrice, vue comme un vecteur de lignes est une collection vis à vis de f : pour chaque
ligne, la somme peut être effectuée indépendamment des autres lignes. Si en revanche la matrice
est vue comme un vecteur de colonnes, la matrice n’est pas une collection vis à vis de f .
En bas de la figure 4.3, le vecteur bleu représente une collection de 2-tuples vis à vis
de la fonction descente_remontee_bloc. Nous nommerons cdrb cette collection dans la suite de
ce chapitre.
Definition 6. Deux éléments d’une collection sont dits semblables lorsqu’ils possèdent le
même profil . Nous définissons une collection homogène comme une collection dont tous
les éléments sont semblables .
La matrice A comporte des blocs ayant exactement la même structure et la même taille ; elle
définit donc une collection homogène de blocs Aii . Tous les blocs de vecteur Xi ont la même
89
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
2-tuple (A, X)
fields_[0] fields_[1]
fields_[0][0]=A0 .fields_[0] fields_[1][0]=X0 .fields_[0]
taille et sont donc naturellement semblables eux aussi. Le vecteur X définit donc également
une collection homogène . Composés d’éléments semblables , les 2-tuples (Aii , Xi ) sont
naturellement semblables . Par composition, la collection cdrb est donc homogène .
MTPS permet de transformer une collection homogène en une structure de données bidi-
mensionnelle semblable à celle présentée dans la section 2.2.2.2. Le format de stockage de ce type
de structure peut facilement être adapté pour exploiter efficacement différentes architectures.
La figure 4.4 illustre cette transformation : les rectangles roses représentent des 2-tuples , les
rectangles oranges représentent des structures de données bidimensionnelles (telles que définies
section 2.2.2.2) et les rectangles jaunes représentent un vecteur appartenant à un bloc. À chaque
ligne de la structure bidimensionnelle correspond un indice de bloc différent. MTPS adaptera
automatiquement l’entrelacement des données appartenant à ces différents blocs en fonction de
l’architecture choisie comme présenté dans la section 2.2.2.2. Un mécanisme de vues compa-
rables aux pViews de STAPL [201] permet à l’utilisateur de manipuler ces différentes données
indépendamment du format de stockage sélectionné.
Nous allons maintenant voir comment la notion de collection permet de définir des opéra-
tions parallélisables sur les différentes plate-formes matérielles. Les deux prochaines sections in-
troduiront les contraintes permettant la parallélisation efficace de ces opérations et proposeront
deux squelettes parallèles permettant de les exprimer : vectorized_for et vectorized_reduce.
F : (Wn )NElts 7→ ∅
F (cf ) ≡ f (cf [i]) ∀i ∈ [0, NElts − 1]
Soit F l’ensemble des fonctions F qui appliquent une fonction f à tous les éléments d’une
collection cf .
90
4.1. Modèle de programmation : introduction aux contextes vectoriels
Definition 7. Une fonction non-divergente est une fonction ne comportant pas de tests sur
la valeur de ses arguments. La suite d’instruction générée par une fonction non-divergente est
donc unique.
Definition 8. Une fonction F appliquant une fonction non-divergente f à tous les éléments
d’une collection cf est dite régulière .
Definition 9. Une opération vectorisable est une opération pouvant être décrite comme
l’application d’une fonction régulière F à une collection homogène cf . MTPS fournit le
squelette vectorized_for afin d’exécuter cette opération en tirant profit des unités SIMT ou
SIMD présentes dans les processeurs :
F (cf ) ≡ vectorized_for(f, cf ).
L’Alg. 2.2 (page 54) présente l’opération de descente remontée implémentée par la fonc-
tion descente_remontee_bloc. Cet algorithme ne comporte pas de tests sur les valeurs des
éléments des blocs. La fonction descente_remontee_bloc peut donc être implémentée de façon
non-divergente .
Résoudre le problème Ad X = X à l’aide d’une descente-remontée revient donc à appliquer
une fonction regulière sur la collection homogène de 2-tuples cdrb . Ce résultat ne doit
pas nous surprendre : nous avons déjà présenté une implémentation vectorisée de cette opération
dans le chapitre 2.
Dans cette section, nous allons introduire les éléments nécessaires pour définir une réduction.
Après avoir présenté formellement cette famille d’opérations, nous appliquerons les concepts
introduits sur l’exemple de la norme d’un vecteur.
L’opération prise en charge par le squelette vectorized_reduce est un cas particulier de
l’opérateur fold 34 [243] disponible dans la majorité des langages fonctionnels. Dans le cas général,
34. L’opérateur fold correspond aux opérations implémentables à l’aide de MapReduce [192]
91
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
Le premier argument, noté ffold ci-dessus est la fonction binaire de réduction. Le second
argument v représente la valeur initiale de la réduction. Enfin, le troisième argument, que nous
noterons l, correspond à une liste d’éléments à réduire.
La parallélisation de l’opérateur fold sur des unités SIMD ou SIMT impose un certain nombre
de contraintes sur ffold , v et l :
• ffold doit être décomposée en deux fonctions non divergentes fm et fr telles que :
fm : α 7→ β
fr : (β, β) 7→ β
ffold (a, b) = fr (fm (a), b)
• add :
add : (floatpositif , floatpositif ) 7→ floatpositif
add(a, b) = a+b
92
4.1. Modèle de programmation : introduction aux contextes vectoriels
93
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
Thread
Entrée dans MTPS
Définition d’un premier contexte
x
Contextes
Changement vectoriels
de contexte MTPS
MTPS y
Sortie de MTPS
Retour au contexte séquentiel
Temps
de manière vectorisable . Pour ce faire, la fonction de résolution d’une ligne doit prendre en
argument (entre autres) un vecteur de DDLs de courant et un vecteur de DDLs de flux. Nous
représentons ainsi l’ensemble des DDLs de flux par un vecteur de vecteurs . Le premier niveau
contient les lignes de DDLs. Chaque ligne étant à son tour représentée par un vecteurs de float.
Autrement dit, l’ensemble des DDLs de flux est représenté par un vecteur 2D correspondant à
un stockage ligne par ligne.
Lorsque l’on traite le problème suivant y, les mêmes causes menant aux mêmes conclusions,
l’ensemble des DDLs de flux est représenté par un vecteur 2D correspondant à un stockage
colonne par colonne.
Ces deux opérations sont donc vectorisables , mais dans des contextes vectoriels dif-
férents. Entre ces deux opérations, un changement de contexte doit donc être effectué. Dans ce
cas, ce changement de contexte consiste à réordonnancer le vecteur de DDLs de flux représenté
par les points verts. Ce changement de contexte est également l’occasion de redéfinir le nombre
de tâches indépendantes présentes à ce stade de l’exécution (4 suivant x puis 6 suivant y). La
figure 4.6 illustre les différentes étapes de la résolution du problème représenté sur la figure 4.5
en utilisant MTPS :
1. Au départ, seul le thread principal du programme s’exécute et permet par exemple d’ini-
tialiser les données.
2. Un premier contexte MTPS est créé pour traiter la direction x. Le stockage des différentes
collections est adapté selon l’architecture ciblée.
3. Chaque thread résout un problème correspondant à une colonne de DDLs.
4. Avant de pouvoir traiter la direction y, un changement de contexte doit avoir lieu. À
cette occasion, les données sont réordonnancées et l’environnement d’exécution est recon-
figuré.
5. Chaque thread résout un problème correspondant à une ligne de DDLs.
6. Enfin, l’exécution terminée, le programme sort de MTPS et revient dans un contexte
séquentiel.
95
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
96
4.2. Principes de fonctionnement de MTPS
collection
format de stockage optimisé
tailles des vecteurs
Figure 4.7 : L’implémentation du concept de collection s’appuie sur la description des éléments
fournie par l’utilisateur et sur les fonctions d’entrelacement fournies par la classe décrivant
l’architecture matérielle afin de générer une structure de données optimisée.
Figure 4.8 : MTPS fournit des vues permettant d’accéder aux différents éléments d’une collection
dont le stockage est optimisé.
36. seules les unités vectorielles SSE sont supportées dans la version de MTPS de la fin 2011
97
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
Squelette parallèle
collection optimisée
indice d’un ou plusieurs éléments
Figure 4.9 : Les opérateurs parallèles utilisent des vues pour appliquer les fonctions définies par
l’utilisateur aux différents éléments de la collection.
collection
format de stockage optimisé
tailles des vecteurs
collection optimisée
Squelette parallèle
98
4.3. Contraintes d’utilisation et architectures matérielles
qu’il est aisé de copier d’un espace mémoire vers un autre. En pratique, MTPS alloue un espace
mémoire par tableau et manipule n pointeurs vers ces espaces mémoire.
Le support efficace des unités SIMT est rendu possible grâce à deux choses. Première-
ment, les éléments d’une collection doivent être semblables . Deuxièmement, les fonctions
appliquées doivent être non-divergentes . La première contrainte permet d’adapter le format
de stockage des données tandis que la seconde permet de vectoriser les opérations en appliquant,
en même temps, la même fonction à différents éléments de la collection à l’aide des unités
SIMT. Le respect de ces deux contraintes permet donc l’obtention de bonnes performances.
Le support des unités SIMD implique le respect des contraintes permettant l’utilisation
des unités SIMT. À ces contraintes s’ajoutent le fait que tous les types de données élémentaires
doivent avoir la même taille. Cela permet de définir des vues qui manipulent différents objets
simultanément.
Le respect de ces contraintes permet d’automatiser la parallélisation et la vectorisation d’un
même code source pour différentes architectures matérielles. Nous verrons dans le chapitre 6
que les performances alors observées sont comparables aux performances obtenues pour des
implémentations optimisées « à la main ».
Nous avons conçu MTPS comme une couche de bas niveau destinée à être utilisée pour
mettre au point des outils de plus haut niveau. Dans le prochain chapitre, nous introduirons
Legolas++, une bibliothèque C++ dédiée à la résolution de système linéaires mettant en oeuvre
des matrices creuses structurées. Nous verrons alors comment Legolas++ se positionne vis à vis
de ces contraintes. Nous présenterons également un démonstrateur de Legolas++ capable de
paralléliser et de vectoriser automatiquement un solveur sur les processeur X86_64.
99
Chapitre 4. MTPS : MultiTarget Parallel Skeleton
100
Il faut savoir ce que l’on veut. Quand on le
sait, il faut avoir le courage de le dire ; quand on
le dit, il faut avoir le courage de le faire.
Georges Clémenceau (1841 – 1929)
Président du conseil
Chapitre 5
101
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
Sommaire
5.1 Legolas++ : présentation de l’existant . . . . . . . . . . . . . . . . . . 103
5.1.1 Les vecteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.1.2 Les matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.1.2.1 Définition d’une matrice . . . . . . . . . . . . . . . . . . . . . . 104
5.1.2.2 création et manipulation d’une matrice . . . . . . . . . . . . . 104
5.1.2.3 Les options de configuration des matrices . . . . . . . . . . . . 105
5.1.3 Les solveurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.2 Contraintes pour une version multicible de Legolas++ . . . . . . . . 107
5.2.1 L’expérience MTPS : rappel des contraintes pour un code multicible . . 107
5.2.1.1 Processeur X86_64 . . . . . . . . . . . . . . . . . . . . . . . . 108
5.2.1.2 Processeur GF100 . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.2.2 Famille de problèmes compatibles avec une implémentation multicible . 108
5.3 Un démonstrateur de Legolas++ multicible . . . . . . . . . . . . . . 109
5.3.1 Structures de données Legolas++ et vues parallèles . . . . . . . . . . . 110
5.3.1.1 Les vecteurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.3.1.2 Les matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.3.1.3 Les solveurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
5.3.2 Un démonstrateur capable de cibler les différentes générations de pro-
cesseurs X86_64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
5.3.3 Utilisation et limitations du démonstrateur . . . . . . . . . . . . . . . . 116
102
5.1. Legolas++ : présentation de l’existant
Pour créer un vecteur, un utilisateur doit seulement fournir sa shape en sus de son type.
Le type permettant de définir la shape d’un vecteur dépend naturellement du type de vecteur
considéré. Ce type est fournit par la classe Legolas::MultiVector sous le nom Shape. L’extrait
de code suivant construit un vecteur à deux niveaux comportant n1 vecteurs unidimensionnels
de taille n2 :
Vector2D :: Shape vectorShape ( n1 , n2 ) ;
Vector2D X ( vectorShape ) ;
Dans Legolas++, l’implémentation des vecteurs s’appuie sur le mécanisme d’expression tem-
plates présenté dans l’annexe A.2. Cette implémentation est en outre parallélisée à l’aide de la
bibliothèque INTEL TBB [139] et conduit à l’obtention de performances équivalentes à celles
obtenues avec les meilleures implémentations présentées à la section 1.2.2 :
Vector2D Y ( vectorShape ) , Z ( vectorShape ) ;
float a , b , c ;
...
103
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
Vector2D V = a * X + b * Y + c * Z ;
104
5.1. Legolas++ : présentation de l’existant
une matrice. En particulier, la taille d’une matrice n’est généralement pas connue lors de l’écrit-
ure de sa définition : elle est souvent calculée à l’exécution à partir d’autres données. Nous allons
ici montrer comment créer une matrice Legolas++ représentant M à partir d’une définition de
matrice MDefinition 39 .
Pour cela, la première étape consiste à construire une classe de matrice correspondant à cette
définition :
typedef Legolas :: GenericMatrixInterface < MDefinition >:: Matrix Matrix ;
La matrice Legolas++ m peut maintenant être utilisée conjointement avec les vecteurs Lego-
las++ :
m . getElement (i , j ) ; // acces generique ( inefficace ) a un element de m
m . l o w e r Ban dedG etEle ment (i , j ) ; // acces efficace a un element de m
105
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
Ces options peuvent ensuite être passées à Legolas++ pour obtenir une classe de matrice
disposant des fonctionnalités correspondantes :
typedef Legolas :: GenericMatrixInterface < MDefinition , MOptions >:: Matrix Matrix ;
L’usage reste inchangé par rapport à une matrice utilisant une configuration par défaut. Il est
ainsi possible de mettre au point une version de référence de l’application avant de l’optimiser
en spécifiant les options des matrices.
106
5.2. Contraintes pour une version multicible de Legolas++
itératifs aux niveaux supérieurs conduit cependant à multiplier les appels aux algorithmes utilisés
pour résoudre les problèmes des niveaux inférieurs. Par exemple, l’algorithme de résolution utilisé
par défaut dans Legolas++ est l’algorithme itératif dit de Gauss-Seidel [96, 97] que nous avons
présenté page 27. À chaque itération, cet algorithme conduit à résoudre les systèmes linéaires
correspondants aux blocs diagonaux de la matrice, et il est important de préserver les résultats
partiels qui peuvent être réutilisés d’une itération sur l’autre. Par exemple, dans le solveur de
neutronique SPN implémenté à l’aide de Legolas++ [33], le premier niveau de la matrice est
résolu à l’aide d’une factorisation des blocs se trouvant sur la diagonale. Cette factorisation
étant très coûteuse, il est nécessaire pour obtenir de bonnes performances de ne pas refaire cette
opération à chaque itération.
Les solveurs Legolas++ ont pour objectif de répondre à cette problématique en préservant
les états internes des algorithmes entre leurs différentes exécutions. Les solveurs suivent la même
construction hiérarchique que les matrices : chaque solveur correspond à un bloc de la matrice et
contient d’une part les données nécessaires à son algorithme et d’autre part les sous-solveurs cor-
respondants aux niveaux inférieurs de la matrice. Ces sous-solveurs utilisent l’algorithme défini
dans les options des sous-matrices. De cette manière, il est possible de construire récursivement
un algorithme de résolution complexe, capable d’exploiter de manière optimale la structure d’une
matrice. Il est également possible de changer simplement l’algorithme utilisé à un niveau sans
toucher aux autres niveaux ; ce qui peut être extrêmement compliqué avec d’autres approches.
L’annexe C.2 présente l’implémentation de la classe SymmetricBandedGaussSeidelAlgorithm,
un exemple d’algorithme Legolas++.
107
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
du format de stockage pour répondre aux particularité des architectures matérielles ciblées. Nous
rappelons dans cette section les contraintes à respecter pour exploiter efficacement les capacités
des processeurs X86_64 (processeurs fabriqués par INTEL, AMD ou VIA) et des processeurs
GF100 (NVIDIA). Ces contraintes sont issues du modèle de programmation de MTPS.
Ces processeurs disposent d’unités SIMD (SSE ou AVX). Leur utilisation automatique impose
l’utilisation d’algorithmes vectorisables qui appliquent une fonction non-divergente à une
collection homogène d’éléments. MTPS fournit des vues parallèles sur les éléments d’une
collection homogène . Ces vues parallèles permettent de manipuler plusieurs éléments si-
multanément et de manière transparente. Elles reposent sur une surcharge des opérateurs pour
les types SIMD proposés par les compilateurs (cf. le type SSEData, section 2.2.2.3). Afin de per-
mettre l’utilisation des unités SIMD, il faut que les données manipulées soient de taille identique.
Ainsi MTPS ne prend-elle pas en charge la vectorisation d’algorithmes mettant en œuvre à la
fois des données de type float et des données de type double.
À la différence des processeurs X86_64, les processeurs GF100 ne disposent pas d’unités
SIMD mais d’unités SIMT. L’obtention de bonnes performances sur ces dernières nécessitent
également un réordonnancement des données et l’utilisation d’algorithmes vectorisables . En
revanche, elles n’imposent pas que les types de données aient la même taille. D’autre part, ces
processeurs sont généralement disponibles sous forme de cartes périphériques disposant de leurs
propres modules de mémoire RAM. L’utilisation de ces processeurs implique alors de pouvoir
copier efficacement les données d’un espace mémoire vers un autre espace mémoire. La latence
observée lors de copies entre ces espaces mémoires est importante. De ce fait, les données doivent
être sérialisées dans un unique espace mémoire contigu afin de limiter le temps requis par ces
copies. Enfin, les différents types de données manipulés doivent également respecter un certain
nombre de contraintes proches des contraintes définissant les POD (Plain Old Data) [74] en
C++. Nous listons une partie de ces contraintes dans la section 2.2.2.1. Les structures de données
MTPS respectent ces contraintes.
108
5.3. Un démonstrateur de Legolas++ multicible
pas apparaître de couplage entre les blocs comme l’illustre l’exemple suivant où A,B,C,D et E
sont cinq matrices inversibles :
−1
C −1
A
B A−1
C = D−1 .
D
E −1
E B −1
Donc, l’ensemble de ces problèmes peut être écrit de sorte à mettre en oeuvre des matrices
diagonales par blocs : il suffit pour cela de changer l’ordre des matrices et des vecteurs de manière
appropriée.
Or, nous avons vu dans le chapitre précédent que pour qu’une fonction ne soit pas divergente
vis à vis d’un ensemble d’éléments, il faut que le parcours des données soit le même pour chacun
de ces éléments. Appliqué à notre problème, cela implique :
– que la structure des différents blocs soit exactement la même (même type de structure et
même shape ),
– que la suite d’instructions exécutée par les différents algorithmes impliqués ne dépende
que de la structure de la matrice.
Supposons que toutes les matrices Legolas++ puissent être sérialisées 41 . Lorsque les dif-
férents blocs d’une matrice possèdent exactement la même structure, cette matrice peut être
considérée comme une collection homogène vis à vis des algorithmes permettant de résoudre
un sous-problème correspondant à un élément de la diagonale.
Au final, notons E l’ensemble des problèmes AX = B vérifiant les propriétés suivantes :
– A est une matrice diagonale par blocs,
– la structure de tous ces blocs est exactement la même,
– l’algorithme choisi pour résoudre les problèmes-blocs Aii Xi = Bi n’est pas divergent ,
c’est à dire que la suite d’instructions correspondante à son exécution ne dépend pas des
données.
E représente l’ensemble des problèmes pour lesquels il est possible de mettre au point une
implémentation multicible en appliquant les principes étudiés au chapitre 4.
Dans la suite de ce chapitre, nous ne considérerons cependant aucune restriction sur la
structure des blocs de la diagonale. La figure 5.1 montre un exemple de matrice correspondant
à ces contraintes. Bien que cette classe de problèmes soit très réduite vis à vis de l’ensemble
des problèmes pouvant être traités avec Legolas++, elle n’en constitue pas moins une classe
de problèmes importante. Par exemple, ce type de structure apparaît à chaque fois que l’on
souhaite utiliser une méthode itérative des directions alternées [247] pour résoudre un problème
discrétisé suivant une grille cartésienne. Les problèmes résultants d’une discrétisation suivant
un maillage irréguliers peuvent toutefois également faire apparaître des structures diagonales
par bloc en utilisant un maillage à deux niveaux : le premier niveau de maillage est un maillage
irrégulier classiques, mais chacune de ses mailles est à son tour discrétisé suivant un maillage
régulier [248, 249].
109
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
Figure 5.1 : Exemple d’une matrice pouvant être représentée par une collection homogène.
moyens permettant de générer des implémentations adaptées aux différentes cibles conformément
aux principes énoncés au chapitre 4.
110
5.3. Un démonstrateur de Legolas++ multicible
Niveau
2 2
1 3 3
0 Légende
# Vecteur contenant # éléments
Pointeur « propriétaire »
Élément flottant
# Vecteur représentant # éléments
Pointeur « utilisateur »
Niveau
2 # Taille de la structure unidimensionnelle
1
3 3
0 6
Le haut de la figure 5.2 représente une implémentation non sérialisée. L’objet représentant le
vecteur de niveau deux contient un pointeur vers un tableau de vecteurs de niveau un. Chacun
de ces objets contient un pointeur vers un tableau d’éléments flottants (ces éléments sont dits
de niveau zéro dans la terminologie Legolas++).
La représentation du bas correspond à une implémentation dont les données sont sérialisées :
tous les éléments flottants sont dans un unique tableau. Pour ce faire, l’allocation de ce tableau
doit être pris en charge par le vecteur de niveau deux. L’idée est la suivante : lors de la con-
struction du vecteur bidimensionnel, le nombre d’éléments de chaque niveau est calculé à partir
du profil du vecteur. Un tableau est alors alloué pour chaque niveau. Ensuite, les différents
niveaux sont initialisés et pointent vers la portion du tableau de niveau inférieur qui leur est
allouée. Ce principe est aisément généralisable à des vecteurs de dimensions supérieures. Afin
de limiter l’empreinte mémoire, il est également possible de n’allouer que le tableau contenant
les éléments flottants et de générer à la demande les vecteurs de niveaux intermédiaires. C’est
la stratégie que nous avons suivi pour implémenter la classe Vector2D dans la section 2.2.2.
En appliquant ce principe, un vecteur à N dimensions peut être ramené à une structure
bidimensionnelle. Il suffit par exemple de sérialiser les N − 1 derniers niveaux du vecteur. Pour
que cette structure soit rectangulaire, il faut que les différents vecteurs de dimension N − 1 aient
le même profil. La figure 5.3 illustre ces concepts en prenant l’exemple d’un vecteur V de dimen-
sion 3. Sur cette figure, les couleurs sont utilisées pour identifier les données appartenant aux
différents vecteurs de niveau 1. À chaque vecteur de niveau 2 correspond une ligne de la structure
bidimensionnelle. Pour que la structure bidimensionnelle soit rectangulaire et compatible avec
les principes énoncés dans le chapitre 4, il est nécessaire que tous ces vecteurs de niveau 2 aient
le même profil . Dans l’exemple de cette figure, le profil de chacun de ces vecteurs peut être
énoncé comme suit : vecteur de dimension 2 contenant deux vecteurs, le premier contenant trois
éléments, le second en contenant quatre. En généralisant la définition présentée section 4.1.3,
nous dirons de ces vecteurs de dimension 2 qu’ils sont semblables . Soit f une fonction prenant
pour argument un vecteur de dimension 2. Le vecteur V peut alors être considéré comme une
collection homogène vis à vis de f .
111
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
Niveau
3 3
2 2 2 2
1 3 4 3 4 3 4
Niveau
3 3
2 2 2
1
3 4 3 4 3 4
Naturellement, les vecteurs de niveau deux représentant les éléments de la collection doivent
avoir le même profil .
Pour être multicible, notre implémentation de vecteurs multidimensionnels doit permettre
d’opérer simultanément sur différents éléments. Ainsi selon l’architecture choisie, l’accès aux
éléments de la collection homogène renverra une vue représentant soit un vecteur unique,
soit plusieurs vecteurs (k si l’architecture cible comporte des unités SIMD/SIMT à k voies).
112
5.3. Un démonstrateur de Legolas++ multicible
Tableau 5.1 : Exemples pour différentes structures de matrices de linéarisation des indices (i, j)
Afin de ne pas stocker les deux types de vues (parallèles ou séquentielles), nous avons choisi
de stocker des vues génériques qui sont transtypées à la demande pour être soit séquentielles,
soit parallèles. Dans l’exemple de la figure 5.3, sur une machine disposant d’unités SIMD à 3
voies, les vues sur les éléments rouges peuvent être transtypées en vues parallèles sur les
3 éléments de la collection . De ce fait, il n’est pas nécessaire de stocker à la fois les vues
séquentielles et parallèles. Le comportement par défaut de notre implémentation des vecteurs
multidimensionnels est identique à celui des vecteurs Legolas++ actuels. Les vues parallèles
sont typiquement crées par des algorithmes Legolas++ (correspondant aux squelettes parallèles
de MTPS) ou par des utilisateurs avertis.
Dans cette section, nous avons montré comment concevoir des vecteurs multidimensionnels
multicibles. Pour y parvenir, nous avons dans un premier temps conçu des vecteurs multidi-
mensionnels dont les données étaient représentées sous la forme de structures bidimensionnelles
avant de nous appuyer sur les principes de MTPS pour transformer les vecteurs Legolas++
en collections homogènes . Contrairement à MTPS qui génère les vues à la demande, nos
vecteurs stockent ces vues , ce qui permet d’assurer une plus grande compatibilité avec le reste
de la bibliothèque Legolas++.
113
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
12 0
0
12
0
8
12
4
8
12
4
8
12
Figure 5.4 : Transformation d’une matrice à deux niveaux en vecteur à deux niveaux.
114
5.3. Un démonstrateur de Legolas++ multicible
charge la sérialisation des éléments. Cependant, pour pouvoir réutiliser nos travaux sur les
vecteurs, nous avons du limiter les possibilités offertes par Legolas++ : il est impossible d’utiliser
un conteneur sérialisé à un seul niveau de la matrice. Pour pouvoir utiliser ces conteneurs, il faut
que la matrice soit définie comme « stockée à tous les niveaux et que des conteneurs sérialisés
soient utilisés à tous les niveaux.
Dans le cadre de cette thèse, nous avons implémenté un conteneur sérialisé permettant de
générer des vues parallèles : Legolas::BandedSymmetricFlatMatrixContainer est spécial-
isé pour les structures bandes symétriques.
Afin de générer des vues parallèles , il faut que la matrice comporte un niveau dont
la structure est diagonale et dont les blocs possèdent le même profil . Le conteneur linéarisé
Legolas::DiagonalFlatMatrixContainer peut alors être utilisé. Il définit un stockage optimisé
pour les matrices diagonales par blocs et assimilables à des collections homogènes de blocs.
Ce conteneur crée la structure de donnée bidimensionnelle permettant le réordonnancement des
données en fonction de l’architecture ciblée. Il est alors possible de créer des vues parallèles
permettant d’opérer de manière transparente sur plusieurs blocs de manière simultanée. Lorsque
le conteneur Legolas::DiagonalFlatMatrixContainer est défini pour plusieurs niveaux de la
matrice, la conteneur de plus haut niveau est utilisé pour la parallélisation.
Dans cette section, nous avons montré que le problème posé par la conception de matrices
multicibles est équivalent au problème posé par la conception de vecteurs multicibles. Nous
avons ainsi étendu les conteneurs parallèles des vecteurs Legolas++ afin de prendre en charge
les matrices Legolas++. Comme nous l’avons indiqué précédemment, cela a été rendu possible
en limitant l’ensemble des cas pouvant être traités (uniquement les problèmes mettant en œuvre
des matrices diagonales par blocs homogènes) et en désactivant certaines possibilités offertes par
Legolas++ (les matrices doivent être « stockées » à tous les niveaux). Malgré ces restrictions,
l’ensemble des problèmes couverts justifie le travail effectué. En effet, la résolution parallèle de
problèmes physique discrétisés suivant un maillage cartésien conduit souvent à la mise en œuvre
de matrices diagonales par blocs.
115
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
116
5.3. Un démonstrateur de Legolas++ multicible
cibler les unités SIMD (processeurs X86_64) et garantit un fonctionnement optimal des unités
SIMT (processeurs GF100). La vérification de cette propriété n’est pas prise en charge par notre
démonstrateur : c’est à l’utilisateur de s’assurer que les algorithmes ne sont pas divergents .
Lorsque l’utilisateur a vérifié que ces conditions étaient réunies, il peut utiliser les algorithmes
Legolas::SIMDDiagInverse et Legolas::SIMDDiagonalMatrixVectorProduct et le conteneur
Legolas::DiagonalFlatMatrixContainer au niveau de la collection . Ces deux algorithmes
correspondent aux squelettes parallèles de MTPS et prennent alors en charge le fait de générer des
vues parallèles. Notons que l’utilisation du conteneur Legolas::DiagonalFlatMatrixContainer
nécessite l’utilisation de conteneurs linéarisés pour les niveaux inférieurs de la matrice.
Finalement, notre démonstrateur permet de paralléliser automatiquement sur les processeurs
X86_64 les deux solveurs présentés dans la section 5.1. Dans le prochain chapitre, nous analy-
serons de manière détaillée les performances obtenues par notre démonstrateur.
117
Chapitre 5. Conception et réalisation d’un démonstrateur multicible de Legolas++
118
Ne me parlez pas de vos efforts. Parlez-moi de
vos résultats.
James J. Ling (1922 – 2004)
Industriel américain et ancien dirigeant de
Ling-Temco-Vought
Chapitre 6
Dans le chapitre 1, nous avons présenté les intérêts et les difficultés de séparer les activités de
développement des fonctionnalités des codes de calcul d’un part et les activités d’optimisation
du code pour un ensemble de plates-formes matérielles données.
Dans le chapitre 2, après avoir rappelé l’apport des différentes architectures matérielles, nous
avons présenté les difficultés liées au développement d’un code multicible. Nous avons identifié
deux difficultés majeures. La première concerne l’expression du parallélisme : selon l’architec-
ture ciblée, les paradigmes de programmation diffèrent, ce qui rend difficile la mise en commun
de portions de codes. La seconde difficulté que nous avons identifié concerne l’adaptation des
structures de données. En effet, afin de pouvoir exploiter au mieux les caractéristiques des archi-
tectures matérielles, les structures de données doivent être modifiées. Cela peut rendre complexe
l’interopérabilité entre des portions de code s’exécutant sur des architectures différentes.
Dans le chapitre 3, nous avons étudié différentes approches permettant de développer des
applications parallèles portables sur différentes architectures. À ce jour, aucun outil disponible
ne prend en charge l’adaptation des structures de données pour les cibles matérielles que nous
ciblons.
Dans le chapitre 4, nous avons décrit le modèle de programmation de MTPS, une biblio-
thèque C++ permettant la parallélisation des opérations vectorisables appartenant au même
contexte vectoriel . Nous avons ensuite présenté comment MTPS prend en charge l’adapta-
tion des structures de données et unifie l’expression du parallélisme.
Dans le chapitre 5, nous avons présenté Legolas++, une bibliothèque permettant de décrire
et de manipuler des matrices creuses et structurées. Après avoir remis en cause certains choix
de conception de MTPS, Nous avons ensuite présenté comment nous avons adapté Legolas++
au modèle de programmation de MTPS afin d’aboutir à une version parallèle capable de cibler
automatiquement les processeurs disposant d’unités vectorielles.
Dans ce chapitre, nous présentons les performances obtenues avec les différentes approches
présentées dans ce document. Les performances des implémentations Legolas++ et MTPS seront
comparées aux performances des implémentations optimisées à la main introduites dans le
chapitre 2.
119
Chapitre 6. Analyse des performances du démonstrateur
Sommaire
6.1 Premier cas : les blocs ont une structure bande symétrique . . . . . 121
6.1.1 Structure de la matrice A et paramètres du problème . . . . . . . . . . 121
6.1.2 Performances d’une implémentation idéale . . . . . . . . . . . . . . . . . 122
6.1.2.1 Performances de l’implémentation optimisée « à la main » . . . 126
6.1.3 Performances de MTPS . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
6.1.4 Performances du démonstrateur Legolas++ multicible . . . . . . . . . . 129
6.1.5 Bilan : accélérations obtenues . . . . . . . . . . . . . . . . . . . . . . . . 130
6.2 Deuxième cas : les blocs ont une structure bande symétrique sur
deux niveaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
6.2.1 Structure de la matrice A et paramètres du problème . . . . . . . . . . 133
6.2.2 Performances du démonstrateur . . . . . . . . . . . . . . . . . . . . . . . 134
6.3 Bilan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
120
6.1. Premier cas : les blocs ont une structure bande symétrique
Dans les deux exemples qui suivent, nous allons résoudre un système linéaire AX = B,
avec deux structures de matrices différentes pour A, impliquant deux algorithmes de résolution
distincts. La matrice A est dans les deux cas une matrice diagonale par blocs. Elle présentera
deux niveaux dans le premier exemple et trois dans le second exemple. Dans le premier cas,
les blocs auront la structure de type bande symétrique présentée dans les chapitres 2 et 4 (cf.
figure 6.1). Dans le second exemple, la structure sera plus complexe : la structure d’un bloc
comportera deux niveaux, chacun de type bande symétrique (cf. figure 6.19). Dans les deux
cas, les sous-problèmes Aii Xi = Bi peuvent être traités indépendamment et parallèlement aux
autres. Le problème AX = B peut en effet être considéré comme une collection 43 vis à vis
de la fonction de résolution d’un problème Aii Xi = Bi .
6.1 Premier cas : les blocs ont une structure bande symétrique
Le premier exemple que nous allons traiter est issu du solveur de neutronique SPN et cor-
respond à l’exemple introduit dans la section 2.2. Les blocs de A ont une structure bande
symétrique. Comme dans la section 2.2, l’algorithme utilisé pour résoudre un sous-problème
Aii Xi = Bi consiste à décomposer le bloc Aii selon une forme LDLT de la factorisation de
Cholesky puis à effectuer une opération dite de « descente-remontée ». Seule cette dernière
opération nous intéresse pour effectuer nos analyses de performances. Il est en effet classique
de ré-utiliser plusieurs fois le résultat de la décomposition pour résoudre des problèmes possé-
dant des seconds membres différents. Ce sera le cas pour résoudre le problème présenté dans la
section 6.2.
Dans cette section, nous allons tout d’abord présenter la structure de la matrice A ainsi
que les différents paramètres permettant de la définir précisément. Nous déterminerons ensuite
les performances maximales théoriques attendues en fonction des caractéristiques des machines
de tests. Finalement, nous comparerons les performances obtenues par les différentes approches
avec les performances théoriques.
Dans le solveur SPN , nb est généralement compris entre 1 × 104 et 3 × 105 . La figure 6.2
32 Nehalem en fonction de n lorsque
représente le nombre d’opérations réalisées par seconde sur 2×4 b
tb et hbw valent respectivement 25 et 2. Nous avons utilisé l’implémentation séquentielle présen-
tée section 2.2. Nous pouvons constater que pour un nombre de blocs compris entre 1 × 104
et 1 × 105 les performances ne dépendent pas du nombre de blocs. Le même constat peut se
généraliser aux autres valeurs des ces deux paramètres, aux autres implémentations (parallèles
et vectorisées) et aux autres machines de tests. Par conséquent, nous ignorerons ce paramètre
dans la suite.
43. voir définition section 4.1.4.1
121
Chapitre 6. Analyse des performances du démonstrateur
hbw
tb
0.6
(GFLOPS)
0.4
0.2
0
10 20 30 40 50 75 100
Nombre de blocs (×103 )
Definition 10. L’intensité arithmétique ia est définie par NVIDIA comme le ratio entre
le nombre d’opérations et le nombre d’accès mémoire d’une portion d’application [133].
122
6.1. Premier cas : les blocs ont une structure bande symétrique
Afin de déterminer l’élément limitant les performances d’un noyau de calcul pour une machine
de calcul donnée, il suffit alors de comparer l’intensité arithmétique du noyau de calcul avec
l’intensité arithmétique critique de la machine de calcul.
– Si ia est inférieur à ic , alors les performances sont limitées par la bande passante mémoire.
L’obtention de meilleures performances nécessite alors de minimiser le nombre d’accès à
la mémoire RAM. Pour cela, il est possible de choisir des algorithmes permettant une
meilleure réutilisation des données. Une autre approche consiste à recalculer à la volée des
éléments plutôt que de les lire en mémoire. C’est dans ce but que la possibilité de ne pas
stocker les matrices Legolas++ a été offerte.
– Si au contraire ia est supérieur à ic , les performances de l’application sont limitées par
la puissance de calcul. Notons que dans ce cas, des efforts supplémentaires d’implémenta-
tion sont requis pour garantir une utilisation optimale du processeur. En effet, l’utilisation
d’algorithmes maximisant l’intensité arithmétique (comme les algorithmes dits « cache
oblivious » [250]) n’est pas suffisant pour garantir des performances optimales. La bib-
liothèque MTL4 [251] permet l’utilisation d’un algorithme récursif minimisant le nombre
d’accès à la mémoire et maximisant donc l’intensité arithmétique. Cependant, cela n’est
pas suffisant pour garantir des performances optimales [252]. Dans son article détaillant les
choix mis en œuvre dans son implémentation des BLAS, Kazushige GOTO donne divers
éléments à prendre en compte pour exploiter pleinement les processeurs [54]. Par exemple,
notre modèle de performance ne prend en compte qu’un seul des trois niveaux de cache, il
est donc optimiste par nature. De plus, notre implémentation ne prend pas en compte ni
le TLB 44 ni l’ordre de parcours des données. Ces deux éléments induisent des latences non
prises en compte par notre modèle lors des accès aux données. De plus, au contraire des
opérations mettant en œuvre des matrices denses, l’algorithme que nous étudions nécessite
plusieurs boucles imbriquées ; ce qui implique des ruptures du pipeline d’instructions 45 et
donc une augmentation de la latence dans l’exécution des opérations de calcul.
Nous allons donc maintenant calculer l’intensité arithmétique de la descente-remontée.
Pour cela, nous devons distinguer l’architecture X86_64 de l’architecture GF100. En effet, la
présence d’un cache de grande capacité sur l’architecture X86_64 permet d’éviter certains accès
mémoire. Nous allons donc distinguer ces deux cas pour le nombre d’accès à la mémoire. Pour
l’architecture X86_64, nous supposons que l’ensemble des données pour un bloc tient en cache
et ne comptabilisons donc que les accès correspondants à une seule lecture du couple matrice-
vecteur 46 et à l’ecriture du résultat dans le même vecteur. Pour l’architecture GF100, tous les
accès sont comptabilisés car les différents niveaux de mémoire cache sont trop petits pour avoir
un impact.
44. Le Translation Lookaside Buffer, ou TLB, est une mémoire cache du processeur utilisé par l’unité de gestion
mémoire dans le but d’accélérer la traduction des adresses virtuelles en adresses physiques.
Source : http://fr.wikipedia.org/wiki/Translation_Lookaside_Buffer
45. Un pipeline est un élément d’un circuit électronique dans lequel les données avancent les unes derrière les
autres, au rythme du signal d’horloge. Dans la microarchitecture d’un microprocesseur, c’est plus précisément
l’élément dans lequel l’exécution des instructions est découpée en étapes. Ce découpage permet d’exécuter la
première étape d’une instruction avant que l’instruction précédente ait fini de s’exécuter. Le premier ordinateur
à utiliser cette technique fut l’IBM Stretch, conçu en 1958.
Source : http://fr.wikipedia.org/wiki/Pipeline_(informatique)
46. Nous comptabilisons dans ce cadre les accès non explicites correspondant aux éléments fantômes de la
matrice. Ces éléments permettent de simplifier les calculs d’indices mais entraînent quelques accès supplémentaires.
123
Chapitre 6. Analyse des performances du démonstrateur
Nous pouvons aisément démontrer que ia,X86_64 et ia,GF100 croissent avec tb et hbw. De plus,
lorsque ces grandeurs tendent vers l’infini et que tb est grand devant hbw, ia,X86_64 et ia,GF100
tendent respectivement vers 4 et 2/3. La figure 6.3 représente ia,X86_64 et ia,GF100 pour différentes
valeurs de tb et hbw.
ia,X86_64 ia,GF100
ia
4
3
2
1
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 20 hbw
Figure 6.3 : Intensité arithmétique sur CPU pour différentes configurations du problème.
Les figures 6.4 à 6.6 comparent ia et ic pour chaque architecture testée. Pour le calcul de
ic , nous avons utilisé les meilleures performances de calcul et de débit de données mesurées
sur la machine selon la méthodologie détaillée section 2.3. Ces mesures sont présentées dans les
tableaux 2.2 et 2.3. Pour les deux machines à base d’architecture X86_64 (figures 6.4 et 6.5),
deux valeurs de ic sont présentées. La plus petite, tracée en bleu clair, correspond à la valeur
de ic calculée pour une exécution séquentielle tandis que la seconde correspond à la valeur de
ic calculée pour une exécution complètement parallélisée (multicœur et SIMD). Pour ces deux
machines, la valeur de ic correspondant à une exécution séquentielle de la descente-remontée est
inférieure ou très proche de ia . Dans ce cas, c’est donc la puissance de calcul du processeur qui
limite les performances. Lorsque la descente-remontée est parallélisée, ic est supérieure à ia et
les performances sont limitées par la bande passante mémoire. Sur la figure 6.6, nous n’avons
représenté ic que pour une exécution parallèle (sur GPU, le faire pour une exécution séquentielle
n’a aucun sens). Sur cette architecture, ic est supérieure à ia et les performances sont limitées
par la bande passante mémoire.
Nous pouvons déduire de cette analyse des performances atteignables sur chaque architecture
par une implémentation idéale. Notons Farchitecture et Darchitecture la puissance de calcul maximale
et le débit de données maximal mesurés sur une architecture donnée. Le temps d’exécution
théorique tarchitecture peut alors être calculé par :
( nOps
Farchitecture si ia > ic
tarchitecture = na,architecture
Darchitecture si ia < ic
124
6.1. Premier cas : les blocs ont une structure bande symétrique
ic séquentiel ia,X86_64
ic parallèle
24
18
12
6
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 20 hbw
32
Figure 6.4 : Comparaison entre ia et les intensités arithmétiques critiques de 2×4 Nehalem.
ic séquentiel ia,X86_64
ic parallèle
45
30
15
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 20 hbw
32
Figure 6.5 : Comparaison entre ia et les intensités arithmétiques critiques de 1×4 SandyBridge.
ic ia,GF100
30
20
10
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 20 hbw
125
Chapitre 6. Analyse des performances du démonstrateur
Ce modèle permet donc d’estimer le temps requis pour résoudre le problème pour une implé-
mentation idéale aussi bien optimisée que les bibliothèques constructeur utilisées pour effectuer
nos mesures de performances maximales (cf. section 2.3). Nous utiliserons ce modèle de perfor-
mances dans la prochaine section afin d’estimer l’optimalité de notre implémentation optimisée
« à la main ».
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.7 : Performances de l’implémentation optimisée sur 2×4 Nehalem (exécution séquentielle).
126
6.1. Premier cas : les blocs ont une structure bande symétrique
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.8 : Performances de l’implémentation optimisée sur 1×4 SandyBridge (exécution séquentielle).
Les figures 6.9 à 6.11 représentent les performances mesurées pour des exécutions parallèles.
Les performances de l’implémentation idéale sont cette fois limitées par la bande passante de la
RAM. Notre implémentation optimisée est alors plus proche de l’implémentation idéale : les per-
formances mesurées correspondent à plus de 80% des performances idéales. Nous pouvons donc
raisonnablement considérer que cette implémentation optimisée constitue une bonne référence
afin de comparer l’efficacité des autres approches présentées dans ce manuscrit.
Implémentation idéale Implémentation optimisée
GFlops
18
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.9 : Performances de l’implémentation optimisée sur 2×4 Nehalem (exécution parallèle).
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.10 : Performances de l’implémentation optimisée sur 1×4 SandyBridge (exécution parallèle).
127
Chapitre 6. Analyse des performances du démonstrateur
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.12 : Performances de MTPS sur 2×4 Nehalem (parallèle).
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.13 : Performances de MTPS sur 1×4 SandyBridge (parallèle).
128
6.1. Premier cas : les blocs ont une structure bande symétrique
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
– Sur la machine 2×432 Nehalem, les écarts de performances entre MTPS et notre implémenta-
tion optimisée sont de 7% en moyenne (20% dans les cas les plus défavorables correspon-
dant aux problèmes les plus petits). Finalement, les performances obtenues avec MTPS
correspondent à plus de 70% des performances qui auraient été obtenues avec une implé-
mentation idéale et plus de 79% des performances de notre implémentation optimisée à la
main.
– Sur la machine 1×432 SandyBridge, les écarts sont beaucoup plus faibles sans que nous soyons
Legolas++ permet même l’obtention de meilleures performances que MTPS. Nous pensons que
cela s’explique par le fait que Legolas++ ne reprend pas l’intégralité des concepts introduits
par MTPS. Legolas++ contient par conséquent moins d’indirections que MTPS, ce qui pourrait
expliquer que les surcoûts soient moindres.
129
Chapitre 6. Analyse des performances du démonstrateur
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.15 : Performances du démonstrateur Legolas++ sur 2×4 Nehalem (parallèle).
12
0
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
Taille
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.16 : Performances du démonstrateur Legolas++ sur 1×4 SandyBridge (parallèle).
mentation de référence uniquement parallélisée avec OpenMP. Sur 1×4 32 SandyBridge, l’accéléra-
130
6.1. Premier cas : les blocs ont une structure bande symétrique
26
21
16
11
6
1 Taille
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
25
50
100
200
500
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.17 : Accélérations apportées par les différentes approches sur 2×4 Nehalem.
des blocs
2 3 4 6 8 10 15 hbw
32
Figure 6.18 : Accélérations apportées par les différentes approches sur 1×4 SandyBridge.
131
Chapitre 6. Analyse des performances du démonstrateur
tion apportée par ce démonstrateur par rapport à cette implémentation de référence parallélisée
avec OpenMP est comprise entre 1,4 et 2,3. Ces accélérations sont apportées par l’utilisation
des unités SIMD (SSE pour 2×432 Nehalem et AVX pour 32 SandyBridge). C’est la spécialisation
1×4
du format de stockage lors de la compilation en fonction de l’architecture matérielle ciblée qui
permet à Legolas++ d’exploiter ces unités SIMD.
Sur la figure 6.17, le facteur d’accélération apporté par l’utilisation de la machine Fermi est
également représenté. Les deux machines 2×4 32 Nehalem et Fermi ayant été mises sur le marché à la
même époque (mars 2009) et correspondant toutes deux à des gammes de produits équivalentes
(station de travail scientifique), cette comparaison est la plus neutre possible. Nous pouvons
donc comparer les gains apportés par une implémentation unique basée sur MTPS sur
deux architectures différentes. Nous constatons sur cette figure que lorsque hbw est inférieur
à 10, les performances sur Fermi sont meilleures que celles obtenues sur 2×4 32 Nehalem. L’écart
maximal étant obtenu pour hbw égal à 2. Lorsque hbw est égal ou supérieur à 10, il devient plus
32 Nehalem que Fermi. Cela s’explique par le fait que sur 32 Nehalem,
intéressant d’utiliser 2×4 2×4
l’intensité arithmétique augmente avec hbw tandis qu’elle reste presque constante sur Fermi (cf.
figures 6.4 à 6.6). En effet, le cache du GF100 est top petit pour permettre une réutilisation des
données lorsque hbw augmente.
Finalement, les facteurs d’accélération obtenus sur 2×432 Nehalem et 32 SandyBridge pour ces
1×4
cas tests varient entre 4 et 16. Ces performances sont donc éloignées du facteur 32 auquel nous
aurions pu nous attendre d’après les caractéristiques des seuls processeurs (8 cœurs × 4 voies
SIMD sur 2×4 32 Nehalem ou 4 cœurs × 8 voies SIMD sur 32 SandyBridge). Comme nous l’avons
1×4
expliqué dans la section 6.1.2, cela s’explique par le fait que les performances sont limitées par
les accès à la mémoire. Dans la prochaine section, nous étudierons les facteurs d’accélérations
obtenus sur des problèmes présentant une intensité arithmétique plus grande et n’étant pas
limités par la bande passante de la mémoire RAM. Nous pourrons alors juger la qualité de la
parallélisation mise en œuvre par MTPS et le démonstrateur Legolas++.
6.2 Deuxième cas : les blocs ont une structure bande symétrique
sur deux niveaux
Dans la section précédente, nous avons étudié les performances obtenues par notre démon-
strateur de Legolas++ multicible sur des cas relativement simples. En effet, la structure de
données associée à ces cas correspond à une structure bidimensionnelle rectangulaire, c’est-à-
dire une structure bidimensionnelle dont les tailles sont constantes pour chaque dimension. Nous
avions d’ailleurs utilisé ce cas pour introduire les différents formats de stockage optimisés dans
la section 2.2.
Dans cette section, nous nous intéressons à un cas plus complexe : la structure de don-
nées associée à ce problème n’est plus bidimensionnelle rectangulaire, mais tridimensionnelle
(une dimension par niveau de structure de la matrice) et non rectangulaire. Notre démonstra-
teur Legolas++ transforme automatiquement lors de la compilation cette structure de données
tridimensionnelle en structure bidimensionnelle rectangulaire. Legolas++ peut ainsi utiliser les
formats de stockage spécialisés pour les différentes architectures (cf. section 2.2). Grâce à cette
adaptation du stockage, Legolas++ peut ensuite utiliser les connaissances issues du domaine
mathématique pour paralléliser et vectoriser automatiquement l’algorithme de résolution choisi.
132
6.2. Deuxième cas : les blocs ont une structure bande symétrique sur deux niveaux
L3 = 3
L2 = 4
L1 = 5
54L2 − 80L + 16
i a = ni .
9L2 − 4L
133
Chapitre 6. Analyse des performances du démonstrateur
Nous nous plaçons dans le cas où ni et L sont supérieurs à 10. Dans ce cas, ia est toujours
supérieur à 50. Nous nous plaçons ainsi dans un contexte où ia est supérieur à ic , quelque soit la
32 Nehalemet 32 SandyBridge, i vaut respectivement
configuration matérielle envisagée : sur 2×4 1×4 c
18.9 et 41.3. D’après notre modèle introduit section 6.1.2, les performances de cet algorithme
doivent alors être limitées par la puissance de calcul et non pas par la bande passante de la
mémoire RAM.
20
10
0
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
ni
10 15 20 25 50 100 L
32
Figure 6.20 : Performances du démonstrateur Legolas++ sur 2×4 Nehalem.
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
ni
10 15 20 25 50 100 L
32
Figure 6.21 : Performances du démonstrateur Legolas++ sur 1×4 SandyBridge.
les performances sont relativement stables. Lorsque L vaut 100, les données nécessaires pour
résoudre un problème correspondant au second niveau de la matrice prennent environ 450 ko.
Afin de ne pas effecteur d’accès redondants à la mémoire RAM, il faut donc que le processeur
dispose d’au moins 450 ko de cache par problème résolu simultanément. Sur 2×4 32 Nehalem, chacun
des deux processeurs dispose de 4 Mo de cache de niveau 3. Chacun de ces processeurs peut
donc résoudre efficacement jusqu’à neuf problèmes simultanément. Dans le cas où Legolas++
est configuré pour utiliser les unités SSE disponibles sur tous les cœurs, 16 problèmes sont résolus
simultanément sur chaque processeur. La baisse de performances pour les cas où L vaut 100 est
donc due à des défauts de cache. Sur 1×4 32 SandyBridge, le même raisonnement est valable en
134
6.2. Deuxième cas : les blocs ont une structure bande symétrique sur deux niveaux
27
14
1
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
ni
10 15 20 25 50 100 L
32
Figure 6.22 : Accélérations apportées par Legolas++ sur 2×4 Nehalem.
28
19
10
1
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
10
100
1000
ni
10 15 20 25 50 100 L
32
Figure 6.23 : Accélérations apportées par Legolas++ sur 1×4 SandyBridge.
résultats en prenant pour chaque donnée indiquée la valeur moyenne observée pour les différentes
configurations du problème.
Afin d’analyser les facteurs d’accélération obtenus, il convient de les comparer au nombre
de voies parallèles utilisées. Nous pouvons tout d’abord noter que l’accélération apportée par
l’utilisation des différents cœurs disponibles est parfaitement linéaire avec le nombre de voies
disponibles.
Lorsque les unités SSE sont utilisées, l’accélération est super-linéaire. Ceci s’explique par le
fait que pour cette famille de problèmes, il est plus performant de choisir un format de stockage
correspondant au format de stockage spécialisé pour les unités SSE (avec un entrelacement des
éléments 4 par 4). Afin de mieux comprendre ce résultat, nous avons mesuré les performances
obtenues sur nos deux machines avec des implémentations « dé-vectorisées ». c’est-à-dire que
nous avons remplacé les fonctions intrinsèques du compilateur correspondant aux instructions
vectorielles par des boucles appliquant l’opération souhaitée sur les différents éléments du vecteur
SIMD. Notons que dans ce cas, notre compilateur (la version 4.6.1 de g++) ne « re-vectorise » pas
la boucle ainsi générée. Nous avons pris comme référence séquentielle les performances obtenues
avec le format de stockage le plus simple car il correspond à ce qui aurait été obtenu sans utiliser
135
Chapitre 6. Analyse des performances du démonstrateur
(séquentiel)
SSE 4 14.3 3.7 25.7 4.9
TBB 8 25.9 5.9 22.7 7.8
32
(séquentiel)
SSE 4 23.5 7.1 30.0 5.0
AVX 8 47.0 8.8 18.6 6.3
TBB 4 23.0 5.6 24.4 4.0
32
Legolas++. Les performances obtenues avec une implémentation séquentielle utilisant le format
de stockage dédié aux unités SSE sont environ 1.4 fois plus élevées que celles obtenues avec le
format de stockage le plus simple. Il serait cependant absurde de mettre au point un code de cal-
cul s’appuyant sur un tel format de stockage sans aller jusqu’au bout de la démarche, c’est-à-dire
jusqu’à l’utilisation des unités SSE. Deux éléments permettent d’expliquer ce phénomène. Pre-
mièrement, cela permet de diviser par 4 le nombre de tests et d’opérations de calculs d’indices :
ces tests et calculs sont communs aux différents éléments d’un bloc SSE. Deuxièmement, les
accès à la mémoire ne sont pas effectués de manière continue dans l’algorithme de résolution
choisi. Le choix d’un format de stockage non entrelacé peut générer des accès conflictuels entre
les bancs du cache L1 47 . Au contraire, les formats de stockage entrelacés minimisent l’appari-
tion de ces conflits. À partir d’un entrelacement 8 par 8, ces conflits ne peuvent plus apparaître.
Cependant, comme nous l’avons évoqué dans la section 2.2.2.2, ceci est obtenu au détriment
de la localité de données et donc de l’utilisation optimale du cache. Finalement, à format de
stockage équivalent, l’utilisation des unités SSE apporte un facteur d’accélération d’environ 3.5.
Lorsque les unités AVX sont utilisées sur 1×4 32 SandyBridge, l’accélération obtenue est in-
férieure à 8. À format de stockage équivalent, l’utilisation des unités AVX n’apporte qu’un gain
de 4.5. Nous pensons que ceci est dû aux limitations du bus de données entre les registres AVX
et le cache L1. En effet, sur l’architecture Sandy Bridge [156], trois bus de transferts de 128 bits
sont disponibles entre ces registres et le cache L1 : deux permettent de charger des données
depuis le cache L1 vers les registres tandis que le dernier permet de stocker des données en
registres vers le cache L1. De ce fait, un seul jeu de données de 256 bits peut transiter depuis
le cache L1 vers les registres AVX à chaque cycle tandis que deux cycles sont nécessaires au
stockage d’un élément. Le taux de réutilisation des données à l’échelle d’un registre est relative-
ment faible dans notre algorithme, nous supposons donc que l’exécution est limitée par le débit
de données entre les registres AVX et le cache L1. Dans le cas d’un produit de matrices dense
(type sgemm [46]), les données déjà en registres sont réutilisées. Cela permet d’éviter ce goulet
d’étranglement. Dans le cas de l’algorithme de résolution choisi, cela parait difficile à mettre en
œuvre. Ce devrait de toute façon être fait dans la version séquentielle de l’algorithme : la paral-
47. Voir pages 2-22 et 3-57 du guide d’optimisation d’INTEL [253] pour plus de détails concernant les conflits
d’accès au cache L1.
136
6.3. Bilan
lélisation est automatiquement effectuée par Legolas++ sur la base de l’algorithme de résolution
séquentiel choisi.
Finalement, les efficacités obtenues correspondent aux résultats obtenus sur de nombreuses
machines avec la version FORTRAN de l’implémentation NETLIB des BLAS [254]. Le travail
effectué par Kazushige Goto pour mettre au point la bibliothèque Goto BLAS [54, 55] consiste
justement à prendre en compte les effets que nous venons de citer. Contrairement au modèle
que nous avons introduit et qui repose sur l’existence d’un cache parfait, unique, uniforme
et de taille infinie, Kazushige Goto s’appuie sur un modèle prenant en compte les registres
et le fonctionnement réel des différents niveaux de caches. Il parvient ainsi à maximiser le
nombre d’opérations pouvant être effectuées par seconde. Parmi les optimisations citées, notons
la mise en place d’accès continus à la mémoire et la maximisation de la réutilisation des données.
Ces transformations de l’algorithme original sont rendues possibles grâce à la simplicité de cet
algorithme. Effectuer le même travail d’optimisation sur l’algorithme que nous étudions paraît
extrêmement compliqué : cela suppose tout d’abord d’écrire cet algorithme « à plat », c’est-
à-dire sans utiliser Legolas++. La complexité de la structure de la matrice choisie présage
d’un algorithme tout aussi compliqué. Une fois cet algorithme écrit, il serait alors possible
de réordonner les opérations de façon à pouvoir maximiser la réutilisation des données dans
les registres. La difficulté serait alors de trouver les ordonnancements valables pour toutes les
configurations de matrices.
6.3 Bilan
Notre démonstrateur permet, à partir d’un code relativement simple et concis, de définir
des matrices structurées et les algorithmes de résolutions correspondant à ces matrices. Le code
des solveurs ainsi écrit est portable sur différentes générations de processeurs. Les performances
obtenues sur ces différentes générations de processeurs sont excellentes : bien que notre im-
plémentation séquentielle ne soit pas mauvaise (environ 20% des performances théoriques), les
accélérations obtenues sont quasi-linéaires en fonction du nombre de voies parallèles lorsque
l’algorithme employé offre une intensité arithmétique suffisante. Nous devons maintenant
étudier comment améliorer les performances de l’implémentation séquentielle avant d’envisager
de la généraliser automatiquement.
137
Chapitre 6. Analyse des performances du démonstrateur
138
Ne t’attarde pas à l’ornière des résultats
René Char (1907 – 1988)
Capitaine Alexandre pendant la Résistance
Poète
Chapitre 7
Conclusions et perspectives
Dans ce chapitre, nous rappelons les résultats obtenus au cours de cette thèse et proposons
des pistes pour poursuivre ces recherches.
7.1 Bilan
Dans cette thèse, nous abordons la problématique de la maintenance des codes de calcul
intensifs dans un contexte industriel. Nous avons vu dans l’introduction que la maintenance
de ces codes est complexe et coûteuse. À ces difficultés s’ajoutent celles de la maintenance des
performances sur les différentes générations de matériel sur lesquelles ces codes de calcul devront
s’exécuter pendant leur durée de vie. En effet, les approches favorisant la maintenabilité d’un
code sont souvent en opposition avec celles qui optimisent les performances. Lors de la mise au
point d’un code de calcul intensif, un compromis doit donc être trouvé entre maintenabilité et
performances.
Développée au sein du département SINETICS d’EDF R&D, la bibliothèque Legolas++
vise à établir un compromis intéressant. En effet, elle propose une approche de haut niveau
dans un domaine restreint, ce qui permet à ses utilisateur une expressivité proche du langage
mathématique pour décrire des solveurs d’algèbre linéaire mettant en œuvre des matrices creuses
structurées. Ce haut niveau d’abstraction permet de faciliter la maintenance des code de calculs
s’appuyant sur Legolas++ en permettant aux utilisateurs de se focaliser sur les aspects mathé-
matiques du problème sans se soucier des aspects informatiques. Afin de compléter cet objectif,
il est important de pouvoir proposer une implémentation optimisée de Legolas++ sur différentes
architectures matérielles. Dans cette thèse, nous avons exploité les connaissances a priori issues
du domaine d’application de Legolas++ afin d’atteindre cet objectif.
Pour cela, dans le chapitre 2, nous avons étudié les implémentations optimisées (parallèles
et vectorisées) pour CPU et pour GPU d’un problème d’algèbre linéaire mettant en œuvre des
matrices creuses structurées. Nous sommes parvenus à la conclusion que deux types de différences
séparent les codes optimisés pour ces architectures matérielles :
– les modèles de programmation utilisés et donc l’expression du parallélisme ne sont pas
identiques,
– les formats de stockage des données conduisant à l’obtention de performances optimales
varient d’une architecture à l’autre.
Dans le but de masquer ces différences, nous avons étudié dans le chapitre 3 les différentes
approches permettant la mise au point d’un code parallèle portable sur différentes architectures
matérielles. Nous avons identifié plusieurs solutions pour masquer les différences entre les modèles
139
Chapitre 7. Conclusions et perspectives
de programmation liées aux différentes architectures matérielles. Cependant aucun des outils que
nous avons étudié ne permet aujourd’hui d’adapter automatiquement le format de stockage des
données.
Afin de répondre à ce besoin, nous avons conçu MTPS (Multi-Target Parallel Skeleton),
une bibliothèque C++ visant à adapter automatiquement le format de stockage des données
sur différentes architectures matérielles. MTPS utilise des squelettes parallèles afin de masquer
les différences entre les modèles de programmation et un système de vues sur les données pour
permettre une manipulation de celles-ci indépendamment de leur format de stockage effectif. Les
performances obtenues avec MTPS sont proches des performances optimales atteignables sur nos
différentes machines de test. Cependant, pour parvenir à ce résultat, nous avons été contraints de
restreindre l’ensemble des cas d’application de MTPS : cette bibliothèque ne permet de traiter
que des problèmes conduisant à appliquer une même fonction à tous les éléments d’une collection
homogène.
Dans le chapitre 5, après avoir introduit Legolas++ et son utilisation, nous avons présenté
comment aboutir à une version multicible de Legolas++ en nous appuyant sur notre expérience
MTPS. Pour cela, nous avons réimplémenté dans notre démonstrateur les concepts permettant
de cibler les processeurs multicœurs disposant d’unités vectorielles différentes. Ce démonstrateur
nous permet de montrer qu’il est possible, avec Legolas++, de générer automatiquement, à par-
tir d’un même code source, des exécutables optimisés pour différentes architectures matérielles.
Naturellement, les contraintes permettant à MTPS d’adapter le format de stockage restent val-
ables avec ce démonstrateur de Legolas++ multicible et seules les opérations mettant en œuvre
des matrices diagonales par bloc et dont les blocs possèdent la même structure peuvent être
optimisées automatiquement pour différentes architectures. De très nombreux cas d’application
de Legolas++ mettant en œuvre de telles matrice, cela ne nous est pas apparu comme une
limitation.
Dans le chapitre 6, après avoir introduit un modèle simplifié des performances d’une applica-
tion sur une architecture donnée, nous avons analysé les performances obtenues par différentes
approches sur un premier exemple de problème. Nous avons alors montré que MTPS et Lego-
las++ introduisent un faible surcoût comparé à des implémentations optimisées : dans tous les
cas, les performances obtenues sont très proches des performances prédites par notre modèle.
Nous avons ensuite introduit un second exemple plus complexe que nous n’avons implémenté
qu’avec notre démonstrateur Legolas++ : l’implémentation de cet exemple aurait été très com-
plexe à mettre en œuvre avec une autre approche. Les implémentations obtenues conduisaient
à des performances comprises entre 20% et 30% textbfdes performances théoriques sur nos dif-
férentes machines de test, ce qui est très satisfaisant à nos yeux compte tenu du type d’algorithme
que nous avons choisis de mettre en œuvre.
7.2 Perspectives
Naturellement, la première suite qui peut être donnée à ces travaux consiste à intégrer les
résultats précédents dans une version stable et industrialisable de Legolas++. Cela pose la ques-
tion technique du choix du langage. Les différents outils conçus dans le cadre de cette thèse ont
été implémentés en C++. Une autre solution aurait été de définir un langage ad hoc et de con-
cevoir son compilateur. Cette solution aurait permis de simplifier la syntaxe, en particulier pour
MTPS, mais cette stratégie implique des investissements importants dans l’environnement de
développement. Cependant, la complexité atteinte dans les mécanismes de métaprogrammation
peuvent laisser penser que nous sommes proches des limites de l’approche dans la mesure où il
140
7.2. Perspectives
141
Chapitre 7. Conclusions et perspectives
142
Annexe A
Du polymorphisme dynamique au
polymorphisme statique
La Programmation Orientée Objet (POO) est une approche permettant d’augmenter l’ex-
pressivité d’une bibliothèque. En effet, la possibilité de définir des objets possédant des fonctions
membres et de passer ces objets en argument d’autres fonctions permet in fine d’écrire des fonc-
tions qui prennent comme argument des fonctions. Un objet est une structure de données valuées
et cachées qui répond à un ensemble de messages correspondant à des fonctions « membres ».
Cette structure de données définit son état tandis que l’ensemble des messages qu’il accepte
décrit son comportement.
Des objets de différentes classes peuvent définir la même interface, c’est-à-dire accepter les
mêmes messages. L’implémentation des fonctions permettant le traitement de ces messages sera
cependant généralement différente d’une classe d’objet à l’autre. La POO permet l’encapsulation
des fonctions et des données : en fonction de leur état interne, deux objets peuvent se comporter
différemment face aux mêmes sollicitations. Du point de vue de l’utilisateur d’une Bibliothèque
Orientée Objet (BOO), un même extrait de code pourra donc générer des exécutions différentes
selon le type et l’état interne des différents objets manipulés.
Nous allons dans cette annexe étudier la conception d’une BOO afin de comprendre comment
cette approche permet d’améliorer l’expressivité d’une bibliothèque. Nous allons nous intéresser
au cas présenté dans la section 1.2.2. Nous présenterons dans un premier temps la conception
et l’implémentation de cette bibliothèque en C++ en utilisant les techniques de programma-
tion habituelles de ce langage. Dans un second temps, nous présenterons une implémentation
alternative permettant l’obtention de meilleures performances mais dont l’utilisation est plus
contrainte.
143
Annexe A. Du polymorphisme dynamique au polymorphisme statique
En C++, une classe abstraite contient au moins une fonction purement virtuelle. Afin de définir
une fonction comme purement virtuelle, il suffit de la déclarer virtuelle (avec le mot-clé virtual)
et de la définir comme étant égale à zéro (=0). La ligne 2 déclare que les classes héritant de
VectorExpression doivent définir une fonction virtuelle appelée size qui ne prend aucun ar-
gument et qui renvoie un entier. La ligne 3 déclare la fonction get qui prend en argument un
entier i et renvoie un nombre flottant.
D’autres informations sémantiques non explicitement formulées s’ajoutent à ces définitions :
– l’entier retourné par la fonction size représente la taille des vecteurs de l’expression,
– le nombre flottant retourné par la fonction get est égal au ième élément de l’expression.
Avec la définition de cette classe, nous disposons de suffisamment d’informations pour pouvoir
mettre au point une fonction qui calcule la norme des expressions vectorielles :
float norm ( const VectorExpression & ve ) {
float out = 0;
for ( int i =0; i < ve . size () ; ++ i ) {
tmp = ve . get ( i ) ;
out += tmp * tmp ;
}
return sqrtf ( out ) ;
}
Naturellement, nous ne pouvons pas utiliser directement cette fonction. En effet, la classe
VectorExpression est abstraite et ne peut donc pas être instanciée. Pour pouvoir utiliser cette
fonction, il nous faut donc définir des classes héritant de VectorExpression. La première classe
que nous allons implémenter est la classe Vector qui représente un vecteur :
1 class Vector : public VectorExpression {
2 public :
3 Vector ( const int size ) : size_ ( size ) , data_ ( new float [ size_ ]) {}
4 ~ Vector () { delete [] data_ ;}
5 ...
6 virtual int size () const { return size_ ; };
7 virtual float get ( int i ) const { return data_ [ i ]; };
8 float & set ( int i ) { return data_ [ i ]; };
9 ...
10 private :
11 const int size_ ;
12 float * data_ ;
13 };
La ligne 1 déclare que la classe Vector hérite de la classe VectorExpression. Afin de pouvoir
être instanciée, cette classe ne doit plus contenir de fonction purement virtuelle. Les fonctions
size et get sont donc respectivement surchargées aux lignes 6 et 7.
Contrairement au paradigme fonctionnel, la programmation orientée objet autorise la mod-
ification des données. La fonction set, définie à la ligne 8, permet donc de modifier les données
du vecteur.
Comme la classe Vector hérite de la classe VectorExpression, nous pouvons maintenant
calculer la norme d’un vecteur. Afin de prendre en charge les expressions vectorielles plus com-
plexes, nous devons définir les autres classes correspondant aux expressions vectorielles. Une
expression vectorielle est une combinaison linéaire d’expressions vectorielles. Deux types d’opéra-
tions doivent donc être exprimables : la somme de deux expressions vectorielles d’une part et le
144
A.1. Programmation orientée objet
produit d’une expression vectorielle avec un scalaire d’autre part. Nous allons donc définir deux
classes AddVectorExpr et ScalVectorExpr correspondant respectivement à ces deux opérations.
Naturellement, AddVectorExpr hérite de la classe abstraite VectorExpression :
1 class AddVectorExpr : public VectorExpression {
2 public :
3 AddVectorExpr ( const VectorExpression & l , const VectorExpression & r )
4 : left_ ( l ) , right_ ( r ) { assert ( left_ . size () == right_ . size () ) ; }
5 virtual int size () const { return left_ . size () ; };
6 virtual float get ( const int i ) const {
7 return left_ . get ( i ) + right_ . get ( i ) ;
8 };
9 private :
10 const VectorExpression & left_ ;
11 const VectorExpression & right_ ;
12 };
Afin de pouvoir calculer les éléments à retourner avec la fonction get, la classe AddVectorExpr
doit contenir des références vers les deux expressions à ajouter : left_ et right_, respectivement
définies aux lignes 10 et 11. Le calcul de la somme des éléments de left_ et right_ est déporté
dans la fonction get définie à la ligne 6. Nous pouvons ainsi éviter l’allocation d’un vecteur
temporaire comme le vecteur Z que nous avions utilisé avec les BLAS (cf. section 1.2.2.1).
La classe ScalVectorExpr ressemble beaucoup à la classe AddVectorExpr :
1 class ScalVectorExpr : public VectorExpression {
2 public :
3 ScalVectorExpr ( const float & a , const VectorExpression & x )
4 : a_ ( a ) , x_ ( x ) {}
5 virtual int size () const { return x_ . size () ; };
6 virtual float get ( const int i ) const { return a_ * x_ . get ( i ) ; };
7 private :
8 const float a_ ;
9 const VectorExpression & x_ ;
10 };
La principale différence réside dans la fonction get (cf. ligne 6) qui multiplie l’élément de l’ex-
pression et le scalaire. Notons que x (cf. ligne 9) peut représenter n’importe quelle classe héri-
tant de VectorExpression. Les classes AddVectorExpr et ScalVectorExpr permettent donc de
représenter n’importe quelle expression vectorielle, aussi longue soit-elle.
L’utilisateur peut maintenant écrire un code comme celui-ci :
1 Vector w ( n ) ,x ( n ) ,y ( n ) ;
2 float a ,b , c ;
3 ...
4 float result = norm (
5 AddVectorExpr (
6 AddVectorExpr (
7 ScalVectorExpr (a , w ) ,
8 ScalVectorExpr (b , x )
9 ),
10 ScalVectorExpr (c , y )
11 )
12 )
Il est aisé de regrouper les classes Vector, ScalVectorExpr, AddVectorExpr ainsi que la fonction
norm dans une bibliothèque. Afin de faciliter l’utilisation de cette bibliothèque, le C++ permet
de surcharger les opérateurs et ainsi de définir des langages spécialisés à un domaine.
145
Annexe A. Du polymorphisme dynamique au polymorphisme statique
C optimisé
3 MKL Parallèle
Haskell Parallèle
2
1 BOO optimisée
L’utilisateur de cette bibliothèque est alors en droit d’espérer que leurs implémentations
soient optimisées. Ce droit est d’autant plus légitime qu’il est relativement aisé de fournir une
implémentation parallèle et vectorisée de cette bibliothèque. Le graphique « BOO » de la fig-
ure A.1 reprend le code présenté ci-dessus tandis que le graphique « BOO optimisé » est obtenu
à partir d’une implémentation parallèle et vectorisée par nos soins. Les autres approches men-
tionnées correspondent aux approches présentées dans la section 1.2.2
Les performances fournies par cette approche (0,17 GFlops pour la version ci-dessus et
3,6 GFlops pour la version optimisée) sont meilleures que celles fournies par la MKL. Elles
restent cependant très inférieures à la version optimisée en C. Ceci est dû au coût des fonctions
virtuelles. En effet, les fonctions virtuelles sont implémentées avec des pointeurs de fonction et
impliquent donc un surcoût d’indirection. Sur la machine 2×4 32 Nehalem, ce surcoût est estimé à
30 cycles. Ce chiffre est à comparer au nombre de cycles requis pour exécuter le corps de nos
fonctions virtuelles (1 ou 2 selon les cas). De plus, l’utilisation de fonctions virtuelles empêche
le compilateur d’effectuer des optimisations interprocédurales. Dans cet exemple, cela se traduit
par le fait que le compilateur est incapable de vectoriser cette implémentation.
Toutes les informations étant disponibles à la compilation, il est regrettable d’utiliser un
mécanisme impliquant un tel surcoût lors de l’exécution. Un outil qui serait capable d’analyser
le code et d’effectuer la composition des opérations lors du processus de compilation permettrait
d’obtenir l’expressivité souhaitée et n’impliquerait pas de surcoût sur les performances. En C,
146
A.2. La programmation générative
Une bibliothèque active est une bibliothèque qui fournit à la fois des abstractions
propres à un domaine et les connaissances requises pour les optimiser et vérifier
leurs exigences sémantiques. En outre, les bibliothèques actives sont composables :
un même fichier source peut combiner l’utilisation de plusieurs d’entre elles.
147
Annexe A. Du polymorphisme dynamique au polymorphisme statique
6 }
7 };
8 ...
9 class Derived : public Base < Derived >{
10 ...
11 };
La ligne 9 signifie que la classe Derived hérite de la classe Base<Derived>. Le CRTP fonctionne
sur l’hypothèse qu’il n’y a que la classe Derived qui hérite de Base<Derived>. Grâce à cette
hypothèse, toute instance de la classe Base<Derived> peut être convertie en une instance de
la classe Derived. Dans l’exemple ci-dessus, la fonction convert (ligne 4) convertit l’instance
courante de Base<DERIVED> en une instance de la classe DERIVED. Cette conversion permet en-
suite aux autres méthodes de la classe Base<DERIVED> de faire directement appel aux fonctions
de la classe DERIVED. Le mot-clé inline précédent la déclaration de la fonction convert en-
courage le compilateur à inliner cette fonction. L’appel à cette fonction a donc généralement un
coût nul.
Nous allons maintenant appliquer ceci aux classes d’expression vectorielles :
1 template < class VECTOR_EXPRESSION > class V e ct o r T em p l a te E x p re s s i on {
2 public :
3 inline int size () const { return convert () . size () ; }
4 inline float get ( const int i ) const { return convert () . get ( i ) ; }
5 private :
6 inline const VECTOR_EXPRESSION & convert () const {
7 return * static_cast < const VECTOR_EXPRESSION * >( this ) ;
8 }
9 };
Nous reconnaissons ici la classe VectorExpression qui a été adaptée pour permettre l’utilisa-
tion du CRTP. Comme dans l’exemple introductif de la classe Base, une instance de la classe
VectorTemplateExpression<VECTOR_EXPRESSION> peut être convertie en une instance de la
classe VECTOR_EXPRESSION. Cette conversion permet d’appeler directement les fonctions size
et get (lignes 3 et 4) qui ne sont plus virtuelles mais font directement appel aux implémenta-
tions de la classe VECTOR_EXPRESSION. Ces fonctions peuvent maintenant être inlinées par le
compilateur. Appeler une de ces fonctions est donc strictement équivalent à appeler directement
l’implémentation de la classe VECTOR_EXPRESSION.
Nous pouvons maintenant écrire une version générique de la fonction norm afin de prendre
en compte ces modifications :
1 template < class VE >
2 float norm ( const VectorTemplateExpression < VE > & ve ) {
3 float out = 0;
4 for ( int i =0; i < ve . size () ; ++ i ) {
5 float tmp = ve . get ( i ) ;
6 out += tmp * tmp ;
7 }
8 return sqrtf ( out ) ;
9 }
Cette fonction est paramétrée par le type réel VE de l’expression vectorielle ve. Lors de l’appel
à cette fonction, le compilateur instanciera automatiquement norm en fonction du type de ve.
Le typage spécifique VectorTemplateExpression<VE> de ve permet d’empêcher le compilateur
d’instancier cette fonction avec un argument incompatible. Le reste de la fonction n’a pas besoin
d’être modifiée.
148
A.2. La programmation générative
Pour les classes, les modifications à apporter sont légèrement plus importantes. Nous allons
prendre l’exemple de la classe ScalVectorTemplateExpr :
1 template < class VE > class Sc al Ve ct orT em pl ate Ex pr
2 : public VectorTemplateExpression < ScalVectorTemplateExpr < VE > >{
3 public :
4 inline Sc al Ve ct orT em pl ate Ex pr ( const float & a
5 , const VectorTemplateExpression < VE >& x )
6 : a_ ( a ) , x_ ( x ) {}
7 inline int size () const { return x_ . size () ; };
8 inline float get ( const int i ) const { return a_ * x_ . get ( i ) ; };
9 private :
10 const float a_ ;
11 const VectorTemplateExpression < VE > & x_ ;
12 };
149
Annexe A. Du polymorphisme dynamique au polymorphisme statique
1 BOO optimisée
BA optimisé
0
Figure A.2 : Performances obtenues pour le calcul de la norme ||aW + bX + cY || sur notre machine de
32
tests 2×4 Nehalem pour des vecteurs de plus de 3 × 106 éléments.
1 VectorTemplate w ( n ) ,x ( n ) ,y ( n ) ;
2 float a ,b , c ;
3 ...
4 float result = norm ( a * w + b * x + c * y ) ;
150
Annexe B
Le format de stockage de matrices creuses CRS (Compressed Row Storage) vise à limiter
l’empreinte mémoire des matrices creuses. Il permet aussi de limiter le nombre d’opérations
à effectuer pour les calculs mettant en œuvre des matrices (ex : produit matrice-vecteur). Le
format CRS nécessite le stockage de plusieurs tableaux. Un premier tableau, val, contient les
éléments non-nuls stockés ligne par ligne. Un second tableau, col_ind, contient les indices de
colonnes des éléments qui sont dans le tableau val. Le dernier tableau, row_ptr, contient les
indices de début de ligne dans le tableau col_ind. Nous allons illustrer ceci par un exemple.
Soit A une matrice de taille 5 × 4 :
a 0 0 b 0
0 0 c 0 d
A= .
0 e 0 0 0
0 0 f 0 0
Cette matrice contient vingt éléments dont quatorze sont nuls. Afin de limiter à la fois
l’empreinte mémoire et le nombre d’opérations à effectuer, il est pertinent de définir un format
de données adapté à de telles matrices. Dans le format de stockage CRS, le tableau val contient
l’ensemble des éléments non nuls tandis que le tableau col_ind contient l’indice de colonne de
ces éléments :
val =[ a b c d e f ]
col_ind = [ 1 4 3 5 2 3 ].
Par exemple, l’élément c est dans la troisième colonne, comme l’élément f . Le tableau row_ptr
contient les indices de début de ligne dans le tableau col_ind :
row_ptr = [ 1 3 5 6 ].
Par exemple, les troisième et quatrième éléments de row_ptr, valent 5 et 6. La différence est de
1, ce qui signifie que la troisième ligne de la matrice A contient 1 élément. Cet élément (e) et
son numéro de colonne (2) sont stockés en cinquième position des tableaux val et col_ind.
151
Annexe B. Introduction aux formats de stockage creux Compressed Row Storage
152
Annexe C
Définitions Legolas++
Dans ce chapitre, nous présentons quelques exemples de classes à implémenter pour utiliser
Legolas++. Nous présentons tout d’abord comment implémenter la définition d’une matrice
avant de présenter comment ajouter un algorithme de résolution à Legolas++. Ce chapitre
s’appuie sur les éléments introduits section 5.1.
153
Annexe C. Définitions Legolas++
20 };
21
22 // un accesseur optimise selon la structure de matrice choisie ligne 7
23 static inline GetElement XXXXX ( int i , int j , const Data & data ) ;
24 // differentes fonctions permettant de recuperer les parametres
25 // variables de la structure de matrice choisie ligne 7
26 static inline int XXXXX ( const Data & data ) ;
27 static inline int XXXXX ( const Data & data ) ;
28 };
Ce concept permet d’accéder à l’ensemble des données définissant une matrice Legolas++.
– Le type des données réelles contenues dans la matrice est défini par REAL_TYPE (ligne 4).
– Le nombre de niveau de la matrice est défini par LEVEL (ligne 4).
– La description de la structure élémentaire de la matrice est accessible d’une part par le type
de structure MatrixStructure (ligne 7) et d’autre part par les accesseurs définis à partir
de la ligne 26. Les différentes structure élémentaires définissent le schéma de remplissage de
la matrice. Elles définissent une hiérarchie. En effet, la structure Legolas::Diagonal est
un cas particulier de la structure Legolas::Banded La liste des accesseurs à implémenter
dépend de la structure MatrixStructure choisie.
– La taille de la matrice et de ses blocs sont accessibles via des accesseurs hérités de la classe
Legolas::DefaultMatrixDefinition<REAL_TYPE, LEVEL> (ligne 4).
– Les données de la matrice sont accessibles via l’accesseur optimisé ligne 23. La signature
de l’accesseur dépend de la structure de matrice choisie.
154
C.1. Définition d’une matrice Legolas++
hbw
tb
Au premier niveau de la matrice se trouvent des blocs de matrice possédant une structure
bande symétrique. Pour ce niveau, nous reprendrons la définition de matrice MDefinition intro-
duite section 5.1.2.1. Il nous reste donc à créer ADefinition, la définition Legolas++ du second
niveau de matrice.
Pour le second niveau, le type de réels ne change pas : il est nécessairement le même à
tous les niveaux. Par contre, les éléments de la matrice sont de type MDefinition::Data. La
classe imbriquée ADefinition::Data doit donc contenir les données nécessaires à l’instanciation
d’éléments de type MDefinition::Data, c’est à dire qu’elle doit contenir la demi-largeur des
blocs. Ceci montre qu’il est impossible de créer une définition générique ne dépendant que de la
structure de niveau courante : la définition d’un niveau de matrice dépend de la définition des
niveaux de matrices inférieurs.
class ADefinition : public Legolas :: DefaultMatrixDefinition < float ,2 >{
public :
typedef Legolas :: Diagonal MatrixStructure ;
typedef typename MDefinition :: Data GetElement ;
// ShapeType est fourni par DefaultMatrixDefinition
typedef MDefinition :: ShapeType ShapeType ;
155
Annexe C. Définitions Legolas++
Ce concept introduit un patron de classe imbriqué nommé Engine 49 . Ce patron est paramétré
par le type TA de la matrice, le type TX du vecteur inconnu et le type TB 50 du vecteur mem-
bre de droite de l’équation AX = B. Ce patron de classe comporte un constructeur et deux
méthodes permettant de résoudre le problème. Le constructeur sert typiquement à allouer les
sous-solveurs et les variables d’état du solveur courant. La fonction solve permet de résoudre
le système AX = B tandis que la fonction transposeSolve permet de résoudre le système
49. Le fait de paramétrer une sous-classe Engine plutôt que la classe ALGORITHM_INV permet de simplifier
l’écriture et le débogage de Legolas++.
50. Les types TX et TB peuvent être différents puisque TB peut par exemple être une expression vectorielle.
156
C.2. Les solveurs
1 class S y m m e t r i c B a n d e d G a u s s S e i d e l A l g o r i t h m {
2 public :
3 template < class TA , class TX , class TB >
4 class Engine : public Legolas :: LinearSolver < TA , TX , TB > {
5 private :
6 typedef typename TX :: Element XElement ;
7 typedef typename TB :: Element BElement ;
8 typedef typename TA :: ConstGetElement AElement ;
9 typedef typename AElement :: template SolverEngine <
10 XElement , BElement >:: Solver BlockSolver ;
11 XElement accu_ ; // accumulateur utilise par l ’ algorithme
12 int nbSolvers_ ; // nombre de blocs sur la diagonale de A
13 BlockSolver * blockSolvers_ ; // tableau de sous - solveurs
14 TX B_estimate_ ; // utilise pour calculer l ’ erreur relative
15
16 public :
17 inline Engine ( const TA & A , TX & X , const TB & B )
18 : ... // allocation et initialisation des membres donnees
19 { ... }
20
21 ~ Engine () { ... /* desallocation des membres donnees */ }
22
23 inline void solve ( const TA & A , TX & X , const TB & B )
24 {
25 double relative_error ;
26 do {
27 for ( int i = 0 ; i < A . nrows () ; ++ i ) {
28 int minj = std :: max (0 , i - A . lsup () ) ;
29 int maxj = std :: min ( nrows , i + A . lsup () +1) ;
30 accu_ = B [ i ];
31 for ( int j = minj ; j < i ; ++ j )
32 accu_ -= A . lowe rBan dedG etEle ment (i , j ) * X [ j ];
33 for ( int j = i +1 ; j < maxj ; ++ j )
34 accu_ -= A . lowe rBan dedG etEle ment (j , i ) * X [ j ];
35 // resolution du systeme Ai,i Xi = accu_
36 blockSolvers_ [ i ]. solve ( A . lowe rBan dedG etEle ment (i , i ) ,X [ i ] , accu_ ) ;
37 }
38 B_estimate_ = A * X ;
39 relative_error = dot ( B_estimate_ -B , B_estimate_ - B ) / dot (B , B ) ;
40 } while ( relative_error > 5. e -6) ;
41 }
42
43 inline void transposeSolve ( const TA & A , TX & X , const TB & B )
44 { solve (A , X , B ) ; } // A est symetrique
45 };
46 };
Le type des sous-solveurs est déterminé à la ligne 10. Ces sous-solveurs sont ensuite initialisés
dans le constructeur et utilisés à la ligne 36 pour résoudre les sous-systèmes correspondant aux
termes diagonaux de la matrice A. Ces sous-solveurs utilisent l’algorithme défini dans les options
des sous-matrices. De cette manière, il est possible de construire récursivement un algorithme
de résolution complexe, capable d’exploiter de manière optimale la structure d’une matrice. Il
157
Annexe C. Définitions Legolas++
est également possible de changer simplement l’algorithme utilisé à un niveau sans toucher aux
autres niveaux ; ce qui peut être extrêmement compliqué avec d’autres approches.
158
Glossaire
AVX (Advanced Vector eXtensions) : extension SIMD du jeu d’instructions X86 qui succède
aux unités SSE. Les unités AVX traitent des paquets de données de 256 bits. elles
permettent donc d’exécuter simultanément huit opérations flottantes en simple précision
ou quatre en double précision.
COCAGNE : nouvelle chaîne de calcul visant à simuler le cœur des réacteurs nucléaire d’EDF.
COCAGNE doit succéder à COCCINELLE.
COCCINELLE : chaîne de calcul de cœur actuellement utilisée pour simuler le cœur des
réacteurs nucléaire d’EDF.
CUDA C/C++ : extension aux langages C et C++ proposée par NVIDIA et permettant la
programmation de GPUs.
INTEL TBB (Threading Building Blocks) : bibliothèque générique C++ proposée par
INTEL et permettant la parallélisation d’une application sur plusieurs threads.
159
Glossaire
160
Bibliographie
161
Bibliographie
[18] S. Marguet, La physique des réacteurs nucléaires. Lavoisier, 2011. Cité pages 3 et 4.
[19] F. Hoareau, « COCAGNE : impact des éléments finis RTk différents par direction sur les calculs
crayon par crayon 3D ». Note Interne no CR-I27-2009-77, EDF R&D, 2009. Cité page 3.
[20] M. Barrault, « Performances de la version 1.0.3 de cocagne (séquentiel) sur deux stations de
travail ». Compte Rendu interne no CR-I23-2010-033, EDF R&D, 2010. Cité page 3.
[21] E. Gelbard, « Simplified Spherical Harmonics Equations and Their Use in Shielding Problems ».
Rapport technique no WAPD-T-1182 (Rev. 1), Westinghouse Electric Corp. Bettis Atomic Power
Lab., Pittsburgh, 1961. Cité pages 4 et 93.
[22] E. Lewis et W. Miller Jr, Computational methods of neutron transport. American Nuclear
Society, 1993. Cité page 4.
[23] B. Lathuilière, Méthode de décomposition de domaine pour les équations du transport simplifié
en neutronique. Thèse de doctorat, LaBRI, Université Bordeaux I, Talence, France, janvier 2010.
Cité pages 4 et 93.
[24] P. Guérin, Méthodes de décomposition de domaine pour la formulation mixte duale du problème
critique de la diffusion des neutrons. Thèse de doctorat, Université Paris VI, 2007. Cité page 4.
[25] B. Carlson, « Solution of the transport equations by sn approximations ». Rapport technique
no LA-1599, Los Alamos, 1953. Cité page 4.
[26] J. Askew, « A charasteristic formulation of the neutron transport equation in complicated geome-
tries », no AEEW-M 1108, 1972. Cité page 4.
[27] M. Halsall, « CACTUS, a charasteristic solution to the neutron transport equation in compli-
cated geometries ». Rapport technique no AEEW-R 1291, Atomic Energy Establishment, Winfrith,
Dorchester, Dorcet, United Kingdom, 1980. Cité page 4.
[28] S. Hong et N. Cho, « Crx : A code for rectangular and hexagonal lattices based on the method
of characteristics », Annals of Nuclear Energy, vol. 25, no 8, 1998, p. 547–565. Cité page 4.
[29] F. Févotte, Techniques de traçage pour la méthode des caractéristiques appliquée à la résolution
de l’équation du transport des neutrons en domaines multi-dimensionnels. Thèse de doctorat,
Université Paris Sud - Paris XI, Orsay, France, 2009. Cité page 4.
[30] H. Joo, J. Cho, K. Kim et al., « Methods and performance of a three-dimensional whole-core
transport code decart », dans Proceedings of PHYSOR 2004 - The Physics of Fuel Cycles and
Advanced Nuclear Systems : Global Developments, Chicago, Illinois, USA, April 2009.
Cité page 4.
[31] M. Dahmani et R. Roy, « Parallel solver based on the three-dimensional characteristics method :
Design and performance analysis », Nuclear Science and Engineering, vol. 150, no 2, 2005.
Cité page 4.
[32] M. Smith, A. Marin-Lafleche, W. Yang et al., « Method of Characteristics Development
Targeting the High Performance Blue Gene/P Computer at Argonne National Laboratory », dans
Proceedings of International Conference on Mathematics and Computational Methods Applied to
Nuclear Science and Engineering (M&C 2011), Rio de Janeiro, RJ, Brazil, May 2011.
Cité page 4.
[33] L. Plagne et A. Ponçot, « Generic Programming for Deterministic Neutron Transport Codes »,
dans Proceedings of Mathematics and Computation, Supercomputing, Reactor Physics and Nuclear
and Biological Applications, Palais des Papes, Avignon, France, September 2005.
Cité pages 4, 22, 24, 93 et 107.
[34] W. Kirschenmann, L. Plagne, S. Ploix et al., « Massively Parallel Solving of 3D Simplified
PN Equations on Graphic Processing Units », dans Proceedings of Mathematics, Computational
Methods & Reactor Physics, Saratoga Springs, New York, USA, May 2009.
Cité pages 4, 28 et 52.
[35] M. Barrault, B. Lathuilière, P. Ramet et J. Roman, « A Non Overlapping Parallel Domain
Decomposition Method Applied to The Simplified Transport Equations », dans Proceedings of
Mathematics, Computational Methods & Reactor Physics, Saratoga Springs, New York, USA, May
2009. Cité page 4.
162
[36] W. Kirschenmann, L. Plagne et S. Vialle, « Parallel SPN on Multi-Core CPUs and Many-
Core GPUs », Transport Theory and Statistical Physics, vol. 39, no 2, 2010, p. 255–281.
Cité pages 4, 27 et 64.
[37] M. Barrault, B. Lathuilière, P. Ramet et J. Roman, « Efficient parallel resolution of the
simplified transport equations in mixed-dual formulation », Journal of Computational Physics, vol.
230, no 5, 2011, p. 2004 – 2020. Cité page 4.
[38] F. Blanchon et J. Planchard, « Méthodes numériques utilisées dans le code de cinétique
neutronique coccinelle ». Note Interne no HI/4109-07, EDF R&D, mars 1982. Cité page 4.
[39] J. Texeraud et M. Hypolite, « Cahier des charges négocié contractuel coccinelle v3.10 ». Note
Interne no HI27-2010-097, EDF R&D, 2010. Cité page 4.
[40] C. L. Lawson, « Background, Motivation, and a Retrospective View of the BLAS ». Rapport
technique, SIAM, 1999. Cité page 6.
[41] C. L. Lawson, R. J. Hanson et F. T. Krogh, « A proposal for standard linear algebra subpro-
grams », ACM SIGNUM Newsletter, vol. 8, 1973. Cité page 6.
[42] C. L. Lawson, R. J. Hanson, D. R. Kincaid et F. T. Krogh, « Basic linear algebra subpro-
grams for fortran usage », ACM Trans. Math. Softw., vol. 5, September 1979, p. 308–323.
Cité page 6.
[43] J. J. Dongarra et S. C. Eisenstat, « Squeezing the most out of an algorithm in cray fortran »,
ACM Trans. Math. Softw., vol. 10, August 1984, p. 219–230. Cité pages 6 et 10.
[44] J. J. Dongarra, J. D. Croz, S. Hammarling et R. J. Hanson, « A proposal for an extended
set of fortran basic linear algebra subprograms », SIGNUM Newsl., vol. 20, January 1985, p. 2–18.
Cité page 6.
[45] J. J. Dongarra, J. Du Croz, S. Hammarling et R. J. Hanson, « An extended set of fortran
basic linear algebra subprograms », ACM Trans. Math. Softw., vol. 14, March 1988, p. 1–17.
Cité page 6.
[46] J. J. Dongarra, J. Du Croz, I. Duff et S. Hammarling, « A proposal for a set of level 3 basic
linear algebra subprograms », SIGNUM Newsl., vol. 22, July 1987, p. 2–14. Cité pages 6 et 136.
[47] J. J. Dongarra, J. Du Croz, S. Hammarling et I. S. Duff, « A set of level 3 basic linear
algebra subprograms », ACM Trans. Math. Softw., vol. 16, March 1990, p. 1–17. Cité page 6.
[48] J. J. Dongarra, « Preface : Basic Linear Algebra Subprograms Technical (Blast) Forum Standard
I », Int. J. High Performance Computing Applications, vol. 16, no 1, Spring 2002, p. 1–111.
Cité page 6.
[49] J. J. Dongarra, « Preface : Basic Linear Algebra Subprograms Technical (Blast) Forum Standard
II », Int. J. High Performance Computing Applications, vol. 16, no 2, Summer 2002, p. 115–199.
Cité page 6.
[50] INTEL, Math Kernel Library (MKL) Documentation, version 10.3. Cité page 7.
[51] AMD, Math Kernel Library (ACML) Documentation – Version 4.4.0. Cité page 7.
[52] IBM, ESSL for AIX V5.1 – ESSL for Linux on POWER V5.1 – Guide and Reference.
Cité page 7.
[53] R. Whaley et J. J. Dongarra, « Automatically tuned linear algebra software », dans Proceed-
ings of the 1998 ACM/IEEE conference on Supercomputing, p. 1–27, San Jose, CA, 1998. IEEE
Computer Society. Cité page 7.
[54] K. Goto et R. A. van de Geijn, « Anatomy of high-performance matrix multiplication », ACM
Trans. Math. Softw., vol. 34, no 3, 2008. Cité pages 7, 123 et 137.
[55] K. Goto et R. A. van de Geijn, « High-performance implementation of the level-3 blas », ACM
Trans. Math. Softw., vol. 35, no 1, 2008. Cité pages 7 et 137.
[56] M. Frigo et S. G. Johnson, « The design and implementation of FFTW3 », Proceedings of the
IEEE, vol. 93, no 2, 2005, p. 216–231. Special issue on “Program Generation, Optimization, and
Platform Adaptation”. Cité page 7.
163
Bibliographie
[57] B. Gough, GNU Scientific Library Reference Manual - Third Edition. Network Theory Ltd.,
édition 3rd, 2009. Cité page 7.
[58] Netlib. « Site internet de netlib ». http://www.netlib.org/. Cité page 8.
[59] OpenMP Architecture Review Board, OpenMP Application Program Interface, version 3.0,
2008. Cité pages 8, 38 et 77.
[60] L. Plagne, F. Hülsemann, D. Barthou et J. Jaeger, « Parallel expression template for large
vectors », dans POOSC ’09 : Proceedings of the 8th workshop on Parallel/High-Performance Object-
Oriented Scientific Computing, p. 8, Genova, Italy, 2009. ACM. Cité pages 8 et 150.
[61] J. D. McCalpin, « Memory Bandwidth and Machine Balance in Current High Performance Com-
puters », IEEE Computer Society Technical Committee on Computer Architecture (TCCA) Newslet-
ter, dec 1995, p. 19–25. Cité pages 8 et 62.
[62] S. P. Jones, éditeur, Haskell 98 Language and Libraries : The Revised Report. http ://haskell.org/,
September 2002. Cité page 12.
[63] G. Keller, M. M. Chakravarty, R. Leshchinskiy et al., « Regular, shape-polymorphic, par-
allel arrays in haskell », dans Proceedings of the 15th ACM SIGPLAN international conference on
Functional programming, ICFP ’10, p. 261–272, Baltimore, Maryland, USA, 2010. ACM.
Cité page 12.
[64] P. Narbel, Programmation fonctionnelle, générique et objet : Une introduction avec le langage
OCaml. Vuibert, 2005. Cité page 13.
[65] MathWorks, MATLAB - The Language Of Technical Computing Documentation, version 2009b.
Cité page 13.
[66] S. Consortium, Scilab 5.2 Documentation. Cité page 13.
[67] A. van Deursen, P. Klint et J. Visser, « Domain-specific languages : an annotated bibliogra-
phy », SIGPLAN Not., vol. 35, June 2000, p. 26–36. Cité page 13.
[68] P. Hudak, « Building domain-specific embedded languages », ACM Comput. Surv., vol. 28, De-
cember 1996. Cité page 15.
[69] K. Czarnecki, J. T. Odonnell, J. Striegnitz et al., « DSL Implementation in MetaOCaml,
Template Haskell, and C++ », LNCS : Domain-Specific Program Generation, vol. 3016, no 2, 2004,
p. 51–72. Cité page 15.
[70] P. Hudak, « Modular domain specific languages and tools », dans Proceedings of the 5th Inter-
national Conference on Software Reuse, coll. « ICSR ’98 », p. 134–, Washington, DC, USA, 1998.
IEEE Computer Society. Cité page 15.
[71] J. Falcou, Un Cluster pour la Vision Temps Réel : Architecture, Outils et Applications. Thèse
de doctorat, l’Université Blaise Pascal (Clermont II), Clermont-Ferrand, France, décembre 2006.
Cité pages 16, 17, 71 et 72.
[72] J. Falcou, J. Sérot, L. Pech et J.-T. Lapresté, « Meta-programming applied to automatic
smp parallelization of linear algebra code », dans E. Luque, T. Margalef et D. Benítez,
éditeurs, Euro-Par 2008 – Parallel Processing, vol. 5168 (coll. Lecture Notes in Computer Science),
p. 729–738. Springer Berlin / Heidelberg, 2008. Cité pages 16, 17 et 71.
[73] P. Plauger, M. Lee, D. Musser et A. A. Stepanov, C++ Standard Template Library. Prentice
Hall PTR, Upper Saddle River, NJ, USA, édition 1st, 2000. Cité pages 17, 71, 106 et 147.
[74] ISO, ISO/IEC 14882 :2003 : Programming languages — C++. International Organization for
Standardization, Geneva, Switzerland, 2003. Cité pages 17, 55, 71, 108 et 147.
[75] T. L. Veldhuizen et D. Gannon, « Active libraries : Rethinking the roles of compilers and
libraries », dans SIAM Workshop on Object Oriented Methods for Inter-operable Scientific and
Engineering Computing (OO’98). SIAM, 1998. Cité pages 17 et 147.
[76] K. Czarnecki, U. W. Eisenecker, R. Glück et al., « Generative programming and active
libraries », dans Selected Papers from the International Seminar on Generic Programming, p. 25–
39, London, UK, 2000. Springer-Verlag. Cité pages 17 et 147.
164
[77] T. L. Veldhuizen, Active libraries and universal languages. Thèse de doctorat, Indianapolis, IN,
USA, 2004. AAI3134053. Cité pages 17 et 147.
[78] T. L. Veldhuizen, « Arrays in Blitz++ », dans ISCOPE ’98 : Proceedings of the Second Interna-
tional Symposium on Computing in Object-Oriented Parallel Environments, p. 223–230, London,
UK, 1998. Springer-Verlag. Cité pages 17, 103 et 147.
[79] J. O. Coplien, « Curiously recurring template patterns », C++ Rep., vol. 7, February 1995, p. 24–
27. Cité pages 17 et 147.
[80] J. O. Coplien, Curiously recurring template patterns, p. 135–144. New York, NY, USA, SIGS
Publications, Inc., 1996. Cité pages 17 et 147.
[81] M. A. Heroux, R. A. Bartlett, V. E. Howle et al., « An overview of the Trilinos project »,
ACM Trans. Math. Softw., vol. 31, no 3, 2005, p. 397–423. Cité pages 17 et 76.
[82] C. G. Baker, H. Carter Edwards, M. A. Heroux et A. B. Williams, « A light-weight
API for Portable Multicore Programming », dans PDP 2010 : Proceedings of The 18th Euromicro
International Conference on Parallel, Distributed and Network-Based Computing, Washington, DC,
USA, 2010. IEEE Computer Society. Cité pages 17 et 76.
[83] S. Blackford, G. Corliss, J. Demmel et al., « Basic linear algebra subprograms technical
forum standard », International Journal of High Performance Applications and Supercomputing,
vol. 16, no 1, Spring 2002. Cité pages 18 et 19.
[84] G. M. Morton, « A computer oriented geodetic data base ; and a new technique in file sequenc-
ing ». Rapport technique, IBM Canada Ltd., Ottawa, Canada, 1966. Cité pages 19 et 106.
[85] P. Gottschling, D. S. Wise et M. D. Adams, « Representation-transparent matrix algorithms
with scalable performance », dans Proceedings of the 21st annual international conference on Su-
percomputing, coll. « ICS ’07 », p. 116–125, Seattle, Washington, 2007. ACM. Cité page 19.
[86] J. Herrero et J. Navarro, « Using non-canonical array layouts in dense matrix operations »,
dans B. Kågström, E. Elmroth, J. J. Dongarra et J. Wasniewski, éditeurs, Applied Parallel
Computing. State of the Art in Scientific Computing, vol. 4699 (coll. Lecture Notes in Computer
Science), p. 580–588. Springer Berlin / Heidelberg, 2007. Cité page 19.
[87] J. J. Dongarra, « Sparse Matrix Storage Formats (section 10.2) », dans Bai et al. [88], p. 315–
319. Cité page 19.
[88] Z. Bai, J. Demmel, J. J. Dongarra et al., éditeurs, Templates for the solution of algebraic
eigenvalue problems : a practical guide. SIAM, Philadelphia, 2000. Cité pages 19 et 165.
[89] P. Hénon, P. Ramet et J. Roman, « PaStiX : A High-Performance Parallel Direct Solver for
Sparse Symmetric Definite Systems », Parallel Computing, vol. 28, no 2, janvier 2002, p. 301–321.
Cité page 21.
[90] F. Pellegrini et J. Roman, « Sparse matrix ordering with Scotch », dans Proceedings of
HPCN’97, Vienna, LNCS 1225, p. 370–378, avril 1997. Cité page 21.
[91] G. Karypis et V. Kumar, « A parallel algorithm for multilevel graph partitioning and sparse
matrix ordering », J. Parallel Distrib. Comput., vol. 48, no 1, 1998, p. 71–95. Cité page 21.
[92] S. Balay, W. D. Gropp, L. C. McInnes et B. F. Smith, « Efficient management of parallelism
in object oriented numerical software libraries », dans E. Arge, A. M. Bruaset et H. P. Lang-
tangen, éditeurs, Modern Software Tools in Scientific Computing, p. 163–202. Birkhäuser Press,
1997. Cité page 22.
[93] S. Balay, J. Brown, et al., « PETSc users manual ». Rapport technique no ANL-95/11 - Revision
3.1, Argonne National Laboratory, 2010. Cité page 22.
[94] S. Balay, J. Brown, K. Buschelman et al. « PETSc Web page », 2011.
http ://www.mcs.anl.gov/petsc. Cité page 22.
[95] M. Benzi, G. H. Golub et J. Liesen, « Numerical solution of saddle point problems », Acta
Numerica, vol. 14, 2005, p. 1–137. Cité page 23.
[96] G. H. Golub et C. F. Van Loan, Matrix computations (3rd ed.). Johns Hopkins University Press,
Baltimore, MD, USA, 1996. Cité pages 27, 107 et 156.
165
Bibliographie
166
[117] NVIDIA, « NVIDIA’s Next Generation CUDA Compute Architecture : Fermi ». Whitepaper,
NVIDIA corp., 2009. Cité pages 35, 41, 42, 43 et 44.
[118] M. Faverge, Ordonnancement hybride statique-dynamique en algèbre linéaire creuse pour de
grands clusters de machines NUMA et multi-coeurs. Thèse de doctorat, LaBRI, Université Bor-
deaux I, Talence, Talence, France, 2009. Cité page 35.
[119] M. Faverge et P. Ramet, « A NUMA aware scheduler for a parallel sparse direct solver », Parallel
Computing, 2009. Submitted. Cité page 35.
[120] S. Weiss et J. E. Smith, « Instruction issue logic for pipelined supercomputers », SIGARCH
Comput. Archit. News, vol. 12, January 1984, p. 110–118. Cité page 35.
[121] R. J. Fisher et H. G. Dietz, « Compiling for simd within a register », dans Proceedings of the
11th International Workshop on Languages and Compilers for Parallel Computing, coll. « LCPC
’98 », p. 290–304, London, UK, 1999. Springer-Verlag. Cité page 36.
[122] D. Koufaty et D. Marr, « Hyperthreading technology in the netburst microarchitecture », Micro,
IEEE, vol. 23, no 2, 2003, p. 56–65. Cité page 38.
[123] J. Nickolls et W. J. Dally, « The gpu computing era », IEEE Micro, vol. 30, March 2010,
p. 56–69. Cité page 40.
[124] S. Upstill, RenderMan Companion : A Programmer’s Guide to Realistic Computer Graphics.
Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1989. Cité page 40.
[125] Pixar, The RenderMan Interface, édition version 3.1 specification, September 1989.
Cité page 40.
[126] A. A. Apodaca et M. W. Mantle, « Renderman : Pursuing the future of graphics », IEEE
Comput. Graph. Appl., vol. 10, July 1990, p. 44–49. Cité page 40.
[127] M. Henne, H. Hickel, E. Johnson et S. Konishi, « The making of Toy Story », dans COM-
PCON ’96. Technologies for the Information Superhighway. Forty-First IEEE Computer Society
International Conference., p. 463–468, Seattle, Washington, 1996. IEEE Comput. Soc. Press. Los
Alamitos, CA, USA. Cité page 40.
[128] OpenGL Architecture Review Board, OpenGL reference manual : the official reference doc-
ument for OpenGL, release 1. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA,
1992. Cité page 40.
[129] H. Yoshizawa, T. Otsuka et S. Sasaki, « High-Performance Architecture of a Next-Generation
3D-CG Rendering Processor. », dans Proceedings of ICSPAT’95, p. 195–199, 1995.
Cité page 40.
[130] T. Mitra et T.-c. Chiueh, « A breadth-first approach to efficient mesh traversal », dans Proceed-
ings of the ACM SIGGRAPH/EUROGRAPHICS workshop on Graphics hardware, coll. « HWWS
’98 », p. 31–38, New York, NY, USA, 1998. ACM. Cité page 40.
[131] M. S. Peercy, M. Olano, J. Airey et P. J. Ungar, « Interactive multi-pass programmable
shading », dans Proceedings of the 27th annual conference on Computer graphics and interactive
techniques, coll. « SIGGRAPH ’00 », p. 425–432, New York, NY, USA, 2000. ACM Press/Addison-
Wesley Publishing Co. Cité page 41.
[132] M. Pharr et R. Fernando, GPU Gems 2 : Programming Techniques for High-Performance
Graphics and General-Purpose Computation (Gpu Gems). Addison-Wesley Professional, 2005.
Cité page 41.
[133] NVIDIA, NVIDIA CUDA C Programming Guide 4.0, 2011.
Cité pages 41, 42, 43, 45, 55, 79 et 122.
[134] S. Collange, Enjeux de conception des architectures GPGPU : unités arithmétiques spécialisées
et exploitation de la régularité. Thèse de doctorat, Université de Perpignan Via Domitia, Perpignan,
France, novembre 2010. Cité page 42.
[135] D. M. Tullsen, S. J. Eggers et H. M. Levy, « Simultaneous multithreading : maximizing
on-chip parallelism », SIGARCH Comput. Archit. News, vol. 23, May 1995, p. 392–403.
Cité page 42.
167
Bibliographie
[136] H. Hirata, K. Kimura, S. Nagamine et al., « An elementary processor architecture with si-
multaneous instruction issuing from multiple threads », SIGARCH Comput. Archit. News, vol. 20,
April 1992, p. 136–145. Cité page 43.
[137] Mark Harris. « Optimizing CUDA », Novembre 2007. Tutoriel CUDA présenté à SuperCom-
puting 2007 (SC’07). Cité pages 45, 49 et 52.
[138] W. J. Bolosky et M. L. Scott, « False sharing and its effect on shared memory performance »,
dans USENIX Systems on USENIX Experiences with Distributed and Multiprocessor Systems -
Volume 4, p. 3–3, Berkeley, CA, USA, 1993. USENIX Association. Cité page 48.
[139] J. Reinders, INTEL threading building blocks. O’Reilly & Associates, Inc., Sebastopol, CA, USA,
2007. Cité pages 52, 76, 103 et 150.
[140] Y. Magda, Visual C++ Optimization with Assembly Code. A-List Publishing, 2004.
Cité page 52.
[141] A. E. Eichenberger, J. K. O’Brien, K. M. O’Brien et al., « Using advanced compiler tech-
nology to exploit the performance of the cell broadband enginetm architecture », IBM Syst. J., vol.
45, January 2006, p. 59–84. Cité page 52.
[142] M. Hassaballah, S. Omran et Y. B. Mahdy, « A review of simd multimedia extensions and
their usage in scientific and engineering applications », Comput. J., vol. 51, November 2008, p. 630–
649. Cité page 52.
[143] C. H. Q. Ding, « An optimal index reshuffle algorithm for multidimensional arrays and its ap-
plications for parallel architectures », IEEE Trans. Parallel Distrib. Syst., vol. 12, March 2001,
p. 306–315. Cité page 52.
[144] D. Fraser, « Array permutation by index-digit permutation », J. ACM, vol. 23, April 1976,
p. 298–309. Cité page 52.
[145] A. Edelman, S. Heller et S. L. Johnsson, « Index transformation algorithms in a linear algebra
framework », IEEE Trans. Parallel Distrib. Syst., vol. 5, December 1994, p. 1302–1309.
Cité page 52.
[146] S. Lennart Johnsson et C.-T. Ho, « Algorithms for matrix transposition on boolean n-cube
configured ensemble architecture », SIAM J. Matrix Anal. Appl., vol. 9, November 1988, p. 419–454.
Cité page 52.
[147] M. Bader, H.-J. Bungartz, D. Mudigere et al., « Fast GPGPU data rearrangement kernels
using CUDA », ArXiv e-prints, novembre 2010. Cité page 52.
[148] P. Lascaux et R. Théodor, Analyse numérique matricielle appliquée à l’art de l’ingénieur vol 1
méthodes directes. Masson, 2ème édition, 1998. Cité page 53.
[149] G. H. Golub et C. F. Van Loan, Matrix computations (3rd ed.). Johns Hopkins University Press,
Baltimore, MD, USA, 3ème édition, 1996. Cité page 53.
[150] J. Falcou et J. Sérot, « EVE, an Object Oriented SIMD Library », dans M. Bubak, G. D. v.
Albada, P. M. A. Sloot et J. J. Dongarra, éditeurs, Computational Science - ICCS 2004, vol.
3038 (coll. Lecture Notes in Computer Science), p. 314–321. Springer Berlin / Heidelberg, 2004.
Cité pages 60 et 71.
[151] INTEL, INTEL C++ Compiler 12.0 User and Reference Guides. Cité page 60.
[152] J. D. McCalpin, « Stream : Sustainable memory bandwidth in high performance computers ».
Rapport technique, University of Virginia, Charlottesville, Virginia, 1991-2007. A continually up-
dated technical report. http ://www.cs.virginia.edu/stream/. Cité page 62.
[153] M. E. Thomadakis, « The Architecture of the Nehalem Processor and Nehalem-EP SMP Plat-
forms ». Rapport technique, Texas A&M University, 2010. Cité page 63.
[154] INTEL, « Intel 64 and ia-32 architectures software developer’s manual volume 1 : Basic architec-
ture ». Rapport technique, 2010. Cité page 63.
[155] INTEL, « Intel 64 and ia-32 architectures software developer’s manual volumes 2a and 2b : In-
struction set reference, a-z ». Rapport technique, 2010. Cité page 63.
168
[156] Linley Gwennap, « Sandy Bridge Spans Generations », Processor Watch, no 328, 2010.
Cité pages 63 et 136.
[157] NVIDIA, CUDA CUBLAS library 4.0, 2011. Cité page 63.
[158] INTEL, INTEL VTune Amplifier XE, 2011. Cité page 66.
[159] D. R. Butenhof, Programming with POSIX threads. Addison-Wesley Longman Publishing Co.,
Inc., Boston, MA, USA, 1997. Cité page 71.
[160] E. A. Lee, « The Problem with Threads ». Rapport technique no UCB/EECS-2006-1, EECS
Department, University of California, Berkeley, Jan 2006. The published version of this paper is in
IEEE Computer 39(5) :33-42, May 2006. Cité page 71.
[161] H.-J. Boehm, « Threads cannot be implemented as a library », SIGPLAN Not., vol. 40, June 2005,
p. 261–268. Cité page 71.
[162] S. Savage, M. Burrows, G. Nelson et al., « Eraser : a dynamic data race detector for multi-
threaded programs », ACM Trans. Comput. Syst., vol. 15, November 1997, p. 391–411.
Cité page 71.
[163] H. Jula, D. Tralamazza, C. Zamfir et G. Candea, « Deadlock immunity : enabling systems to
defend against deadlocks », dans Proceedings of the 8th USENIX conference on Operating systems
design and implementation, OSDI’08, p. 295–308, San Diego, California, 2008. USENIX Associa-
tion. Cité page 71.
[164] K. Serebryany et T. Iskhodzhanov, « Threadsanitizer : data race detection in practice », dans
Proceedings of the Workshop on Binary Instrumentation and Applications, coll. « WBIA ’09 »,
p. 62–71, New York, New York, 2009. ACM. Cité page 71.
[165] A. Jannesari et W. F. Tichy, « On-the-fly race detection in multi-threaded programs », dans
Proceedings of the 6th workshop on Parallel and distributed systems : testing, analysis, and debug-
ging, coll. « PADTAD ’08 », p. 6 :1–6 :10, Seattle, Washington, 2008. ACM. Cité page 71.
[166] A. Mühlenfeld et F. Wotawa, « Fault Detection in Multi-Threaded C++ Server Applications »,
Electronic Notes in Theoretical Computer Science, vol. 174, no 9, 2007, p. 5 – 22. Cité page 71.
[167] D. D. Chamberlin et R. F. Boyce, « Sequel : A structured english query language », dans
Proceedings of the 1974 ACM SIGFIDET (now SIGMOD) workshop on Data description, access
and control, coll. « SIGFIDET ’74 », p. 249–264, Ann Arbor, Michigan, 1974. ACM.
Cité page 71.
[168] E. Meijer, B. Beckman et G. Bierman, « Linq : reconciling object, relations and xml in the .net
framework », dans Proceedings of the 2006 ACM SIGMOD international conference on Management
of data, coll. « SIGMOD ’06 », p. 706–706, New York, NY, USA, 2006. ACM. Cité page 71.
[169] P. Pialorsi et M. Russo, Introducing microsoft linq. Microsoft Press, Redmond, WA, USA,
édition first, 2007. Cité page 71.
[170] Y. Yu, M. Isard, D. Fetterly et al., « DryadLINQ : a system for general-purpose distributed
data-parallel computing using a high-level language », dans Proceedings of the 8th USENIX con-
ference on Operating systems design and implementation, coll. « OSDI’08 », p. 1–14, San Diego,
California, 2008. USENIX Association. Cité page 71.
[171] M. Isard et Y. Yu, « Distributed data-parallel computing using a high-level programming lan-
guage », dans Proceedings of the 35th SIGMOD international conference on Management of data,
coll. « SIGMOD ’09 », p. 987–994, Providence, Rhode Island, USA, 2009. ACM. Cité page 71.
[172] M. Isard, M. Budiu, Y. Yu et al., « Dryad : distributed data-parallel programs from sequential
building blocks », SIGOPS Oper. Syst. Rev., vol. 41, March 2007, p. 59–72. Cité page 71.
[173] M. Isard, M. Budiu, Y. Yu et al., « Dryad : distributed data-parallel programs from sequential
building blocks », dans Proceedings of the 2nd ACM SIGOPS/EuroSys European Conference on
Computer Systems 2007, coll. « EuroSys ’07 », p. 59–72, Lisbon, Portugal, 2007. ACM.
Cité page 71.
[174] Querydsl. « Site internet de querydsl ». http://www.querydsl.com/. Cité page 71.
169
Bibliographie
[175] LINQ++. « LINQ++ : An embeded dsl for c++ (site internet de LINQ++) ». https://github.
com/hjiang/linqxx/wiki. Cité page 71.
[176] T. Faison, Event-Based Programming : Taking Events to the Limit. Apress, Berkely, CA, USA,
2006. Cité page 73.
[177] J. Armstrong et S. Virding, « Erlang - an experimental telephony programming language »,
dans XIII International Switching Symposium, p. 43 – 48, 1990. Cité page 74.
[178] R. Virding, C. Wikström et M. Williams, Concurrent programming in ERLANG (2nd ed.).
Prentice Hall International (UK) Ltd., Hertfordshire, UK, UK, 1996. Cité page 74.
[179] L. V. Kale et S. Krishnan, « Charm++ : a portable concurrent object oriented system based on
C++ », dans Proceedings of the eighth annual conference on Object-oriented programming systems,
languages, and applications, coll. « OOPSLA ’93 », p. 91–108, Washington, D.C., United States,
1993. ACM. Cité page 74.
[180] L. V. Kale et S. Krishnan, « Charm++ : a portable concurrent object oriented system based
on C++ », SIGPLAN Not., vol. 28, October 1993, p. 91–108. Cité page 74.
[181] D. Kunzman, « Charm++ on the Cell Processor ». Rapport de DÉA, Dept. of Computer Science,
University of Illinois, 2006. Cité page 74.
[182] D. Kunzman, G. Zheng, E. Bohm et L. V. Kalé, « Charm++, Offload API, and the Cell
Processor », dans Proceedings of the Workshop on Programming Models for Ubiquitous Parallelism,
Seattle, WA, USA, September 2006. Cité page 74.
[183] L. Wesolowski, « An application programming interface for general purpose graphics processing
units in an asynchronous runtime system ». Rapport de DÉA, Dept. of Computer Science, University
of Illinois, 2008. Cité page 74.
[184] M. Cole, Algorithmic skeletons : structured management of parallel computation. MIT Press,
Cambridge, MA, USA, 1991. Cité page 74.
[185] H. González-Vélez et M. Leyton, « A survey of algorithmic skeleton frameworks : high-level
structured parallel programming enablers », Softw. Pract. Exper., vol. 40, November 2010, p. 1135–
1160. Cité pages 74 et 75.
[186] E. Gamma, R. Helm, R. Johnson et J. Vlissides, Design patterns : elements of reusable object-
oriented software. Addison-Wesley Professional, 1995. Cité page 74.
[187] S. MacDonald, D. Szafron, J. Schaeffer et S. Bromling, « Generating parallel program
frameworks from parallel design patterns », dans Euro-Par ’00 : Proceedings from the 6th Interna-
tional Euro-Par Conference on Parallel Processing, p. 95–104, London, UK, 2000. Springer-Verlag.
Cité page 75.
[188] J. Falcou, J. Sérot, T. Chateau et J.-T. Lapresté, « Quaff : efficient C++ design for parallel
skeletons », Parallel Computing, vol. 32, no 7-8, 2006, p. 604–615. Cité page 75.
[189] M. D. McCool, « Structured parallel programming with deterministic patterns », dans Proceedings
of the 2nd USENIX conference on Hot topics in parallelism, coll. « HotPar’10 », p. 5–5, Berkeley,
CA, 2010. USENIX Association. Cité page 75.
[190] E. W. Dijkstra, « Go To statement considered harmful », Comm. ACM, vol. 11, no 3, 1968,
p. 147–148. Letter to the Editor. Cité page 75.
[191] L. Bougé, « The data-parallel programming model : A semantic perspective », dans A. Darte et
G.-R. Perrin, éditeurs, The Data-Parallel Programming Model, vol. 1132 (coll. LNCS Tutorial),
p. 4–26. Springer Verlag, juin 1996. Invited Conference. Cité page 75.
[192] J. Dean et S. Ghemawat, « MapReduce : simplified data processing on large clusters », dans
Proceedings of the 6th conference on Symposium on Opearting Systems Design & Implementation
- Volume 6, p. 10–10, San Francisco, CA, 2004. USENIX Association. Cité pages 75 et 91.
[193] J. Dean et S. Ghemawat, « MapReduce : simplified data processing on large clusters », Commun.
ACM, vol. 51, January 2008, p. 107–113. Cité page 75.
[194] G. H. Botorog et H. Kuchen, « Efficient high-level parallel programming », Theoretical Com-
puter Science, vol. 196, no 1-2, 1998, p. 71 – 107. Cité page 75.
170
[195] B. Bacci, M. Danelutto, S. Orlando et al., « p3 l : A structured high-level parallel language,
and its structured support », Concurrency - Practice and Experience, vol. 7, no 3, 1995, p. 225–255.
Cité page 75.
[196] R. Loogen, Y. Ortega-mallén et R. Peña marí, « Parallel functional programming in Eden »,
J. Funct. Program., vol. 15, May 2005, p. 431–475. Cité page 75.
[197] L. Rauchwerger, F. Arzu et K. Ouchi, « Standard Templates Adaptive Parallel Library
(stapl) », dans Selected Papers from the 4th International Workshop on Languages, Compilers,
and Run-Time Systems for Scalable Computers, coll. « LCR ’98 », p. 402–409, London, UK, 1998.
Springer-Verlag. Cité page 76.
[198] P. An, A. Jula, S. Rus et al., « STAPL : an adaptive, generic parallel C++ library », dans
Proceedings of the 14th international conference on Languages and compilers for parallel computing,
coll. « LCPC’01 », p. 193–208, Cumberland Falls, KY, USA, 2003. Springer-Verlag.
Cité page 76.
[199] A. Buss, Harshvardhan, I. Papadopoulos et al., « STAPL : Standard Template Adaptive
Parallel Library », dans Proceedings of the 3rd Annual Haifa Experimental Systems Conference,
coll. « SYSTOR ’10 », p. 14 :1–14 :10, Haifa, Israel, 2010. ACM. Cité page 76.
[200] N. Thomas, G. Tanase, O. Tkachyshyn et al., « A framework for adaptive algorithm selection
in STAPL », dans Proceedings of the tenth ACM SIGPLAN symposium on Principles and practice
of parallel programming, coll. « PPoPP ’05 », p. 277–288, Chicago, IL, USA, 2005. ACM.
Cité page 76.
[201] A. Buss, A. Fidel, Harshvardhan et al. « The STAPL pView », 2010. Cité pages 76 et 90.
[202] J. Hoberock et N. Bell. Thrust : http ://code.google.com/p/thrust/. Cité page 76.
[203] J. Enmyren et C. W. Kessler, « SkePU : a multi-backend skeleton programming library for
multi-GPU systems », dans Proceedings of the fourth international workshop on High-level parallel
programming and applications, coll. « HLPP ’10 », p. 5–14, Baltimore, Maryland, USA, 2010.
Cité page 76.
[204] D. V. Dyk, M. Geveler, S. Mallach et al., « HONEI : A collection of libraries for numerical
computations targeting multiple processor architectures », Computer Physics Communications, vol.
180, no 12, 2009, p. 2534–2543. Cité page 76.
[205] M. Süß et C. Leopold, « A user’s experience with parallel sorting and OpenMP », dans Pro-
ceedings of the Sixth European Workshop on OpenMP - EWOMP’04, p. 23–38, Stockholm, 2004.
Cité page 77.
[206] A. Basumallik, S.-J. Min et R. Eigenmann, « Towards OpenMP execution on software dis-
tributed shared memory systems », dans H. Zima, K. Joe, M. Sato et al., éditeurs, High Perfor-
mance Computing, vol. 2327 (coll. Lecture Notes in Computer Science), p. 357–362. Springer Berlin
/ Heidelberg, 2006. Cité page 77.
[207] D. Millot, A. Muller, C. Parrot et F. Silber-Chaussumier, « Step : A distributed OpenMP
for coarse-grain parallelism tool », dans R. Eigenmann et B. de Supinski, éditeurs, OpenMP in
a New Era of Parallelism, vol. 5004 (coll. Lecture Notes in Computer Science), p. 83–99. Springer
Berlin / Heidelberg, 2008. Cité page 77.
[208] M. Sato, S. Satoh, K. Kusano et Y. Tanaka, « Design of OpenMP compiler for an SMP
cluster », dans Proceedings of First European Workshop on OpenMP (EWOMP), Lund, Sweden,
1999. Cité page 77.
[209] A. Basumallik et R. Eigenmann, « Towards automatic translation of OpenMP to MPI », dans
Proceedings of the 19th annual international conference on Supercomputing, coll. « ICS ’05 », p. 189–
198, Cambridge, Massachusetts, 2005. ACM. Cité page 77.
[210] D. Margery, G. Vallée, R. Lottiaux et al., « Kerrighed : A SSI Cluster OS Running
OpenMP », dans Proceeding of the Fifth European Workshop on OpenMP (EWOMP‘03), 2003.
Cité page 77.
171
Bibliographie
[211] S. Lee, S.-J. Min et R. Eigenmann, « Openmp to gpgpu : a compiler framework for automatic
translation and optimization », SIGPLAN Not., vol. 44, February 2009, p. 101–110.
Cité page 77.
[212] E. Ayguadé, R. M. Badia, F. D. Igual et al., « An extension of the starss programming model
for platforms with multiple gpus », dans Proceedings of the 15th International Euro-Par Conference
on Parallel Processing, coll. « Euro-Par ’09 », p. 851–862, Berlin, Heidelberg, 2009. Springer-Verlag.
Cité page 77.
[213] E. Ayguade, R. M. Badia, D. Cabrera et al., « A proposal to extend the openmp tasking
model for heterogeneous architectures », dans Proceedings of the 5th International Workshop on
OpenMP : Evolving OpenMP in an Age of Extreme Parallelism, coll. « IWOMP ’09 », p. 154–167,
Berlin, Heidelberg, 2009. Springer-Verlag. Cité page 77.
[214] F. Cappello et D. Etiemble, « Mpi versus mpi+openmp on ibm sp for the nas benchmarks »,
dans Proceedings of the 2000 ACM/IEEE conference on Supercomputing (CDROM), coll. « Super-
computing ’00 », Washington, DC, USA, 2000. IEEE Computer Society. Cité page 77.
[215] G. Krawezik, « Performance comparison of mpi and three openmp programming styles on shared
memory multiprocessors », dans Proceedings of the fifteenth annual ACM symposium on Parallel
algorithms and architectures, coll. « SPAA ’03 », p. 118–127, New York, NY, USA, 2003. ACM.
Cité page 77.
[216] F. Bodin et S. Bihan, « Heterogeneous multicore parallel programming for graphics processing
units », Sci. Program., vol. 17, December 2009, p. 325–336. Cité page 77.
[217] OpenHMPP Consortium, OpenHMPP Concepts & Directives, version 1.0, 06 2011.
Cité page 77.
TM
[218] OpenACC, The OpenACC Application Programming Interface, 11 2011. Cité page 77.
[219] S. Peyton Jones, « Harnessing the multicores : Nested data parallelism in haskell », dans G. Ra-
malingam, éditeur, Programming Languages and Systems, vol. 5356 (coll. Lecture Notes in Com-
puter Science), p. 138–138. Springer Berlin / Heidelberg, 2008. Cité page 78.
[220] S. Marlow, S. Peyton Jones et S. Singh, « Runtime support for multicore haskell », dans
Proceedings of the 14th ACM SIGPLAN international conference on Functional programming, coll.
« ICFP ’09 », p. 65–78, Edinburgh, Scotland, 2009. ACM. Cité page 78.
[221] S. Marlow, S. Peyton Jones et S. Singh, « Runtime support for multicore haskell », SIGPLAN
Not., vol. 44, August 2009, p. 65–78. Cité page 78.
[222] G. E. Blelloch, « NESL : A nested data-parallel language (version 2.6) ». Rapport technique
no CMU-CS-93-129, School of Computer Science, Carnegie Mellon University, avril 1993.
Cité page 78.
[223] G. E. Blelloch, S. Chatterjee, J. C. Hardwick et al., « Implementation of a portable nested
data-parallel language », Journal of Parallel and Distributed Computing, vol. 21, no 1, avril 1994,
p. 4–14. Cité page 78.
[224] M. M. T. Chakravarty, R. Leshchinskiy, S. P. Jones et al., « Data Parallel Haskell : a status
report », dans Proceedings of the 2007 workshop on Declarative aspects of multicore programming,
coll. « DAMP ’07 », p. 10–18, Nice, France, 2007. ACM. Cité page 78.
[225] M. M. Chakravarty, G. Keller, S. Lee et al., « Accelerating Haskell array codes with multicore
GPUs », dans Proceedings of the sixth workshop on Declarative aspects of multicore programming,
coll. « DAMP ’11 », p. 3–14, Austin, Texas, USA, 2011. ACM. Cité page 78.
[226] I. Buck, T. Foley, D. Horn et al., « Brook for GPUs : stream computing on graphics hardware »,
dans ACM SIGGRAPH 2004 Papers, coll. « SIGGRAPH ’04 », p. 777–786, Los Angeles, California,
2004. ACM. Cité page 78.
[227] I. Buck, T. Foley, D. Horn et al., « Brook for GPUs : stream computing on graphics hardware »,
ACM Trans. Graph., vol. 23, August 2004, p. 777–786. Cité page 78.
[228] M. D. McCool et B. D’Amora, « M08 - programming using RapidMind on the Cell BE »,
dans Proceedings of the ACM/IEEE SC2006 Conference on High Performance Networking and
Computing, p. 222. ACM Press, 11 2006. Cité page 78.
172
[229] M. D. McCool, K. W. andBrent Henderson et H.-Y. Lin, « Poster reception - perfor-
mance evaluation of GPUs using the rapidmind development platform », dans Proceedings of the
ACM/IEEE SC2006 Conference on High Performance Networking and Computing, p. 181. ACM
Press, 11 2006. Cité page 78.
[230] M. McCool, S. Du Toit, T. Popa et al., « Shader algebra », ACM Trans. Graph., vol. 23,
August 2004, p. 787–795. Cité page 78.
[231] A. Ghuloum, E. Sprangle, J. Fang et al., « Ct : A flexible parallel programming model for
tera-scale architectures ». Rapport technique, INTEL, October 2007. Cité page 78.
[232] R. Ronny Ronen, « Larrabee : a many-core intel architecture for visual computing », dans Pro-
ceedings of the 6th ACM conference on Computing frontiers, coll. « CF ’09 », p. 225–225, New York,
NY, USA, 2009. ACM. Cité page 78.
[233] C. J. Newburn, M. McCool, B. So et al., « Intel array building blocks : A retargetable, dynamic
compiler and embedded language », dans The International Symposium on Code Generation and
Optimization (CGO), Chamonix, France, 2011. Cité page 78.
[234] INTEL, « Parallel programming. multicore processors today, many-core co-processors ready ».
Rapport technique, INTEL, Juin 2011. Cité page 79.
[235] J. A. Stratton, S. S. Stone et W.-M. W. Hwu, « MCUDA : An efficient implementation of
CUDA kernels for multi-core CPUs », dans J. N. Amaral, éditeur, Languages and Compilers for
Parallel Computing, p. 16–30. Springer-Verlag, Berlin, Heidelberg, 2008. Cité page 79.
[236] N. Farooqui, A. Kerr, G. Diamos et al., « A framework for dynamically instrumenting GPU
compute applications within GPU Ocelot », dans Proceedings of the Fourth Workshop on General
Purpose Processing on Graphics Processing Units, coll. « GPGPU-4 », p. 9 :1–9 :9, Newport Beach,
California, 2011. ACM. Cité page 79.
[237] G. F. Diamos, A. R. Kerr, S. Yalamanchili et N. Clark, « Ocelot : a dynamic optimization
framework for bulk-synchronous applications in heterogeneous systems », dans Proceedings of the
19th international conference on Parallel architectures and compilation techniques, coll. « PACT
’10 », p. 353–364, Vienna, Austria, 2010. ACM. Cité page 79.
[238] R. Domínguez, D. Schaa et D. Kaeli, « Caracal : dynamic translation of runtime environments
for GPUs », dans Proceedings of the Fourth Workshop on General Purpose Processing on Graphics
Processing Units, coll. « GPGPU-4 », p. 5 :1–5 :7, New York, NY, USA, 2011. ACM.
Cité page 79.
[239] Khronos OpenCL Working Group, The OpenCL Specification, version 1.1, 2008.
Cité page 79.
[240] S. Rul, H. Vandierendonck, J. D’Haene et K. De Bosschere, « An experimental study on
performance portability of OpenCL kernels », dans Application Accelerators in High Performance
Computing, 2010 Symposium, Papers, 2010. Cité page 79.
[241] W. Kirschenmann, L. Plagne et S. Vialle, « Multi-target C++ implementation of parallel
skeletons », dans POOSC ’09 : Proceedings of the 8th workshop on Parallel/High-Performance
Object-Oriented Scientific Computing, Genova, Italy, 2009. ACM. Cité pages 83 et 87.
[242] W. Kirschenmann, L. Plagne et S. Vialle, « Multi-Target Vectorization With MTPS C++
Generic Library », dans Proceedings of PARA 2010 conference : State of the Art in Scientific and
Parallel Computing PARA 2010 : State of the Art in Scientific and Parallel Computing, Reykjavik
Islande, 06 2010. Cité pages 83 et 87.
[243] G. Hutton, « A tutorial on the universality and expressiveness of fold », J. Funct. Program., vol.
9, July 1999, p. 355–372. Cité page 91.
[244] C. Strachey, « Fundamental concepts in programming languages », Higher-Order and Symbolic
Computation, vol. 13, 2000, p. 11–49. Cité page 92.
[245] B. Stroustrup, The C++ Programming Language. Addison-Wesley Longman Publishing Co.,
Inc., Boston, MA, USA, 2000. Cité page 93.
173
Bibliographie
[246] J. Lautard, D. Schneider et A. Baudron, « Mixed Dual Methods for Neutronic Reactor Core
Calculations in the CRONOS System », dans Proc. Int. Conf. Mathematics and Computation, Re-
actor Physics and Environmental Analysis of Nuclear Systems, Madrid, Spain, Senda International,
SA, Madrid, p. 814–826, 1999. Cité page 93.
[247] J. Douglas et J. E. Gunn, « A general formulation of alternating direction methods », Numerische
Mathematik, vol. 6, 1964, p. 428–453. 10.1007/BF01386093. Cité page 109.
[248] T. Boubekeur et C. Schlick, « Generic mesh refinement on gpu », dans Proceedings of the ACM
SIGGRAPH/EUROGRAPHICS conference on Graphics hardware, coll. « HWWS ’05 », p. 99–104,
New York, NY, USA, 2005. ACM. Cité page 109.
[249] M. Lefebvre, J.-M. L. Gouez et C. Basdevant, « Generic Refinement and Block Partitioning
for Unstructured GPU Simulations », dans Parallel CFD 2012, Atlanta, USA, May 2012.
Cité page 109.
[250] M. Frigo, C. E. Leiserson, H. Prokop et S. Ramachandran, « Cache-oblivious algorithms »,
dans Proceedings of the 40th Annual Symposium on Foundations of Computer Science, coll. « FOCS
’99 », p. 285–, Washington, DC, USA, 1999. IEEE Computer Society. Cité page 123.
[251] P. Gottschling, D. S. Wise et M. D. Adams, « Representation-transparent matrix algorithms
with scalable performance », dans ICS ’07 : Proceedings of the 21st annual international conference
on Supercomputing, p. 116–125, Seattle, Washington, 2007. ACM. Cité page 123.
[252] P. Gottschling, D. S. Wise et A. Joshi, « Generic support of algorithmic and structural
recursion for scientific computing », The International Journal of Parallel, Emergent and Distributed
Systems (IJPEDS), vol. 24, no 6, December 2009, p. 479–503. Cité pages 123 et 126.
[253] INTEL, Intel R 64 and IA-32 Architectures Optimization Reference Manual, June 2011. 248966-
025. Cité page 136.
[254] J. J. Dongarra, « Performance of various computers using standard linear equations software »,
SIGARCH Comput. Archit. News, vol. 20, June 1992, p. 22–44. Updated version (June 20, 2011).
Cité page 137.
[255] T. L. Veldhuizen, Expression templates, p. 475–487. New York, NY, USA, SIGS Publications,
Inc., 1996. Cité page 150.
[256] T. L. Veldhuizen et M. E. Jernigan, « Will C++ be faster than fortran ? », dans Proceedings of
the Scientific Computing in Object-Oriented Parallel Environments, coll. « ISCOPE ’97 », p. 49–56,
London, UK, 1997. Springer-Verlag. Cité page 150.
174
175
Résumé
Cette thèse aborde les difficultés de mise au point de codes multicibles – c’est-à-dire de codes dont
les performances sont portables entre différentes cibles matérielles. Nous avons identifié deux principales
difficultés à surmonter : l’unification de l’expression du parallélisme d’une part et la nécessité d’adapter
le format de stockage des données d’autre part.
Afin de mettre au point une version multicible de la bibliothèque d’algèbre linéaire Legolas++ mise
au point à EDF R&D, nous avons conçu MTPS (MultiTarget Parallel Skeleton), une bibliothèque dédiée
à la mise au point de codes multicible. MTPS permet d’obtenir une implémentation multicible pour les
problèmes appliquant une même fonction aux différents éléments d’une collection. MTPS prend alors en
charge l’adaptation du format de stockage des données en fonction de l’architecture ciblée.
L’intégration des concepts de MTPS dans Legolas++ a conduit à l’obtention d’un prototype multicible
de Legolas++. Ce prototype a permis de mettre au point des solveurs dont les performances sont proches
de l’optimal sur différentes architectures matérielles.
Abstract
This thesis addresses the challenges of developing multitarget code – that is to say, codes whose
performance is portable across different hardware targets. We identified two key challenges : the unifi-
cation of the the parallelism expression and the need to adapt the format for storing data according to
the target architecture.
In order to develop a multitarget version of Legolas++, a linear algebra library developed at
EDF R&D, we designed MTPS (Multi-Tatget Parallel Skeleton), a library dedicated to the development
of multitarget codes. MTPS allows for multitarget implementations of problems that apply the same
function to all the elements of a collection. MTPS then handles the adaptation of the format for storing
data according to the targeted architecture.
Integrating the concepts of MTPS in Legolas++ has led to the production of a multitarget proto-
type of Legolas++. This prototype has allowed the development of solvers whose performances near the
harware limits on different hardware architectures.
176