L'API OpenMP, permet de transformer, typiquement par des #pragma (à tout le moins en C et en C++), des segments de code a priori séquentiels en équivalents parallèles.
Paralléliser du code avec OpenMP est simple mais contraignant (boucles for avec indice de type int et test de poursuite utilisant l'opérateur <, par exemple). Par contre, la parallélisation de sections de code se fait par annotation du code, tout simplement, ce qui est alléchant pour certains programmes.
OpenMP est très répandu, plusieurs compilateurs le supportant implicitement.
Il suffit d'inclure <omp.h> et de compiler avec les options requises pour qu'OpenMP soit supporté (en général, on parle d'une option de compilation), puis d'annoter le code visé. Notez toutefois que les annotations d'OpenMP sont un petit langage en soi, et qu'il vaut mieux lire la documentation attentivement avant de procéder – des annotations mal organisées peuvent mener à une pessimisation du code et à une version parallèle plus lente que la version séquentielle.
Petit exemple illustratif :
#include <iostream>
#include <algorithm>
#include <cassert>
#include <vector>
#include <cmath>
#include <string>
#include <numeric>
#include <random>
#include <chrono>
#include <thread>
using namespace std;
using namespace std::chrono;
template <class It>
float somme_racines(It debut, It fin) {
float somme = {};
for (; debut != fin; ++debut)
somme += sqrt(*debut);
return somme;
}
template <class It>
float somme_racines_stl(It debut, It fin) {
return accumulate(debut, fin, float{}, [](float so_far, float cur) {
return so_far + sqrt(cur);
});
}
template <class It>
void ajout(It debut, It fin, float ajout) {
for (; debut != fin; ++debut)
*debut += ajout;
}
template <class It>
void ajout_stl(It debut, It fin, float ajout) {
transform(debut, fin, debut, [=](float p) {
return p + ajout;
});
}
#include <omp.h> // compiler avec /openmp
float somme_racines_parallele(const float *p, int n) { // pas le choix: faut un entier n signé
float somme = {};
//
// Sans la clause reduction, OpenMP cherchera à synchroniser les accès
// concurrents à somme (plutôt que d'introduire des variables temporaires
// locales aux threads et de faire une réduction à proprement dit), et
// l'exécution parallèle deviendra beaucoup plus lente que la séquentielle
//
#pragma omp parallel for reduction(+ : somme)
for (int i = 0; i < n; ++i) // notez le test
somme += sqrt(p[i]);
return somme;
}
void ajout_parallele(float *p, int n, float ajout) { // pas le choix: faut un entier n signé
float somme = {};
#pragma omp parallel for
for (int i = 0; i < n; ++i) // notez le test
p[i] += ajout;
}
template <class F>
auto tester(F f, const string & msg, ostream & out) {
auto avant = system_clock::now();
auto resultat = f();
auto apres = system_clock::now();
out << msg << ", " << duration_cast<milliseconds>(apres-avant).count() << " ms." << endl;
return resultat;
}
int main() {
enum { N = 100'000'000 };
vector<float> v(N);
generate(begin(v), end(v), []() { return rand() + 0.5f; });
clog.rdbuf(nullptr);
cout << "Tests sur " << N << " echantillons de type float, ordinateur muni de "
<< thread::hardware_concurrency() << " coeurs" << endl;
for(int i = 1; i <= 16; ++i) {
omp_set_num_threads(i);
//
// Les affichages bidons servent a prevenir certaines optimisations
// en provoquant un effet de bord mensonger
//
clog << tester([v]() mutable {
auto somme = somme_racines_parallele(&v[0], v.size());
ajout_parallele(&v[0], v.size(), somme);
somme = somme_racines_parallele(&v[0], v.size());
ajout_parallele(&v[0], v.size(), somme);
somme = somme_racines_parallele(&v[0], v.size());
return somme;
}, "Operations paralleles, " + to_string(i) + " thread(s) OMP", cout) << endl; // prévenir l'optimisation
clog << tester([v]() mutable {
auto somme = somme_racines(begin(v), end(v));
ajout(begin(v), end(v), somme);
somme = somme_racines(begin(v), end(v));
ajout(begin(v), end(v), somme);
somme = somme_racines(begin(v), end(v));
return somme;
}, "Operations sequentielles", cout) << endl; // prévenir l'optimisation
clog << tester([v]() mutable {
auto somme = somme_racines_stl(begin(v), end(v));
ajout_stl(begin(v), end(v), somme);
somme = somme_racines_stl(begin(v), end(v));
ajout_stl(begin(v), end(v), somme);
somme = somme_racines_stl(begin(v), end(v));
return somme;
}, "Operations sequentielles (saveur STL)", cout) << endl; // prévenir l'optimisation
}
}
Il est manifestement possible d'en arriver à des solutions homogènes pour le code parallèle avec OpenMP et pour le code séquentiel, mais cela implique d'enrober les appels.
Les chiffres montrent clairement les avantages de la version parallèle sur la version séquentielle :
Tests sur 100000000 echantillons de type float, ordinateur muni de 8 coeurs
Operations paralleles, 1 thread(s) OMP, 1588 ms.
Operations sequentielles, 1590 ms.
Operations sequentielles (saveur STL), 1592 ms.
Operations paralleles, 2 thread(s) OMP, 827 ms.
Operations sequentielles, 1600 ms.
Operations sequentielles (saveur STL), 1624 ms.
Operations paralleles, 3 thread(s) OMP, 633 ms.
Operations sequentielles, 1602 ms.
Operations sequentielles (saveur STL), 1594 ms.
Operations paralleles, 4 thread(s) OMP, 552 ms.
Operations sequentielles, 1594 ms.
Operations sequentielles (saveur STL), 1596 ms.
Operations paralleles, 5 thread(s) OMP, 522 ms.
Operations sequentielles, 1590 ms.
Operations sequentielles (saveur STL), 1595 ms.
Operations paralleles, 6 thread(s) OMP, 518 ms.
Operations sequentielles, 1591 ms.
Operations sequentielles (saveur STL), 1599 ms.
Operations paralleles, 7 thread(s) OMP, 507 ms.
Operations sequentielles, 1588 ms.
Operations sequentielles (saveur STL), 1594 ms.
Operations paralleles, 8 thread(s) OMP, 508 ms.
Operations sequentielles, 1593 ms.
Operations sequentielles (saveur STL), 1593 ms.
Operations paralleles, 9 thread(s) OMP, 703 ms.
Operations sequentielles, 1587 ms.
Operations sequentielles (saveur STL), 1593 ms.
Operations paralleles, 10 thread(s) OMP, 602 ms.
Operations sequentielles, 1591 ms.
Operations sequentielles (saveur STL), 1595 ms.
Operations paralleles, 11 thread(s) OMP, 621 ms.
Operations sequentielles, 1589 ms.
Operations sequentielles (saveur STL), 1592 ms.
Operations paralleles, 12 thread(s) OMP, 653 ms.
Operations sequentielles, 1589 ms.
Operations sequentielles (saveur STL), 1593 ms.
Operations paralleles, 13 thread(s) OMP, 649 ms.
Operations sequentielles, 1588 ms.
Operations sequentielles (saveur STL), 1591 ms.
Operations paralleles, 14 thread(s) OMP, 686 ms.
Operations sequentielles, 1589 ms.
Operations sequentielles (saveur STL), 1598 ms.
Operations paralleles, 15 thread(s) OMP, 640 ms.
Operations sequentielles, 1589 ms.
Operations sequentielles (saveur STL), 1594 ms.
Operations paralleles, 16 thread(s) OMP, 664 ms.
Operations sequentielles, 1590 ms.
Operations sequentielles (saveur STL), 1595 ms.
Évidemment, ces chiffres sont à titre indicatif seulement. Selon les architectures et selon les circonstances, la qualité des résultats variera.
Quelques liens pour enrichir le propos.