Survol de Threading Building Blocks (TBB)

La bibliothèque Threading Building Blocks, ou TBB, développée chez Intel, est une bibliothèque générique portable à code ouvert en C++.

Présentation succincte

La bibliothèque TBB propose des métaphores OO de programmation parallèle. Intel offre l'avant-dernière version de TBB en version à code ouvert, mais il faut payer pour avoir la toute dernière et le support qui lui est associé.

TBB propose sa version parallèle de quelques algorithmes classiques, et encadre le code client dans sa démarche de parallélisation. Comme la plupart des moteurs de parallélisme, TBB profite au maximum de structures de données Cache-Friendly comme les tableaux et les vecteurs.

Comment l'utiliser

Il suffit de télécharger la version la plus récente de TBB, de l'installer et d'inclure les en-têtes pertinents. La documentation est tout à fait convenable, et il existe des livres pour vous guider, incluant ../../Liens/Suggestions-lecture.html#intel_threading_building_blocks

Exemple concret

Petit exemple illustratif :

#include "../../../../tbb44_20151010oss/include/tbb/parallel_for.h"
#include "../../../../tbb44_20151010oss/include/tbb/parallel_reduce.h"
#include "../../../../tbb44_20151010oss/include/tbb/parallel_while.h"
#include "../../../../tbb44_20151010oss/include/tbb/blocked_range.h"
#include "../../../../tbb44_20151010oss/include/tbb/task_scheduler_init.h"
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
#include <cmath>
#include <cassert>
#include <random>
#include <numeric>
#include <chrono>
using namespace std;
using namespace tbb;
using namespace std::chrono;

class racines_carrees {
   vector<double> &src;
   mutable vector<double> &dest;
public:
   using size_type = vector<double>::size_type;
   racines_carrees(vector<double> &src, vector<double> &dest)
      : src{src}, dest{dest}
   {
      assert(src.size() <= dest.size());
   }
   void operator()(const blocked_range<size_type> &r) const {
      for (auto i = begin(r); i != end(r); ++i)
         dest[i] = sqrt(src[i]);
   }
};

class cumul {
   double *src;
   double somme;
public:
   cumul(double *src)
      : src{src}, somme{}
   {
   }
   cumul(cumul &c, split)
      : src{c.src}, somme{}
   {
   }
   void join(const cumul &c) {
      somme += c.valeur();
   }
   template <class T>
      void operator()(const blocked_range<T> &r) {
         for (auto i = begin(r); i != end(r); ++i)
            somme += src[i];
      }
   double valeur() const noexcept {
      return somme; }
   }
};

class list_wrapper {
   struct noeud;
public:
   using nodeptr = noeud*;
private:
   struct noeud {
      double val;
      nodeptr next;
      noeud(double val)
         : val{val}, next{}
      {
      }
   };
   nodeptr tete;
public:
   template <class It>
      list_wrapper(It debut, It fin)
         : tete{}
      {
         if (debut == fin) return;
         try {
            tete = new noeud{*debut};
            ++debut;
            noeud *q = tete;
            for(; debut != fin; ++debut) {
               auto p = new noeud{*debut};
               q->next = p;
               q = p;
            }
         } catch(...) {
            clear();
            throw;
         }
      }
   bool pop_if_present(nodeptr &p) noexcept {
      if (!p) return false;
      p = p -> next;
      return true;
   }
   void clear() noexcept {
      while (tete) {
         auto p = tete->next;
         delete tete;
         tete = p;
      }
   }
   ~list_wrapper() {
      clear();
   }
   friend class chercher_plus_gros;
};

class chercher_plus_gros {
   mutable list_wrapper::noeud *cur;
public:
   chercher_plus_gros() noexcept
      : cur{}
   {
   }
   void operator()(list_wrapper::noeud *candidat) const noexcept {
      if (!cur || candidat->val > valeur())
         cur = candidat;
   }
   double valeur() const noexcept {
      return cur->val;
   }
};

int main() {
   task_scheduler_init init;
   enum { N = 50'000'000 };
   clog.rdbuf(nullptr);
   vector<double> src(N);
   //
   // parallel_for
   //
   iota(begin(src), end(src), 0.0);
   {
      vector<double> dest(N);
      auto avant = system_clock::now();
      parallel_for(
         blocked_range<vector<double>::size_type>(0, src.size()),
         racines_carrees(src, dest)
      );
      auto apres = system_clock::now();
      cout << "Version parallele: " << N << " racines carrees en "
           << duration_cast<milliseconds>(apres-avant).count() << " ms." << endl;
      clog << "Valeur bidon: " << dest[rand() % dest.size()] << endl;
   }
   iota(begin(src), end(src), 0.0);
   {
      vector<double> dest(N);
      auto avant = system_clock::now();
      for (vector<double>::size_type i = 0; i != src.size(); ++i)
         dest[i] = sqrt(src[i]);
      auto apres = system_clock::now();
      cout << "Version sequentielle: " << N << " racines carrees en "
           << duration_cast<milliseconds>(apres-avant).count() << " ms." << endl;
      clog << "Valeur bidon: " << dest[rand() % dest.size()] << endl;
   }

   //
   // parallel_reduce
   //
   {
      auto avant = system_clock::now();
      double somme = {};
      cumul c{&src[0]};
      parallel_reduce(
         blocked_range<vector<double>::size_type>(0, src.size()), c
      );
      auto apres = system_clock::now();
      cout << "Version parallele: " << N << " additions en "
           << duration_cast<milliseconds>(apres-avant).count() << " ms." << endl;
      clog << "Valeur bidon: " << c.valeur() << endl;
   }
   {
      auto avant = system_clock::now();
      double somme = {};
      for (vector<double>::size_type i = 0; i != src.size(); ++i)
         somme += src[i];
      auto apres = system_clock::now();
      cout << "Version sequentielle: " << N << " additions en "
           << duration_cast<milliseconds>(apres-avant).count() << " ms." << endl;
      clog << "Valeur bidon: " << somme << endl;
   }
}

Ceci compare les opérations avec TBB et selon d'autres approches. Bien que TBB propose ses propres métaphores (Split Constructors, blocked_range<T> et autres), utiliser TBB n'est pas particulièrement dépaysant.

Ce que ça donne à l'exécution

Les chiffres montrent clairement les avantages de la version parallèle sur la version séquentielle :

Version parallele: 50000000 racines carrees en 62 ms.
Version sequentielle: 50000000 racines carrees en 211 ms.
Version parallele: 50000000 additions en 40 ms.
Version sequentielle: 50000000 additions en 134 ms.

Évidemment, ces chiffres sont à titre indicatif seulement. Selon les architectures et selon les circonstances, la qualité des résultats variera.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !