Académique Documents
Professionnel Documents
Culture Documents
I. Introduction
V. Conclusion
2
INTRODUCTION
La programmation parallèle hybride peut être réalisée par exemple en utilisant à la fois
OpenMP et MPI.
Element de base
Les éléments de base d'OpenMP sont les constructions pour la création de threads, la
distribution de la charge de travail (partage du travail), la gestion de l'environnement de
données, la synchronisation des threads, les routines d'exécution au niveau de l'utilisateur et
les variables d'environnement.
INTRODUCTION
Le pragma omp parallel est utilisé pour bifurquer des threads supplémentaires pour effectuer
le travail inclus dans la construction en parallèle. Le thread d'origine sera désigné
comme thread maître avec l'ID de thread 0.
La gestion de l’execution des processus est assurée par le système d’exploitation (OS)
Le mécanisme fourni par les deux directives de variantes pour sélectionner des variantes est plus
pratique à utiliser que le prétraitement C/C++ car il prend directement en charge la sélection de
variantes dans OpenMP et permet à un compilateur OpenMP d'analyser et de déterminer la
directive finale à partir des variantes et du contexte.
Clauses
Comme OpenMP est un modèle de programmation à mémoire partagée, la plupart des variables du
code OpenMP sont visibles par défaut pour tous les threads. Mais parfois, des variables privées
sont nécessaires pour éviter les conditions de concurrence et il est nécessaire de transmettre des
valeurs entre la partie séquentielle et la région parallèle (le bloc de code exécuté en parallèle), de
sorte que la gestion de l'environnement de données est introduite sous forme de clauses d'attribut de
partage de données en les ajoutant à la directive OpenMP. Les différents types de clauses sont :
partagé : les données déclarées hors d'une région parallèle sont partagées, c'est-à-dire visibles et
accessibles par tous les threads simultanément. Par défaut, toutes les variables de la région de
partage de travail sont partagées, à l'exception du compteur d'itérations de boucle. private : les
données déclarées dans une région parallèle sont privées pour chaque thread, ce qui signifie que
chaque thread aura une copie locale et l'utilisera comme variable temporaire. Une variable privée
n'est pas initialisée et la valeur n'est pas conservée pour une utilisation en dehors de la région
parallèle. Par défaut, les compteurs d'itération de boucle dans les constructions de boucle OpenMP
sont privés. default : permet au programmeur d'indiquer que la portée des données par défaut dans
une région parallèle sera soit shared , soit none pour C/C++, soit shared , firstprivate , private , ou
firstprivate : comme private sauf initialisé à la valeur d'origine. lastprivate : comme
private sauf que la valeur d'origine est mise à jour après la construction. réduction :
un moyen sûr de joindre le travail de tous les threads après la construction.
Concept Processus –Concept Thread
Un processus est parfois appelé tâche lourde. Un thread est souvent appelé processus léger.
Les processus sont independant l’un independants l’un de l’autre , tandis que les threads
appartiennent au meme processus
Les procesuus on t des adresses memoires diifenrentes, tandis que les threads partagent la
meme zone mémoire
Les processus peuvent communinquer ebte eux a travers leur capacité d’enchanges de données,
tandisque les threads peuvent avoir access des direct aux ressource occupées par leur processus
Tous les threads appartiennent à un processus partagent des descripteurs de fichiers communs,
une mémoire de tas et d’autres ressources,
Répartition des tâches
Sans spécification particulière, les tâches sont réparties de manière égalitaire entre les différents
threads. Les threads traitant le centre de la fractale ont plus de calculs à traiter que les threads
chargés de l'extérieur de la fractale. Les threads les plus rapides attendent donc les threads les plus
lents avant la finalisation du programme. La répartition des tâches est configurable à la fin de la
directive omp parallel for avec la clause schedule. Le découpage peut se faire selon quatre
manières différentes.
Répartition statique
Si rien n'est spécifié, la répartition statique est utilisée, elle peut être aussi explicitement
choisie en utilisant la clause schedule(static). Le nombre d'itérations de la boucle est
divisé par le nombre de threads, tous les threads traitent donc la même quantité de
données. Il est possible de fixer le nombre de données traité par chaque thread après la
déclaration static, par exemple : schedule(static,32).
Répartition dynamique
Chaque thread traite une quantité de données spécifiée par la taille passée après dans la
déclaration dynamic. Dès qu'un thread a terminé son lot de données, il peut reprendre un lot
de données à traiter. Les threads n'ont pas d'ordre de passage, certains peuvent être plus longs
que d'autres.
Par défaut, un seul élément est traité par chaque thread. Le nombre d'éléments traités peut être
fixé après la déclaration dynamic. Sauf si les calculs sont très longs, il doit être initialisé pour
prendre une valeur supérieure à la valeur par défaut : schedule(dynamic,32).
Répartition guidée
Cette organisation des tâches est basée sur une taille décroissante des blocs traités. Au
départ, les threads traitent une grande quantité de données, puis le nombre d'éléments
traités décroît pour optimiser le temps de calcul. Le nombre minimal d'éléments à
traiter est fixé par la valeur passée en paramètre.
Répartition à l'exécution
❖ Inconvénients:
➢ Risque d'introduire des bogues de synchronisation difficiles à déboguer et des conditions de
concurrence
➢ Nécessite un compilateur prenant en charge OpenMP.
➢ L'évolutivité est limitée par l'architecture de la mémoire.
➢ Pas de support pour compare-and-swap
➢ La gestion fiable des erreurs fait défaut.
➢ Manque de mécanismes précis pour contrôler le mappage thread-processeur
Cas pratique
Pour commencer, un programme élémentaire va être pris comme exemple pour montrer
l'utilisation générale d'OpenMP. Ce programme est présenté ci-dessous, la boucle
représente le traitement de différents éléments de manière séquentielle.
La compilation se
fait avec la ligne :
Compilation
Deux threads ont été automatiquement créés. L'ensemble des indices de la boucle a été séparé en deux parties : les
indices allant de 0 à 3 sont traités par le premier thread, les indices allant de 4 à 7 sont traités par le second thread.
Fixation du nombre de threads
Dans l'exemple précédent, le nombre de threads a été automatiquement fixé par OpenMP. Il est
possible de fixer le nombre de threads de plusieurs manières. Il peut être configuré en utilisant la
variable système OMP_NUM_THREADS.
Il est ainsi possible d'adapter le nombre de threads aux processeurs équipant la machine.
Exemple de programme réalisant le produit
de matrices avec OpenMP
Le calcul du produit de deux matrices est une application typique (mais un peu
matheuse...) des problèmes de parallélisation. Chaque élément peut être calculé de
manière indépendante des autres, la tâche doit donc pouvoir se traiter avec OpenMP.
Le calcul du produit de matrices fait appel à trois boucles les unes dans les autres (pour
l'algorithme naïf, des versions optimisées existent).
Partage des variables entre les threads
Avant de présenter le détail du programme, nous allons nous attarder sur une option de
la directive omp parallel for utilisée dans le programme précédent.
Par défaut, toutes les variables présentes dans la boucle sont partagées entre les
différents threads, sauf le compteur de boucles (chaque thread dispose d'une copie
qu'il est libre de modifier, sans conséquence pour les autres threads). Il est souvent
nécessaire de protéger des variables contre des accès simultanés (un thread écrit
pendant qu'un autre lit). Les variables à protéger sont listées dans la clause private.
Exemple de programme réalisant le produit
de matrices avec OpenMP
double get_time() {
struct timeval tv;
gettimeofday(&tv, (void *)0);
return (double) tv.tv_sec + tv.tv_usec*1e-6;
}
Code du produit de matrices avec OpenMP
stop=get_time();
t=stop-start;
printf("%d\t%f\n",nb,t);
}
// Libérations
free(matrice_A);
free(matrice_B);
free(matrice_res);
return EXIT_SUCCESS;
}
Résultats
Pour tester les différents programmes qui vont suivre, nous avons utilisé une
machine équipée de deux processeurs Xeon 5060. Chaque processeur est
un double cœur capable de gérer l'hyperthreading. En théorie (ou selon
les commerciaux...), cette machine peut donc exécuter 8 threads
simultanément.
Le système d'exploitation est Ubuntu Server 19.10 avec un
compilateur gcc dans sa version 4.3. Pour tous les essais, le système est
utilisé sans interface graphique pour minimiser le nombre de tâches liées au
système d'exploitation.
Résultats
Cet exemple simple nous a montré qu'il est facile d'utiliser tous les cœurs
d'un processeur sans modifier lourdement un programme. Dans la suite de
cet article, nous allons détailler deux autres cas concrets afin d'aborder le
partage des tâches et les conflits de mémoire.