thesis/src/6_conclusion.tex

220 lines
14 KiB
TeX

\chapterx*{Conclusion}
%{{{ introduction "
La programmation parallèle induit de nombreuses difficultés par rapport à la programmation
séquentielle classique.
En premier lieu, il faut identifier les portions de code source qui peuvent être exécutées en
parallèle sans invalider leur comportement, c'est-à-dire en conservant le même déroulement lors de
leur exécution jusqu'à l'obtention du même résultat.
Lorsque ces portions de code sont identifiées, la mise en place d'une stratégie de parallélisation
demande un nouvel effort.
De plus, une évolution du programme, même mineure, peut nécessiter une maintenance coûteuse
puisqu'il faut évaluer à nouveau s'il est possible de l'exécuter en parallèle et mettre à jour le
code en conséquence.
Un programme efficacement parallélisé, s'il utilise par exemple des nombres pseudo-aléatoires, peut
ne plus être répétable d'une exécution à l'autre.
D'autre part, en matière de vérification, il est précieux de pouvoir tester les résultats d'un
programme parallèle par rapport à son équivalent exécuté séquentiellement.
Cette thèse présente des outils conçus pour répondre à différents problèmes introduits par la
programmation parallèle, dont la répétabilité de codes stochastiques faisant usage de nombres
pseudo-aléatoires.
Afin de pouvoir expliquer la théorie et le fonctionnement des propositions faites dans cette thèse,
celle-ci explique les rudiments de la programmation parallèle ainsi que les différentes abstractions
et automatisations qui existent.
Deux chapitres sont ensuite dédiés à la généricité, notamment en C++, et à la métaprogrammation,
notamment celle dite template du C++.
Ils détaillent en particulier les fonctionnalités permises par ces paradigmes et qui servent ensuite
dans l'implémentation des bibliothèques actives que sont les outils proposés.
%}}}
%{{{ pfor "
La première bibliothèque, présentée dans \acref{ch:pfor}, propose une interface pour automatiser la
parallélisation d'instructions au sein d'une boucle.
Celle-ci emploie les patrons d'expressions pour acquérir une représentation des instructions de la
boucle sous la forme d'un \gls{ASA}.
Cet \gls{ASA} détaille jusqu'aux opérations sur les indices d'accès aux tableaux -- appelées
fonctions d'indice -- dont les opérandes sont alors connus dès la compilation.
Grâce à cela, la bibliothèque vérifie si tous les accès aux données permettent une exécution
parallèle, et ce selon les caractéristiques des fonctions d'indice :
\begin{enumerate}
\item si toutes sont affines, la bibliothèque répond, avec une spécificité et une sensibilité
parfaite, en calculant l'existence de solutions à un ensemble d'équations diophantiennes ;
\item sinon, si toutes sont injectives, elle répond, avec une sensibilité imparfaite, en utilisant
un test simplifié ;
\item sinon la bibliothèque suppose par défaut que les instructions ne peuvent être parallélisées.
\end{enumerate}
Cette séquence de tests garantit une spécificité parfaite et empêche donc la parallélisation d'un
code qui ne peut l'être sainement.
Ces tests sont appliqués à des groupes d'instructions qui sont formés de sorte que deux instructions
quelconques venant de groupes différents sont indépendantes quant aux données qu'elles utilisent.
De cette manière, les instructions qui ne doivent être exécutées en parallèle peuvent être
maintenues séparées de celles pouvant être exécutées en parallèle si elles sont indépendantes.
Ainsi, la non-parallélisabilité de ces premières instructions ne gêne pas la parallélisabilité des
dernières, alors qu'une analyse sur l'ensemble complet aurait logiquement rapporté une réponse
négative quant à la possibilité d'exécuter les instructions en parallèle.
À partir des fonctions d'indices seules, la bibliothèque détermine automatiquement si celles-ci sont
affines.
Quant aux autres propriétés telles que l'injectivité, elles peuvent être indiquées voire déduites.
C'est par exemple le cas de l'injectivité si la fonction d'indice est strictement croissante ou
strictement décroissante.
Une fois que les ensembles d'instructions parallélisables et non parallélisables sont ainsi définis,
la bibliothèque reproduit le code représenté par l'\gls{ASA} en intégrant ce qui est nécessaire pour
que soient exécutées en parallèles les instructions concernées.
Pour cela, il est possible d'utiliser différents générateurs.
La bibliothèque propose une parallélisation avec OpenMP ou en utilisant des \en{threads} \gls{POSIX}
ou encore une génération des instructions en utilisant la technique du déroulement de boucle.
La bibliothèque a été testée en comparant ses performances avec des programmes équivalents écrits
dans les meilleures règles de l'art, de façon \og artisanale \fg (et donc sans l'utiliser).
Les temps de compilation, bien qu'il y ait certainement encore des améliorations possibles sur cet
aspect, sont raisonnables et permettent une utilisation au sein de projets.
Les temps d'exécution montrent que l'abstraction apportée par la bibliothèque n'implique que des
surcoûts minimes.
%}}}
%{{{ alsk "
La seconde proposition faite dans cette thèse est une autre bibliothèque active ayant pour objectif
la parallélisation assistée par les squelettes algorithmiques.
Contrairement à la première proposition, les portions du programme qui peuvent être exécutées en
parallèle sont intrinsèquement liées au squelette algorithmique que définit le développeur.
Cette bibliothèque dispose de sa propre manière de concevoir des squelettes algorithmiques.
Celle-ci repose sur une séparation initiale de deux concepts : la structure et les liens.
La structure du squelette est une composition d'autres structures et d'os, les éléments structurels
atomiques introduits dans cette thèse.
Chaque os correspond à un motif d'exécution, par exemple l'exécution séquentielle de plusieurs
tâches ou l'exécution parallèle d'une même tâche répétée, suivie d'une autre pour sélectionner le
meilleur résultat produit.
Quant aux liens, il s'agit d'une description des transferts de données entre les différentes tâches
à exécuter.
Cela se définit en utilisant des paramètres spéciaux, lesquels indiquent à la bibliothèque ce par
quoi ils doivent être remplacés (un paramètre de la tâche appelante, la valeur de retour d'une
autre, ...).
La structure du squelette permet de savoir quels éléments peuvent être exécutés en parallèle.
Il est donc par exemple possible de déterminer le nombre de niveaux de parallélisation dont on
dispose pour un squelette donné.
Pour cela, une bibliothèque annexe d'outils pour la métaprogrammation template est utilisée.
Celle-ci comporte des algorithmes pour parcourir des listes et des arbres de types, et un squelette
algorithmique peut être transformé en arbre (et un arbre en liste si nécessaire).
En utilisant, notamment, l'information du nombre de niveaux parallélisables, la bibliothèque permet
l'optimisation de la répartition des tâches sur les différents \en{threads} selon plusieurs
politiques d'exécution : \en{thread pool} ; répartition équilibrée ; répartition équilibrée
n'utilisant que le premier niveau ; ...
Dans le cas du \en{thread pool}, l'équilibrage de la charge entre les différents \en{threads} est
automatique et dynamique, au coût d'une synchronisation à effectuer pour l'accès aux tâches à
exécuter.
La répartition équilibrée proposée suppose une durée similaire dans l'exécution des différentes
tâches et les distribue aux \en{threads} de manière à ce que chacun en ait, à une près, le même
nombre.
Cette hypothèse semble raisonnable en particulier pour des os répétant une même tâche plusieurs
fois, os fréquemment utilisés dans l'implémentation de métaheuristiques.
Grâce aux connaissances apportées par le choix d'une politique d'exécution et au contrôle permis par
les liens, cette thèse propose une solution au problème de la perte de répétabilité lors de
l'exécution parallèle d'un programme utilisant des nombres pseudo-aléatoires.
Cette solution permet plus généralement le maintien de la répétabilité lors de l'utilisation de
données qui doivent n'être utilisées que par un unique \en{thread}.
Ces données doivent donc être associées aux tâches qui partagent le \en{thread} qui les exécute.
Une solution triviale consiste à fournir à chaque tâche ses propres données.
Lorsqu'il s'agit de nombres pseudo-aléatoires, cela signifie qu'il faut déterminer une séquence
indépendante pour chaque tâche, ce qui devient coûteux, entre autres en mémoire, lorsque le nombre
de tâches augmente et que le statut initial du générateur est conséquent (proche de
\SI{2.5}{\kibi\octet} par exemple pour un état initial de Mersenne Twister MT19937).
En tenant compte de la distribution des tâches sur les \en{threads}, et ce pour différentes
quantités de \en{threads}, on détermine quelles tâches sont toujours exécutées par un \en{thread}
commun (indépendamment du nombre de \en{threads}) et on leur associe une séquence de nombres
pseudo-aléatoires commune (état initial partagé).
Ceci permet de garantir la répétabilité non seulement d'une exécution à l'autre, mais également
lorsque le nombre de \en{threads} varie.
Les tâches ayant besoin de nombres pseudo-aléatoires peuvent alors en recevoir un automatiquement au
moyen d'un paramètre spécial que les liens permettent d'utiliser.
Cette bibliothèque a été utilisée pour résoudre des instances de \gls{TSP} par la métaheuristique
\graspels{}.
Les temps d'exécution sont comparés à ceux obtenus avec une implémentation manuelle d'un \graspels{}
et correspondent à des exécutions parallèles et à des exécutions séquentielles (afin de valider que,
même dans ce cas, la bibliothèque n'induit pas de surcoût).
Dans tous les cas, les temps obtenus sont analogues.
%}}}
\chapterx*{Limites et perspectives}
%{{{ perspectives "
%{{{ pfor "
Nous avons bien conscience que notre première proposition, orientée sur l'analyse et la
parallélisation automatique de boucles, est incomplète dans la mesure où de multiples boucles
imbriquées ne sont pas détectées.
Pour le moment, l'utilisation de multiples boucles imbriquées signifie qu'une seule sera traitée
pour la parallélisation.
Ce fait n'est pas gênant si l'on considère les architectures matérielles multi-cœurs actuelles.
La bibliothèque ne propose pas d'outil pour mettre en place une parallélisation automatique sur
plusieurs niveaux car elle ne permet pas, à ce stade, l'utilisation de l'indice d'un niveau à un
niveau inférieur.
C'est une piste de recherche que nous envisageons d'explorer à l'avenir.
De plus, il sera intéressant de tester différentes méthodes pour appliquer la parallélisation.
Notamment l'utilisation d'un \en{thread pool} afin de voir si l'introduction de sections critiques
induit un coût négligeable par rapport au gain supposé apporté par le fait d'éviter la recréation de
\en{threads} à chaque nouvelle boucle parallélisée.
Au niveau de l'analyse et particulièrement des tests permettant de déterminer la parallélisabilité
des instructions, l'implémentation du modèle polyédral est également une piste.
Celle-ci peut notamment être utile au support de multiples boucles imbriquées, mais peut
éventuellement également servir à augmenter la sensibilité du test pour des boucles à un seul niveau
lorsque les fonctions d'indice ne sont ni affines ni injectives.
Une autre piste d'amélioration consiste en l'application de transformations sur le code source
traité.
Actuellement, celui-ci est analysé pour sa parallélisabilité, et s'il ne passe pas le test, il est
exécuté séquentiellement.
Il est possible, en modifiant astucieusement les instructions, de conserver le comportement du
programme en éliminant des dépendances, augmentant donc potentiellement sa parallélisabilité.
%}}}
%{{{ alsk "
Par rapport à la seconde proposition, axée sur la parallélisation assistée par l'utilisation de
squelettes algorithmiques, le travail effectué était principalement centré sur l'abstraction
apportée par la bibliothèque de squelettes algorithmiques, au détriment de l'optimisation des
différentes politiques d'exécution fournies.
Ces politiques d'exécution ne sont donc pas implémentées de manière optimale.
Une personne dont la parallélisation est le domaine d'expertise pourrait améliorer cela.
En outre, il serait très intéressant de tester la piste évoquée dans
\acref{subsubsec:alsk/exec/impl/static}, à savoir la préparation, dès la compilation, de la séquence
complète des tâches qu'exécutera chaque \en{thread} afin d'éviter un défaut actuel de la politique
d'exécution répartissant les tâches de manière équilibrée : les \en{threads} des niveaux parallèles,
à l'exception du tout premier, sont créés de multiples fois.
Une autre idée est de permettre l'ajustement du poids des tâches à exécuter afin de ne plus supposer
un temps d'exécution homogène.
En utilisant ces poids, une distribution équilibrée des tâches sur les différents \en{threads} peut,
peut-être, être aussi efficace en termes d'équilibrage de charge que ce que peut accomplir
dynamiquement un \en{thread pool}.
De plus, une exécution, partielle ou sur des données réduites, du programme permettrait
éventuellement la génération automatique de ces poids.
Les os proposés au sein de cette thèse répondent aux besoins levés par l'applicatif en \gls{RO} que
nous avons utilisé.
Néanmoins, des motifs tels que le \en{pipeline}, sont manquants pour une utilisation réellement
générale.
Certains de ces nouveaux os pourraient nécessiter l'ajout de primitives au sein des exécuteurs.
Le système de liens apporte des avantages intéressants, mais également une syntaxe plus lourde pour
le développeur qui, en utilisant d'autres bibliothèques de squelettes algorithmiques, pourrait
préférer une valeur par défaut.
L'\gls{EDSL} introduit en partie cette possibilité de valeur par défaut et même de déduction de
liens, mais cela manque aux couches plus basses.
Ce système de liens permet par ailleurs de connaître exactement quelles tâches nécessitent
l'utilisation, par exemple, de nombres pseudo-aléatoires.
En utilisant cette information, il est possible de réduire encore le nombre de \gls{PRNG} devant
être créés pour garantir la répétabilité du programme.
%}}}
%}}}