High-performance computing
Découverte d'OpenMP : Optimisez vos codes en parallèle, sans prise de tête
L'optimisation du calcul parallèle est devenue une compétence incontournable pour tout développeur ou ingénieur qui veut maximiser les performances de ses applications. OpenMP est l'un des outils les plus populaires pour paralléliser vos codes en C, C++ ou Fortran, en quelques lignes. Dans cet article, on va plonger dans OpenMP d'une manière ludique et accessible, avec des exemples concrets et du code que vous pourrez tester vous-même.
Sommaire :
1. Création et gestion des threads
2. Maîtriser les régions parallèles : `single` et `master`
3. Barrières et synchronisation
4. La différence entre `critical` et `atomic`
5. Debugging et analyse de la sortie attendue
6. Exercice de code parallèle et optimisé
7. Conclusion et exercices pratiques
`
1. Création et gestion des threads
Imaginez que vous devez embaucher trois assistants pour exécuter différentes tâches. En programmation parallèle, ces assistants sont des **threads**. Pour en créer plusieurs, OpenMP vous propose la directive magique `#pragma omp parallel` avec l'option `num_threads(P)`. P étant le nombre de threads.
Exemple de code :
#include <stdio.h>
#include <omp.h>
#pragma omp parallel num_threads(3)
{
int thid = omp_get_thread_num();
printf("Thread %d est en train d'exécuter du code en parallèle.\n", thid);
}
```
Ce petit bijou de code crée 3 threads qui s'exécutent en parallèle, chacun imprimant son identifiant (0, 1 ou 2). Essayez de le lancer sur votre machine pour voir ces assistants en action !
2. Maîtriser les régions parallèles : "single" et "master"
Deux directives très importantes dans OpenMP : `single` et `master`. Elles vous permettent de contrôler quel thread exécute une section de code.
single : Un seul thread (choisi aléatoirement) exécute la section.
master : Seul le thread maître (le premier thread, avec l'ID 0) exécute la section.
Exemple de code :
#pragma omp single
{
printf("Hello World from threadId=%d (single)\n", threadId);
}
#pragma omp master
{
printf("Hello World from threadId=%d (master)\n", threadId);
}
Expérimentez avec ces directives pour voir comment elles influencent l'ordre des threads qui s'exécutent.
3. Barrières et synchronisation
Quand vous avez plusieurs threads, ils ne travaillent pas toujours au même rythme. C’est là qu'intervient la **barrière**. Une barrière, c'est un checkpoint où chaque thread doit attendre les autres avant de continuer.
Barrière explicite en OpenMP :
#pragma omp barrier
Tous les threads doivent atteindre ce point avant que l'exécution ne continue. Pratique pour synchroniser le travail entre eux.
4. La différence entre "critical" et "atomic"
Parfois, deux threads essaient de modifier la même variable au même moment, ce qui peut causer des erreurs. Pour résoudre cela, OpenMP nous propose deux solutions :
critical : Permet qu'un seul thread à la fois accède à une section critique du code.
atomic : Plus léger, mais ne fonctionne que sur des opérations simples (comme une addition).
Exemple de code :
#pragma omp critical
{
sum += partial_sum;
}
Exemple de code :
#pragma omp atomic
{
sum += partial_sum;
}
Utilisez `critical` pour des sections complexes et `atomic` pour des opérations simples.
Quand on écrit du code parallèle, l'ordre d'exécution des threads peut varier d'une exécution à l'autre. Cela rend le **débogage** un peu plus délicat.
Exemple :
int threadId = omp_get_thread_num();int numThreads = omp_get_num_threads();printf("Thread %d out of %d threads\n", threadId, numThreads);
Les sorties possibles pourraient être :
```Thread 0 out of 3 threadsThread 1 out of 3 threadsThread 2 out of 3 threads```
Mais l'ordre des lignes peut varier !
6. Exercice de code parallèle et optimisé
Maintenant, voici un exercice pratique. Vous devez paralléliser deux boucles tout en réduisant les frais de gestion des threads. Voici un exemple à essayer :
#include <vector>#include <omp.h>#include <chrono>#include <iostream>
int main() { int N = 10000000; // 10 millions d'éléments std::vector<double> A(N); double sum = 0.0;
auto start = std::chrono::high_resolution_clock::now();
#pragma omp parallel { // Initialisation du tableau #pragma omp for for (int i = 0; i < N; ++i) { A[i] = i; }
// Calcul de la somme #pragma omp for reduction(+:sum) for (int i = 0; i < N; ++i) { sum += A[i]; } }
auto time = std::chrono::high_resolution_clock::now() - start; std::cout << "Sum is " << sum << "\n"; std::cout << "Execution time: " << time.count() << "s\n";
return 0;}```
Optimisez le code en utilisant une seule région parallèle qui englobe les deux boucles, pour éviter de créer des threads deux fois.
7. Conclusion et exercices pratiques
OpenMP est un outil puissant qui vous permet de tirer parti du calcul parallèle de manière simple et efficace. Jouez avec les directives `parallel`, `single`, `master`, et les mécanismes de synchronisation comme les barrières.
**Exercice bonus :** Tentez de modifier le programme ci-dessus pour observer l'impact de la suppression des barrières. Quelle sera la différence dans les résultats ?
J'espère que cet article vous a donné envie de plonger dans OpenMP ! N'hésitez pas à tester les exemples, ajuster les paramètres, et surtout, amusez-vous à voir vos programmes tourner plus vite grâce au calcul parallèle !