Vers une programmation fonctionnelle en
appel par valeur sur systèmes multi-coeurs :
évaluation asynchrone et ramasse-miettes
parallèle1
Luca Saiu
LIPN - UMR 7030
Université Paris 13 - CNRS
99, av. J.B. Clément - F-93430 Villetaneuse
RÉSUMÉ. Les machines multi-coeurs modernes sont conçues pour exécuter de manière ef-
ficace des programmes assembleur explicitement parallèles. En effet, la technologie actuelle
avance toujours moins dans la direction du parallélisme au niveau des instructions, qui ne de-
mande pas une modification des programmes, et toujours plus vers le parallélisme au niveau
des tâches qui, au contraire, demande une modification des programmes. Dans ce contexte,
l’exploitation du parallélisme nécessite une modification soit des programmes assembleurs, en
aval de la chaîne de compilation, soit des programmes d’un langage de plus haut niveau en
amont de la chaîne. Autrement dit, l’exploitation des machines multi-coeurs peut suivre deux
directions alternatives : changer le modèle de programmation ou bien cacher à l’utilisateur la
complexité d’une telle tâche en déléguant au compilateur la parallélisation d’un programme de
plus haut niveau, dans un style préférablement déclaratif. Dans les deux cas, les versions compi-
lées doivent être efficaces sur ce type d’architecture. L’efficacité du support à temps d’exécution
est donc cruciale. En particulier, lorsque le langage est de haut niveau et prévoit une gestion au-
tomatique de la mémoire, le ramasse-miettes est un goulet d’étranglement potentiel qui risque
de limiter le gain de performance de l’application toute entière. Cet article est un retour d’expé-
rience sur l’association de deux outils que nous avons implantés, un interpréteur d’un langage
fonctionnel augmenté par une construction pour le calcul asynchrone et un ramasse-miettes
1. Travail partiellement financé par Marie Curie Actions, Contract n˚ MTKI-CT-2005-029849
WEBSICOLA.
pour architectures parallèles. La combinaison des deux outils offre à un utilisateur, humain ou
compilateur, une manière d’exploiter le parallélisme matériel d’une machine multiprocesseur à
mémoire partagée par un style de programmation déclaratif et essentiellement fonctionnel.
ABSTRACT. Modern multi-core machines are conceived to efficiently execute explicitly paral-
lel assembly programs. Current technology is moving away from instruction-level parallelism,
exploitable without changes in the software, toward task-level parallelism requiring modifica-
tions in programs. In this context exploiting parallelism entails either exploiting new paral-
lelizing toolchains, or programming in new (higher-level) languages supporting explicit paral-
lelism. In other words exploiting multi-core machines may follow two different routes: changing
the programming model or hiding from the user the complexity of this task by delegating to a
compiler the parallelization of a higher-level program, preferably in a declarative style. In ei-
ther case compiled programs must be efficient on such architectures, hence the efficiency of the
language runtime support is also central. In particular, when the language is high-level and
provides automatic memory management, the garbage collector risks to be the bottleneck limit-
ing the overall performance gain. This paper is an experience report on the association of two
softwares which we have implemented, an interpreter for a functional language augmented by a
construct for asynchrone computations and a garbage collector for parallel machines. The two
tools combined provide the user (be it a human or a compiler), a way to exploit the hardware
parallelism of a shared-memory multi-processor machine with a declarative and essentially
functional programming style.
MOTS-CLÉS : multi-coeur, future, ramasse-miettes, programmation fonctionnelle
KEYWORDS: multi-core, future, garbage collection, functional programming
2
1. Introduction
La pratique de la programmation fonctionnelle montre à quel point
l’adoption du modèle applicatif permet la réalisation de logiciels com-
plexes de façon plus économique, particulièrement dans le cadre des
langages typés statiquement. Par ailleurs, les performances se sont amé-
liorées dans le temps et il est courant d’observer des compilateurs de
langages fonctionnels qui génèrent du code ayant des performances
comparables à celles d’un code impératif. Le compilateur de OCaml
et certains compilateurs du langage SML en sont de bons exemples.
Pendant plusieurs années, les constructeurs de matériel ont focalisé
leur offre sur des machines mono-processeur où le processeur devenait
de plus en plus performant et complexe, mais d’une complexité dont
l’utilisateur pouvait faire abstraction. En effet, l’apparition d’une
nouvelle génération de processeurs induisait un accroissement gratuit
des performances : tout processeur rendait plus efficace le code existant
par une exploitation accrue du parallélisme au niveau des instructions,
de la taille des mémoires cache mais, surtout, par une augmentation
souvent spectaculaire de la fréquence d’horloge.
On a mangé notre pain blanc (the free lunch is over, [21]) : si la
loi de Moore reste valide [6] depuis 1975, l’augmentation du nombre
de transistors par puce ne correspond plus à une augmentation de la
fréquence d’horloge. Les producteurs de processeurs génériques se
sont ainsi réorientés vers une architecture multi-coeurs à mémoire
partagée (Symmetric Multi-Processing ou SMP) permettant l’avance-
ment parallèle de plusieurs fils d’exécution ou threads. La complexité
interne des processeurs1, qui exploite le parallélisme au niveau des
instructions, doit alors se réduire : pour inclure le plus grand nombre
de coeurs dans une puce, chaque coeur doit occuper le moins de place
possible quitte à renoncer aux structures de pipelining longues et
quitte à réduire le degré (nombre de voies) de l’architecture super-
scalaire. Il ne s’agit pas d’un vrai choix de la part des producteurs :
c’est une nécessité due à des limites physiques, reconnue de façon
1. Dans ce contexte, nous ignorerons la distinction entre coeur et CPU
3
presque unanime2, qui ne sera pas remise en question à moins d’une
révolution technologique imprévue. Actuellement, des processeurs
avec des centaines voire milliers de coeurs sont ainsi annoncés en
cours de développement [4]. Or, programmer une telle puissance de
calcul est loin d’être simple et l’opposition de Knuth à la direction
prise par les constructeurs est recevable au moins dans ce sens. Pour
approcher les limites théoriques des performances des processeurs
multi-coeurs un modèle de programmation différent s’impose, tout au
moins au niveau assembleur. Dans l’idéal, plusieurs fils d’exécution
devraient pouvoir avancer sur les différents coeurs du système avec
une distribution de la charge uniforme et sans demander excessivement
de synchronisations. On passe de la forme de parallélisme au niveau
des instructions à celle au niveau des tâches, une forme qui ne peut
pas être «déduite» automatiquement par le processeur mais qui, au
contraire, demande que le code soit explicitement parallèle, c’est-à-dire
découpé en «tâches», threads ou processus. Ce découpage n’est pas
une pratique nouvelle : une utilisation explicite de la concurrence
est parfois pertinente dans la construction d’interfaces graphiques
ou logiciels interactifs ; mais, il s’agit d’un besoin de modularité du
programme, d’expressivité, plutôt qu’un souhait de parallélisation afin
d’améliorer les performances. Toutefois, quelque soit l’objectif, les
développeurs qui pratiquent la programmation concurrente s’exposent
aux risques d’un modèle intrinsèquement complexe où le partage de
données mutables, c’est-à-dire non persistantes, et les synchronisations
explicites sont monnaie courante. Cette forme de contrôle explicite
reste donc une technique de bas niveau essentiellement impérative, qui
contraste avec le style fonctionnel et qu’il serait souhaitable de cacher
derrière un style de programmation de plus haut niveau.
Les techniques de programmation parallèle le plus couramment
utilisées ne sont pas de haut niveau, que le modèle de parallélisme soit
à squelettes algorithmiques (skeleton programming), à flot de données
(dataflow), ou data-parallèle imbriqué (nested data parallel), et que
le paradigme de programmation soit au départ impératif, fonctionnel,
logique ou autre. Dans le cadre impératif, le système OpenMP [19]
2. Il existe sur ce choix l’opinion divergente de Donald Knuth [16], qui aurait insisté dans la
direction monoprocesseur pour les difficultés évidentes de la programmation de telles machines.
4
mérite d’être cité comme une tentative plutôt réussie d’extension du C
et de Fortran avec des constructions de haut niveau pour le parallélisme
explicite avec partage de mémoire.
Plusieurs tentatives existent aussi dans le cadre fonctionnel quoique
les tentatives se soient soldées par des échecs en parallélisme implicite
pur. Avec moins d’ambition mais, sans doute, plus de réalisme à court
terme, on peut imaginer l’introduction d’annotations ou de construc-
tions syntaxiques spécifiques à la parallélisation de certaines parties du
programme, ce qui est notamment le cas avec les constructions future
ou let-par [5, 15, 7, 14, 18]. Ainsi, le langage JoCaml [8] est une ex-
tension de OCaml inspiré par le Join Calculus, un modèle de la concur-
rence à échange de messages proche du π-calcul. Ce système, tout
comme le cadre MPI, correspond à un modèle de processus à environ-
nement local. À contrario, notre étude repose sur un modèle à mémoire
partagée, c’est-à-dire un modèle de concurrence à environnement glo-
bal.
Dans cette multitude de possibilités, le paradigme fonctionnel
semble constituer, a priori, une meilleure base pour l’analyse automa-
tique par la simplicité de sa sémantique et de son modèle d’exécution.
Dans un premier temps, nous présenterons le langage fonctionnel
nanolisp. nanolisp intègre, en sus des constructions fonctionnelles
élémentaires, la primitive future de Baker et Hewitt [14] permet-
tant l’évaluation asynchrone d’une expression. nanolisp pourra être
considéré de deux manières différentes : comme exemple de langage
fonctionnel avec parallélisme explicite ou comme une étape intermé-
diaire ou finale de la traduction d’un langage fonctionnel implicitement
parallèle.
Que nanolisp soit le point de départ ou bien un langage intermédiaire
dans une hypothétique chaîne de compilation, ses performances
dépendront de la présence d’un système de gestion de la mémoire
capable de bien exploiter le parallélisme matériel. En effet, d’une part,
il serait difficile d’imaginer un langage parallèle sans un support à
temps d’exécution lui aussi parallèle. D’autre part, il serait difficile
d’imaginer un langage fonctionnel sans une gestion automatique de
la mémoire (allocation et récupération). La deuxième partie de ce
5
1 / 31 100%
La catégorie de ce document est-elle correcte?
Merci pour votre participation!

Faire une suggestion

Avez-vous trouvé des erreurs dans linterface ou les textes ? Ou savez-vous comment améliorer linterface utilisateur de StudyLib ? Nhésitez pas à envoyer vos suggestions. Cest très important pour nous !