• Aucun résultat trouvé

Description implicite du parallélisme applicatif

2.3 Modèles de programmation parallèle haut niveau

2.3.3 Description implicite du parallélisme applicatif

Dans la section précédente, nous avons présenté des langages ou environnements de programmation permettant de décrire explicitement le parallélisme de l’application par création dynamique d’activité (ou création de tâche lorsque l’on se place du point de vue de l’application) et par synchronisation explicite de ces activités.

Une autre méthode de programmation parallèle consiste à laisser à un compilateur et/ou à un système d’exécution le soin de découvrir le parallélisme de l’application. On parle alors de parallélisme implicite. Cette méthode s’applique aussi bien à des langages sans séquencement comme les langages fonctionnels ou logiques qu’aux langages impé-ratifs classiques tel que Fortran ou C. La détection du parallélisme de l’application peut être réalisée par un compilateur paralléliseur à partir du code de l’application mais elle peut également être réalisée dynamiquement en cours d’exécution (c’est le cas pour beau-coup de langages fonctionnels ou logiques). Dans les deux cas la détection du parallélisme est obtenue par analyse des dépendances de données du programme.

Les caractéristiques des langages dans lesquels aucun ordre d’exécution des instruc-tions n’est spécifié, permettent d’en extraire naturellement les dépendances de données. Ainsi, parmi les langages fonctionnels Sisal [13], Multilisp [61] et Haskell [71, 121] offrent des compilateurs ou des supports exécutifs permettant l’exécution parallèle.

Nous nous intéresserons principalement dans la suite à l’extraction du parallélisme des langages séquentiels.

2.3.3.1 Extraction du parallélisme à la compilation

La parallélisation automatique d’un code séquentiel est un problème difficile et non encore complètement résolu à ce jour [40]. Cette technique est actuellement surtout adap-tée à la parallélisation des nids de boucles. En effet ces nids de boucles permettent de représenter de manière compacte une quantité importante de parallélisme potentiel, l’ana-lyse en est donc facilitée.

Des directives de compilation peuvent également être rajoutées dans le programme source pour faciliter l’extraction du parallélisme ainsi que pour aider au placement des calculs et des données sur les processeurs. Cette approche est par exemple utilisée dans HPF [65] dans lequel des directives de partitionnement et de placement des tableaux sont fournies par le programmeur conduisant à un placement des calculs sur les processeurs.

Finalement, une limitation importante des compilateurs paralléliseurs est qu’il n’y a pas toujours suffisamment d’information lors de la compilation pour générer un code parallèle. Par exemple lorsque le parallélisme de l’application dépend des données en entrée, le compilateur n’est pas capable de paralléliser le programme.

2.3.3.2 Extraction du parallélisme à l’exécution

Contrairement aux techniques de parallélisation à la compilation qui analysent les sources du programme pour extraire le parallélisme, les techniques présentées maintenant effectuent cette analyse à l’exécution. Elles permettent ainsi d’extraire le parallélisme du programme qui dépend des données en entrée du programme (par exemple la factorisation creuse de Cholesky).

Cependant cette analyse à l’exécution va naturellement engendrer un surcoût lors de l’exécution. Dans le but de réduire ce surcoût, l’analyse n’est pas réalisée sur des instruc-tions élémentaires comme dans les compilateurs paralléliseurs mais sur des tâches qui correspondent à des séquences d’instructions du programme. L’exécution du programme est alors explicitement décomposée en tâches de calcul par le programmeur. L’analyse à l’exécution consiste alors essentiellement à extraire le parallélisme présent entre ces tâches.

Plusieurs langages ou environnements de programmation ont ainsi été proposés, qui permettent d’extraire à l’exécution le parallélisme présent au niveau des tâches. C’est le cas par exemple de Jade [108, 107] et de Mentat [59]. Athapascan-1 entre également dans ce cadre.

Dans ces langages, les tâches sont explicitement définies et créées par le program-meur. Ainsi, dans Jade, une tâche correspond à un bloc d’instructions inséré dans une construction spécifique du langage (pour le détail, voir la section 2.4.2 page 34). Une

Modèles de programmation parallèle haut niveau 2.3 tâche est alors créée à chaque fois que le flot d’exécution rencontre ce bloc. Dans le lan-gage Mentat les tâches correspondent à l’exécution de certaines fonctions identifiées dans le programme grâce à une construction du langage3. Les tâches sont alors créées lors de l’appel de l’une de ces fonctions. En Athapascan-1, les tâches correspondent à l’exécu-tion de procédures et sont explicitement créées en ajoutant un mot clé devant l’appel de la procédure. Notons également que pour ces trois langages les tâches peuvent être créées récursivement.

L’extraction du parallélisme entre les tâches nécessite d’analyser les dépendances de données entre les tâches. Plusieurs solutions ont été proposées pour permettre cette ana-lyse des dépendances de données. Dans Jade et Athapascan-1 des mécanismes permettent d’indiquer au système d’exécution, lors de la création d’une tâche, l’ensemble des effets de bords pouvant éventuellement être réalisés par celle-ci. Dans Mentat les tâches sont soit des fonctions pures donc sans effet de bord soit des fonctions qui réalisent des effets de bord sur un objet bien identifié4.

Grâce à cette analyse des dépendances de données, le système peut maintenir pendant l’exécution un graphe de flot de données qui contient les tâches à exécuter ainsi que les dépendances de données entre ces tâches. Le système d’exécution repose alors sur des mécanismes d’abstraction de la machine parallèle similaires à ceux présentés dans les deux sections précédentes pour exécuter ces tâches tout en respectant les contraintes de précédences induites par les dépendances de données. Ainsi dans Jade, Mentat et Atha-pascan-1 une mémoire partagée distribuée de niveau objet est proposée et les effets de bord des tâches ne peuvent être réalisés que sur des objets présents dans cette mémoire partagée. De même pour l’exécution des tâches, des mécanismes de régulation de charge sont mis en œuvre.

En conclusion, l’intérêt de cette approche est essentiellement de faciliter la program-mation parallèle puisque le programmeur ne gère pas explicitement les synchronisations entre les tâches (il n’y a plus de problème liés à la cohérence des accès en mémoire par-tagée ou à l’indéterminisme de l’exécution). Cependant un autre intérêt de cette approche est d’exhiber, à l’exécution, une représentation fine de l’exécution future sous la forme du graphe de flot de données (la précision ou granularité de cette représentation est contrô-lée par le programmeur). Pour certaines applications, dont le parallélisme peut dépendre des données en entrée, il est même possible de dérouler en début d’exécution l’ensemble du graphe de flot de données de l’exécution à venir : c’est le cas par exemple de la fac-torisation numérique creuse de Cholesky présentée dans les chapitres 8 et 9 et dont le parallélisme dépend fortement de la structure de la matrice creuse. L’intérêt de disposer de ce graphe est essentiel tant du point de vue de l’ordonnancement de l’application (des techniques d’ordonnancement statiques de ce graphe sont détaillées dans le chapitre 5 et 3. Plus exactement Mentat est une extension de C++ possédant trois « types » de classes étendant les classes du langage C++. Les tâches correspondent alors aux fonctions membres de ces classes et sont créées lors de l’appel de l’une de ces fonctions membres.

peuvent être mises en œuvre) que du point de vue de la gestion de la mémoire partagée distribuée. Notons que les langages qui supportent un modèle de programmation parallèle explicite comme Cilk ou Cid n’offrent que des mécanismes simples de synchronisation qui ne permettent pas une description aussi précise du futur de l’exécution. Ainsi, dans Cilk et Cid seul un mécanisme de synchronisation sur la fin d’activité est offert. La repré-sentation, à un instant donnée, du futur de l’exécution est alors restreinte à un graphe join (i.e. un arbre inverse) représentant les synchronisations entre les activités.