Parallélisme : travaux dirigés et pratiques

2. Algorithmes et Métriques

Dans ce chapitre nous nous intéressons à quelques algorithmes que l'on peut paralléliser. Certains sont trivialement parallélisables, d'autres demandent un peu plus de travail et de rélexion. Le but est de se familiariser avec les techniques liées au passage du séquentiel au parallèle.

2.1. Réduction

On considère un tableau de $n$ éléments et un opérateur associatif et commutatif $⊕$ doté d'un élément neutre $I$. A partir du tableau

$$ [ a_0, a_1, ..., a_{n-1} ] $$

on désire obtenir un unique résultat :

$$ a_0 ⊕ a_1 ⊕ ... ⊕ a_{n-1} $$

Si $⊕$ est l'addition alors la réduction permet d'obtenir la somme des éléments du tableau.

On peut appliquer cet algorithme avec d'autres opérateurs comme la multiplication, minimum, maximum.

Pour que l'algorithme soit parallélisable efficacement il sufit que l'opérateur $⊕$ dispose des propriétés d'associativité et de commutativité :

2.1.1. code séquentiel

Voici le code séquentiel (et parallèle) en C++ de la réduction qui est facilement vectorisable :

  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. #include <iostream>
  9. #include <iomanip>
  10. using namespace std;
  11.  
  12. /* ==================================================================
  13.  * SUBPROGRAM
  14.  *
  15.  *      float sum_reduction( float *t, int size );
  16.  *
  17.  * WHAT
  18.  *
  19.  *  Perform sum of values of the array @param(t) of
  20.  *  @param(size) elements
  21.  *
  22.  * HOW
  23.  *
  24.  *  Simply iterate and sum each value.
  25.  *
  26.  * PARAMETERS
  27.  *
  28.  *  @paramdef(float, t) : array
  29.  *  @paramdef(int, size) : size of the array
  30.  *
  31.  * ==================================================================
  32.  */
  33. float sum_reduction( float *t, int size ) {
  34.  
  35.     float total = 0.0;
  36.    
  37.     // OpenMP directive for parallelization
  38.     #pragma omp parallel for reduction(+:total)
  39.     for (int i = 0; i < size; ++i) {
  40.         total += t[ i ];
  41.     }
  42.    
  43.     return total;
  44. }
  45.  
  46. /* ==================================================================
  47.  * SUBPROGRAM
  48.  *
  49.  *      int main(int argc, char *argv[])
  50.  *
  51.  * WHAT
  52.  *
  53.  *   Main program
  54.  *
  55.  * PARAMETERS
  56.  *
  57.  *  @paramdef(int,argc) : number of command line arguments
  58.  *  @paramdef(char **, argv) : command line arguments as C-strings
  59.  *
  60.  * ==================================================================
  61.  */
  62. int main(int argc, char *argv[]) {
  63.  
  64.     // maximum number of values in the array
  65.     const int MAX_VALUES = 10'000'000;
  66.     // array of floats
  67.     float *data;
  68.    
  69.     // allocate array
  70.     data = new float [ MAX_VALUES ];
  71.    
  72.     // fill array
  73.     for (int i=0; i <MAX_VALUES; ++i) {
  74.         data[ i ] = 1.0;
  75.     }
  76.    
  77.     // perform reduction as sum of values
  78.     float sum = sum_reduction( data, MAX_VALUES );
  79.    
  80.     // print result
  81.     cout << "sum=" << std::fixed << std::setprecision(2) << sum << endl;
  82.    
  83.  
  84.     return EXIT_SUCCESS;
  85. }
  86.  

2.1.2. code parallèle

On dispose de $K$ processeurs et on répartit les indices à traiter entre les processeurs.

reduction

Pour la répartition des indices il existe deux possibilités à moins que les indices se répartissent équitablement :

2.1.2.a  le dernier thread traite moins de valeurs que les autres

Chaque thread traite au plus $ μ_1 = (N + K -1) / K $ valeurs

 N   K   $μ_1$   values 
 100   2   50   50, 50 (=100) 
 100   3   34   34, 34, 32 (=100) 
 100   4   25   25, 25, 25, 25 (=100) 
 100   5   20   20, 20, 20, 20, 20 (=100) 
 100   6   17   17, 17, 17, 17, 17, 15 (=100) 
 100   7   15   15, 15, 15, 15, 15, 15, 10 (=100) 
 100   8   13   13, 13, 13, 13, 13, 13, 13, 9 (=100) 
 101   2   51   51, 50 (=101) 
 101   3   34   34, 34, 33 (=101) 
 101   4   26   26, 26, 26, 23 (=101) 
 101   5   21   21, 21, 21, 21, 17 (=101) 
 101   6   17   17, 17, 17, 17, 17, 16 (=101) 
 101   7   15   15, 15, 15, 15, 15, 15, 11 (=101) 
 101   8   13   13, 13, 13, 13, 13, 13, 13, 10 (=101) 
 102   2   51   51, 51 (=102) 
 102   3   34   34, 34, 34 (=102) 
 102   4   26   26, 26, 26, 24 (=102) 
 102   5   21   21, 21, 21, 21, 18 (=102) 
 102   6   17   17, 17, 17, 17, 17, 17 (=102) 
 102   7   15   15, 15, 15, 15, 15, 15, 12 (=102) 
 102   8   13   13, 13, 13, 13, 13, 13, 13, 11 (=102) 
 103   2   52   52, 51 (=103) 
 103   3   35   35, 35, 33 (=103) 
 103   4   26   26, 26, 26, 25 (=103) 
 103   5   21   21, 21, 21, 21, 19 (=103) 
 103   6   18   18, 18, 18, 18, 18, 13 (=103) 
 103   7   15   15, 15, 15, 15, 15, 15, 13 (=103) 
 103   8   13   13, 13, 13, 13, 13, 13, 13, 12 (=103) 
Exemples de répartitions
  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. #include <iostream>
  9. #include <string>
  10. #include <vector>
  11. #include <numeric>
  12. using namespace std;
  13.  
  14. /* ==================================================================
  15.  * SUBPROGRAM
  16.  *
  17.  *      void share_elements(int N, int K, vector<int>& velems);
  18.  *
  19.  * WHAT
  20.  *
  21.  *  Distribute @param(N) elements over @param(K) processors
  22.  *
  23.  * HOW
  24.  *
  25.  *  Assume that last processor will potentially have less
  26.  *  elements that others
  27.  *
  28.  * PARAMETERS
  29.  *
  30.  *  @paramdef(int, N) : number of elements
  31.  *  @paramdef(int, K) : number of processors / process / threads
  32.  *  @paramdef(vector<int>&, velems) : vector that receives for each
  33.  *          processor the number of elements it will have to treat
  34.  *
  35.  * ==================================================================
  36.  */
  37. void share_elements(int N, int K, vector<int>& velems) {
  38.  
  39.     int u_1, left = N;
  40.    
  41.     u_1 = (N + K - 1) / K;
  42.  
  43.     for (int k = 0; k < K-1; ++k) {
  44.         velems.push_back( u_1 );
  45.         left -= u_1;
  46.     }
  47.     velems.push_back( left );
  48.    
  49. }
  50.  
  51. /* ==================================================================
  52.  * SUBPROGRAM
  53.  *
  54.  *      int main(int argc, char *argv[])
  55.  *
  56.  * WHAT
  57.  *
  58.  *   Main program
  59.  *
  60.  * PARAMETERS
  61.  *
  62.  *  @paramdef(int,argc) : number of command line arguments
  63.  *  @paramdef(char **, argv) : command line arguments as C-strings
  64.  *
  65.  * ==================================================================
  66.  */
  67. int main(int argc, char *argv[]) {
  68.    
  69.     // Number of elements, i.e. size of an array
  70.     int N = 105;
  71.     // Number of processors / process / threads
  72.     int K = 6;
  73.    
  74.     if (argc > 1) N = atoi( argv[1] );
  75.     if (argc > 2) K = atoi( argv[2] );
  76.    
  77.     vector<int> v_nbr_elements;
  78.     share_elements( N, K, v_nbr_elements );
  79.    
  80.     cout << "-------------------" << endl;
  81.     for (int k = 0; k < K; ++k) {  
  82.         cout << "k=" << k << ", #elements=" << v_nbr_elements[ k ] << endl;
  83.     }
  84.    
  85.     int sum = accumulate( v_nbr_elements.begin(),
  86.         v_nbr_elements.end(),
  87.         0);
  88.        
  89.     cout << "-------------------" << endl;
  90.     cout << "sum=" << sum << endl;
  91.     cout << "-------------------" << endl;
  92.    
  93.    
  94.     return EXIT_SUCCESS;
  95.    
  96. }
  97.  
  98.  
  99.  

2.1.2.b  le dernier thread traite plus de valeurs que les autres

Chaque thread traite au moins $ μ_2 = N / K $ valeurs

 N   K   $μ_2$   values 
 100   2   50   50, 50 (=100) 
 100   3   33   33, 33, 34 (=100) 
 100   4   25   25, 25, 25, 25 (=100) 
 100   5   20   20, 20, 20, 20, 20 (=100) 
 100   6   16   16, 16, 16, 16, 16, 20 (=100) 
 100   7   14   14, 14, 14, 14, 14, 14, 16 (=100) 
 100   8   12   12, 12, 12, 12, 12, 12, 12, 16 (=100) 
 101   2   50   50, 51 (=101) 
 101   3   33   33, 33, 35 (=101) 
 101   4   25   25, 25, 25, 26 (=101) 
 101   5   20   20, 20, 20, 20, 21 (=101) 
 101   6   16   16, 16, 16, 16, 16, 21 (=101) 
 101   7   14   14, 14, 14, 14, 14, 14, 17 (=101) 
 101   8   12   12, 12, 12, 12, 12, 12, 12, 17 (=101) 
 102   2   51   51, 51 (=102) 
 102   3   34   34, 34, 34 (=102) 
 102   4   25   25, 25, 25, 27 (=102) 
 102   5   20   20, 20, 20, 20, 22 (=102) 
 102   6   17   17, 17, 17, 17, 17, 17 (=102) 
 102   7   14   14, 14, 14, 14, 14, 14, 18 (=102) 
 102   8   12   12, 12, 12, 12, 12, 12, 12, 18 (=102) 
 103   2   51   51, 52 (=103) 
 103   3   34   34, 34, 35 (=103) 
 103   4   25   25, 25, 25, 28 (=103) 
 103   5   20   20, 20, 20, 20, 23 (=103) 
 103   6   17   17, 17, 17, 17, 17, 18 (=103) 
 103   7   14   14, 14, 14, 14, 14, 14, 19 (=103) 
 103   8   12   12, 12, 12, 12, 12, 12, 12, 19 (=103) 
Exemples de répartitions
  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. #include <iostream>
  9. #include <string>
  10. #include <vector>
  11. #include <numeric>
  12. using namespace std;
  13.  
  14. /* ==================================================================
  15.  * SUBPROGRAM
  16.  *
  17.  *      void share_elements(int N, int K, vector<int>& velems);
  18.  *
  19.  * WHAT
  20.  *
  21.  *  Distribute @param(N) elements over @param(K) processors
  22.  *
  23.  * HOW
  24.  *
  25.  *  Assume that last processor will potentially have more
  26.  *  elements that others
  27.  *
  28.  * PARAMETERS
  29.  *
  30.  *  @paramdef(int, N) : number of elements
  31.  *  @paramdef(int, K) : number of processors / process / threads
  32.  *  @paramdef(vector<int>&, velems) : vector that receives for each
  33.  *          processor the number of elements it will have to treat
  34.  *
  35.  * ==================================================================
  36.  */
  37. void share_elements(int N, int K, vector<int>& velems) {
  38.  
  39.     int u_2, left = N;
  40.    
  41.     u_2 = N / K;
  42.  
  43.  
  44.     for (int k = 0; k < K-1; ++k) {
  45.         velems.push_back( u_2 );
  46.         left -= u_2;
  47.     }
  48.     velems.push_back( left );
  49. }
  50.  
  51. /* ==================================================================
  52.  * SUBPROGRAM
  53.  *
  54.  *      int main(int argc, char *argv[])
  55.  *
  56.  * WHAT
  57.  *
  58.  *   Main program
  59.  *
  60.  * PARAMETERS
  61.  *
  62.  *  @paramdef(int,argc) : number of command line arguments
  63.  *  @paramdef(char **, argv) : command line arguments as C-strings
  64.  *
  65.  * ==================================================================
  66.  */
  67. int main(int argc, char *argv[]) {
  68.    
  69.     // Number of elements, i.e. size of an array
  70.     int N = 105;
  71.     // Number of processors / process / threads
  72.     int K = 6;
  73.    
  74.     if (argc > 1) N = atoi( argv[1] );
  75.     if (argc > 2) K = atoi( argv[2] );
  76.    
  77.     vector<int> v_nbr_elements;
  78.     share_elements( N, K, v_nbr_elements );
  79.    
  80.     cout << "-------------------" << endl;
  81.     for (int k = 0; k < K; ++k) {  
  82.         cout << "k=" << k << ", #elements=" << v_nbr_elements[ k ] << endl;
  83.     }
  84.    
  85.     int sum = accumulate( v_nbr_elements.begin(),
  86.         v_nbr_elements.end(),
  87.         0);
  88.        
  89.     cout << "-------------------" << endl;
  90.     cout << "sum=" << sum << endl;
  91.     cout << "-------------------" << endl;
  92.    
  93.    
  94.     return EXIT_SUCCESS;
  95. }
  96.  
  97.  
  98.  

2.1.2.c  répartition équitable

On répartit les $N$ éléments sur les $K$ threads, il reste alors $R = N - K (N/K)$ éléments que l'on assigne successivement à chaque thread :

 N   K   values 
 100   2   50,50 
 100   3   34,33,33 
 100   4   25,25,25,25 
 100   5   20,20,20,20,20 
 100   6   17,17,17,17,16,16 
 100   7   15,15,14,14,14,14,14 
Exemples de répartitions équitables
  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. #include <iostream>
  9. #include <string>
  10. #include <vector>
  11. #include <numeric>
  12. using namespace std;
  13.  
  14. /* ==================================================================
  15.  * SUBPROGRAM
  16.  *
  17.  *      void share_elements(int N, int K, vector<int>& velems);
  18.  *
  19.  * WHAT
  20.  *
  21.  *  Distribute @param(N) elements over @param(K) processors
  22.  *
  23.  * HOW
  24.  *
  25.  *  Assume that the number of elements is well distributed over
  26.  *  the processors
  27.  *
  28.  * PARAMETERS
  29.  *
  30.  *  @paramdef(int, N) : number of elements
  31.  *  @paramdef(int, K) : number of processors / process / threads
  32.  *  @paramdef(vector<int>&, velems) : vector that receives for each
  33.  *          processor the number of elements it will have to treat
  34.  *
  35.  * ==================================================================
  36.  */
  37. void share_elements(int N, int K, vector<int>& velems) {
  38.    
  39.     int u_2 = N / K, left = N;
  40.  
  41.     int k;
  42.     for (k = 0; k < K; ++k) {
  43.         velems.push_back( u_2 );
  44.         left -= u_2;
  45.     }
  46.  
  47.     k = 0;
  48.     while (left != 0) {
  49.         ++velems[ k ];
  50.         --left;
  51.         // here the '% K' is not necessary has there are left < K
  52.         // elements to place
  53.         k = (k + 1 ) % K;
  54.     }
  55. }
  56.  
  57. /* ==================================================================
  58.  * SUBPROGRAM
  59.  *
  60.  *      int main(int argc, char *argv[])
  61.  *
  62.  * WHAT
  63.  *
  64.  *   Main program
  65.  *
  66.  * PARAMETERS
  67.  *
  68.  *  @paramdef(int,argc) : number of command line arguments
  69.  *  @paramdef(char **, argv) : command line arguments as C-strings
  70.  *
  71.  * ==================================================================
  72.  */
  73. int main(int argc, char *argv[]) {
  74.    
  75.     // Number of elements, i.e. size of an array
  76.     int N = 105;
  77.     // Number of processors / process / threads
  78.     int K = 6;
  79.    
  80.     if (argc > 1) N = atoi( argv[1] );
  81.     if (argc > 2) K = atoi( argv[2] );
  82.    
  83.     vector<int> v_nbr_elements;
  84.     share_elements( N, K, v_nbr_elements );
  85.    
  86.     cout << "-------------------" << endl;
  87.     for (int k = 0; k < K; ++k) {  
  88.         cout << "k=" << k << ", #elements=" << v_nbr_elements[ k ] << endl;
  89.     }
  90.    
  91.     int sum = accumulate( v_nbr_elements.begin(),
  92.         v_nbr_elements.end(),
  93.         0);
  94.        
  95.     cout << "-------------------" << endl;
  96.     cout << "sum=" << sum << endl;
  97.     cout << "-------------------" << endl;
  98.    
  99.    
  100.     return EXIT_SUCCESS;
  101.    
  102. }
  103.  
  104.  
  105.  
  106.  

2.1.3. Application : calcul d'intégrale

Pour calculer l'intégrale d'une fonction $f(x)$ entre $a$ et $b$ on utilise la méthode des rectangles (ou des trapèzes si on désire une meilleure précision) qui consiste à décomposer l'intervalle $[a..b]$ en un ensemble de petits rectangles dont on va calculer l'aire.

Plus le nombre de rectangles est important plus la précision du résultat sera proche de la valeur réelle.

$$ ∫_a^b f(x)\, dx = ∑↙{i=1}↖N f(a + i × δx) × δx = δx × ∑↙{i=1}↖N f(a + i × δx) $$

avec

$$ δx = {(b - a)} / N $$

integrale

2.1.3.a  code séquentiel

Le code en C++ du calcul de l'intégrale de $f(x) = x^2$ entre 0 et 3.0 est le suivant :

  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. // Compute integral of function f(x) = x^2 between X_0 and X_1
  9. // using area of rectangles
  10.  
  11. #include <iostream>
  12. #include <cmath>
  13. using namespace std;
  14.  
  15. typedef double real;
  16.  
  17. real X_0  = 1.0;
  18. real X_1  = 3.0;
  19. int STEPS = 1'000'000;
  20.  
  21. /* ==================================================================
  22.  * SUBPROGRAM
  23.  *
  24.  *      real f(real x ;
  25.  *
  26.  * WHAT
  27.  *
  28.  *  Function to integrate
  29.  *
  30.  *
  31.  * PARAMETERS
  32.  *
  33.  *  @paramdef(real, x) : abscissa
  34.  *
  35.  * ==================================================================
  36.  */
  37. real f( real x ) {
  38.     return x * x;
  39. }
  40.  
  41. /* ==================================================================
  42.  * SUBPROGRAM
  43.  *
  44.  *      real integrate(real x_0, real x_1, int steps)
  45.  *
  46.  * WHAT
  47.  *
  48.  *  Use the sum of rectangles under the function
  49.  *
  50.  * HOW
  51.  *
  52.  *  We compute the sum of (x_1 - x_0) / steps rectangles
  53.  *
  54.  * PARAMETERS
  55.  *
  56.  *  @paramdef(real, x_0) : lower bound
  57.  *  @paramdef(real, x_1) : upper bound
  58.  *  @paramdef(int, steps) : number of rectangles to use
  59.  *
  60.  * RETURN VALUE
  61.  *
  62.  *  @return(real) the sum of all rectangles that compose the
  63.  *  integral
  64.  *
  65.  * ==================================================================
  66.  */
  67. real integrate(real x_0, real x_1, int steps) {
  68.  
  69.     real dx, local;
  70.     real  delta_x = (x_1 - x_0) / steps;
  71.    
  72.     real area = 0;
  73.     for (int i = 1; i <= steps; ++i) {
  74.         area += f(x_0 + i * delta_x);
  75.     }
  76.    
  77.     return delta_x * area;
  78. }
  79.  
  80. /* ==================================================================
  81.  * SUBPROGRAM
  82.  *
  83.  *      int main(int argc, char *argv[])
  84.  *
  85.  * WHAT
  86.  *
  87.  *   Main program
  88.  *
  89.  * PARAMETERS
  90.  *
  91.  *  @paramdef(int,argc) : number of command line arguments
  92.  *  @paramdef(char **, argv) : command line arguments as C-strings
  93.  *
  94.  * ==================================================================
  95.  */
  96. int main() {
  97.  
  98.     cout << "integrate(" << X_0 << ", " << X_1 << ")=" << integrate(X_0, X_1, STEPS) << endl;
  99.    
  100.     return EXIT_SUCCESS;
  101. }
  102.  

2.1.3.b  code parallèle

Si on dispose de $K$ processeurs ou machines, on répartit les calculs sur $K$ machines

integrale parallel

Chaque $P_k, k ∊ [1..K]$ calcule en moyenne $γ = N/K$ valeurs :

  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. // Compute integral of function f(x) = x^2 between X_0 and X_1
  9. // using area of rectangles
  10.  
  11. #include <iostream>
  12. #include <cmath>
  13. using namespace std;
  14.  
  15. typedef double real;
  16.  
  17. real X_0  = 1.0;
  18. real X_1  = 3.0;
  19. int STEPS = 1'000'000;
  20.  
  21. /* ==================================================================
  22.  * SUBPROGRAM
  23.  *
  24.  *      real f(real x ;
  25.  *
  26.  * WHAT
  27.  *
  28.  *  Function to integrate
  29.  *
  30.  *
  31.  * PARAMETERS
  32.  *
  33.  *  @paramdef(real, x) : abscissa
  34.  *
  35.  * ==================================================================
  36.  */real f(real x ) {
  37.     return x * x;
  38. }
  39.  
  40. /* ==================================================================
  41.  * SUBPROGRAM
  42.  *
  43.  *      real integrate(real x_0, real x_1, int steps)
  44.  *
  45.  * WHAT
  46.  *
  47.  *  Use the sum of rectangles under the function
  48.  *
  49.  * HOW
  50.  *
  51.  *  We compute the sum of (x_1 - x_0) / steps rectangles
  52.  *
  53.  * PARAMETERS
  54.  *
  55.  *  @paramdef(real, x_0) : lower bound
  56.  *  @paramdef(real, x_1) : upper bound
  57.  *  @paramdef(int, steps) : number of rectangles to use
  58.  *
  59.  * RETURN VALUE
  60.  *
  61.  *  @return(real) the sum of all rectangles that compose the
  62.  *  integral
  63.  *
  64.  * ==================================================================
  65.  */
  66. real integrate(real x_0, real x_1, int steps) {
  67.  
  68.     real dx, local;
  69.     real  delta_x = (x_1 - x_0) / steps;
  70.    
  71.     real area = 0;
  72.    
  73.     #pragma omp parallel for reduction(+:area)
  74.     for (int i = 1; i <= steps; ++i) {
  75.         area += f(x_0 + i * delta_x);
  76.     }
  77.    
  78.     return delta_x * area;
  79. }
  80.  
  81. /* ==================================================================
  82.  * SUBPROGRAM
  83.  *
  84.  *      int main(int argc, char *argv[])
  85.  *
  86.  * WHAT
  87.  *
  88.  *   Main program
  89.  *
  90.  * PARAMETERS
  91.  *
  92.  *  @paramdef(int,argc) : number of command line arguments
  93.  *  @paramdef(char **, argv) : command line arguments as C-strings
  94.  *
  95.  * ==================================================================
  96.  */
  97. int main() {
  98.  
  99.     cout << "integrate(" << X_0 << ", " << X_1 << ")=" << integrate(X_0, X_1, STEPS) << endl;
  100.    
  101.     return EXIT_SUCCESS;
  102. }
  103.  

2.2. courbe de Julia

Les courbes ou ensembles de Julia, du mathématicien français Gaston Julia (1893 à 1978) ou ensembles de Mandelbrot du mathématicien Benoît Mandelbrot (1924-2010) sont un bon exemple d'application du parallélisme.

Etant donné un nombre complexe $z_0$ qui correspond à un point dans le plan, on associe la suite : $z_{n+1} = z^n^2 + c$, où $c$ est une constante complexe.

Le plan est alors partagé entre :

L'ensemble de Julia associé à $z_n$ est alors la frontière commune de ces deux ensembles, en fait, la ligne de démarcation entre les prisonniers et les fugitifs.

  1. #include <iostream>
  2. #include <complex>
  3. using namespace std;
  4.  
  5. double X_MIN = -1.0;
  6. double X_MAX =  1.0;
  7.  
  8. double Y_MIN = -1.0;
  9. double Y_MAX =  1.0;
  10.  
  11. double X_SIZE = X_MAX - X_MIN;
  12. double Y_SIZE = Y_MAX - Y_MIN;
  13.  
  14. const int WIDTH = 100;
  15. const int HEIGHT = 100;
  16.  
  17. int ITERATIONS = 1000;
  18. complex<double> c( 0.3, 0.5 );
  19.  
  20. double PRISONERS_LIMIT  = 4.0;
  21.  
  22. char *jset;
  23.  
  24. /**
  25.  * check if function converge or not
  26.  */
  27. bool is_prisoner(complex<double> z_0) {
  28.  
  29.     complex<double> z = z_0;
  30.     int i = 0;
  31.    
  32.     while ((std::norm(z) < PRISONERS_LIMIT) and (i < ITERATIONS)) {
  33.         z = z * z + c;
  34.         i = i + 1;
  35.     }
  36.    
  37.     return i >= ITERATIONS;
  38.  
  39. }
  40.  
  41. // -----------------------------
  42. //
  43. // -----------------------------
  44. void julia_set() {
  45.  
  46.     complex<double> z_0;
  47.    
  48.     for (int y = 0; y < HEIGHT; ++y) {
  49.         for (int x = 0; x < WIDTH; ++x) {
  50.             z_0 = complex<double>(X_MIN + ((double)x) / WIDTH * X_SIZE,
  51.                 Y_MIN + ((double)y) / HEIGHT * Y_SIZE );
  52.  
  53.  
  54.             if (is_prisoner(z_0)) {
  55.                 jset[y * WIDTH + x] = 'X';
  56.             } else {
  57.                 jset[y * WIDTH + x] = '.';
  58.             }
  59.         }
  60.     }
  61.    
  62. }
  63.  
  64. void print() {
  65.     for (int y = 0; y < HEIGHT; ++y) {
  66.         for (int x = 0; x < WIDTH; ++x) {
  67.             cout << jset[ y * WIDTH + x ];
  68.         }
  69.         cout << endl;
  70.     }
  71. }
  72.  
  73. // -----------------------------
  74. // program
  75. // -----------------------------
  76. int main(int argc, char *argv[]) {
  77.  
  78.     double factor = 1.0;
  79.    
  80.     if (argc > 1) factor = atof( argv[1] );
  81.    
  82.     X_MIN *= factor;
  83.     X_MAX *= factor;
  84.     Y_MIN *= factor;
  85.     Y_MAX *= factor;
  86.    
  87.     X_SIZE = X_MAX - X_MIN;
  88.     Y_SIZE = Y_MAX - Y_MIN;
  89.  
  90.     jset = new char [ HEIGHT * WIDTH ];
  91.     julia_set();
  92.     cout << "<html><body><pre>" << endl;
  93.     print();
  94.     cout << "</pre></body></html>" << endl;
  95.    
  96.     return EXIT_SUCCESS;
  97. }
  98.  

2.3. Scan

2.3.1. principe

L'algorithme de scan (aussi qualifié de prefix sum, cumulative sum, inclusive scan et exclusive scan) possède de nombreuses applications en informatique. Il consiste étant donnée une séquence de nombres $[a_0, a_1, ..., a_{n-1}]$ et un opérateur binaire associatif $⊕$ doté d'un élément neutre $I$, à calculer une deuxième séquence :

Vous trouverez plusieurs applications sur le site suivant : toves.org (voir notamment la section 3.3. Adding big integers)

Voici une première version séquentielle des algorithmes :

  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;
  4.  
  5. void sequential_inclusive_scan( vector<int>& x ) {
  6.     for (int i = 1; i < static_cast<int>(x.size()); ++i) {
  7.         x[ i ] += x[ i-1 ];
  8.     }
  9. }
  10.  
  11. void sequential_exclusive_scan( vector<int>& x ) {
  12.     int previous = x[0];
  13.     x[0] = 0;
  14.    
  15.     for (int i = 1; i < static_cast<int>(x.size()); ++i) {
  16.         int current = x[ i ];
  17.         x[ i ] = x[ i-1 ] + previous;
  18.         previous = current;
  19.     }
  20. }  
  21. int main() {
  22.     vector<int> x1 = { 1, 3, 5, 7, 9, 20 };
  23.     vector<int> x2 = { 1, 3, 5, 7, 9, 20 };
  24.    
  25.    
  26.     sequential_inclusive_scan( x1 );
  27.    
  28.     cout << "inclusive sacn=";
  29.     for (auto x : x1) cout << x << " ";
  30.     cout << endl;
  31.  
  32.     sequential_exclusive_scan( x2 );
  33.    
  34.     cout << "exclusive sacn=";
  35.     for (auto x : x2) cout << x << " ";
  36.     cout << endl;
  37.    
  38.     return EXIT_SUCCESS;
  39.    
  40. }
  41.  

Par exemple avec la séquence [1, 3, 5, 7, 9] et l'opérateur d'addition on obtient :

La complexité de cet algorithme est en $O(n)$

Malheureusement ces deux sous-programmes ne sont pas parallélisables directement.

2.3.2. Versions parallèles

2.3.2.a  un premier algorithme naïf

Un premier algorithme parallèle peut être exhibé mais il n'est pas très efficace : il repose sur l'utilisation de deux tableaux qui sont utilisés successivement en entrée et en sortie

Naive Scan With Two Arrays

  1. // ==================================================================
  2. //      Author: Jean-Michel RICHER
  3. //       Email: jean-michel.richer@univ-angers.fr
  4. // Institution: LERIA, Faculty of Science
  5. //              University of Angers, France
  6. // ==================================================================
  7.  
  8. #include <iostream>
  9. #include <vector>
  10. #include <cmath>
  11. #include <numeric>
  12. using namespace std;
  13.  
  14. /**
  15.  * Send vector contents to output stream
  16.  *
  17.  */
  18. template<class T>
  19. ostream& operator<<( ostream& out, vector<T>& v) {
  20.     for (auto x : v) out << x << " ";
  21.     return out;
  22. }
  23.  
  24.  
  25. /* ==================================================================
  26.  * SUBPROGRAM
  27.  *
  28.  *  vector<int>& sequential_inclusive_scan( vector<int> &x,
  29.  *              vector<int>& y );
  30.  *
  31.  * WHAT
  32.  *
  33.  *  Perform inclusive scan in parallel with two buffers
  34.  *
  35.  * HOW
  36.  *
  37.  *  We use a log2(n) algorithm, however, for the result to be correct
  38.  *  we need to add 1 to log2(n) if n > 2^log2(n). For example, if n,
  39.  *  the size of the input vector @param(in) is equal to 8, then
  40.  *  @var(log2n) = 3. But if n == 9, @var(log2n) should be set to 4.
  41.  *
  42.  *  At each step we compute 2^(d-1) that we call @var(shift) where
  43.  *  @var(d) ranges from 1 to @var(log2n).
  44.  *
  45.  *  We copy the data from the input vector @param(in) from 0 to
  46.  *  @var(shift)-1.
  47.  *  We add to the other elements of the input vector, the elements
  48.  *  that are shifted from @var(shift).
  49.  *
  50.  *
  51.  * PARAMETERS
  52.  *
  53.  *  @paramdef(vector<int>&, in) : input vector of data
  54.  *  @paramdef(vector<int>&, out) : vector used for calculations
  55.  *
  56.  *
  57.  * RETURN VALUE
  58.  *   @return(vector<int>&) reference to last modified vector
  59.  *
  60.  * ==================================================================
  61.  */
  62. vector<int>& sequential_inclusive_scan( vector<int> &x, vector<int>& y ) {
  63.    
  64.     vector<int> &in  = x;
  65.     vector<int> &out = y;
  66.    
  67.     int n = static_cast<int>( x.size() );
  68.    
  69.     // if use this, then we won't have the correct result if n
  70.     // is not a power of 2
  71.     //int log2n = round( log2( n ) );
  72.    
  73.     int log2n = 32 - __builtin_clz( n ) - 1;
  74.     if (__builtin_popcount( n ) > 1) ++log2n;
  75.    
  76.    
  77.     for (int d = 1; d <= log2n; ++d) {
  78.         int shift = 1 << (d-1);
  79.        
  80.         // copy values under shift
  81.         #pragma omp parallel for
  82.         for (int i = 0; i < shift; ++i) {
  83.             out[ i ] = in [ i ];
  84.         }
  85.        
  86.         // add values over shift
  87.         #pragma omp parallel for
  88.         for (int i = shift; i < n; ++i) {
  89.             out[ i ] = in[ i ] + in[ i - shift ];
  90.         }
  91.    
  92.         swap( in, out );
  93.     }
  94.    
  95.     return in;
  96. }
  97.  
  98.  
  99. /* ==================================================================
  100.  * SUBPROGRAM
  101.  *
  102.  *      int main(int argc, char *argv[])
  103.  *
  104.  * WHAT
  105.  *
  106.  *   Main program
  107.  *
  108.  * PARAMETERS
  109.  *
  110.  *  @paramdef(int,argc) : number of command line arguments
  111.  *  @paramdef(char **, argv) : command line arguments as C-strings
  112.  *
  113.  * ==================================================================
  114.  */
  115. int main(int argc, char *argv[]) {
  116.     vector<int> x1;
  117.     vector<int> x2;
  118.  
  119.     int n = 8;
  120.    
  121.     if (argc > 1) n = atoi( argv[1] );
  122.    
  123.     x1.resize( n );
  124.     x2.resize( n );
  125.    
  126.     //iota(x1.begin(), x1.end(), 1);
  127.     for (auto i = x1.begin(); i != x1.end(); ++i) *i = 1;
  128.    
  129.     cout << "initial data=" << x1 << endl;
  130.        
  131.     vector<int>& r = sequential_inclusive_scan( x1, x2 );
  132.    
  133.     cout << "inclusive scan=" << r << endl;
  134.  
  135.     return EXIT_SUCCESS;
  136.    
  137. }
  138.  

La complexité de cet algorithme est en $O(n × \log_2(n))$, elle augmente donc la complexité de l'algorithme séquentiel par un facteur $\log_2(n)$.

2.3.2.b  un second algorithme efficace

L'idée est d'utiliser un parcours sous forme d'arbre. On utilise deux phases appelées sweep (balayage) :

Ce genre d'algorithme se révèle moins eficace que l'algorithme séquentiel mais peut être intéressant sur des architectures de type GPU.

Up-Sweep Principle Down-Sweep Principle

a) phase up-sweep

la phase de balayage vers la haut consiste à parcourir le tableau sous la forme d'un arbre binaire en calculant des sommes partielles :

Up-Sweep

Voici une première version avec $d ∈ [1..log_2(n)]$ :

// up-sweep - version 1
for d in range(1, log2(n)) do
        
        variable shift is integer = 2^d
        
        for i in range(1, n) do
                if integer.modulo(i, shift) = 0 then
                        a[i] = a[i] + a[i - shift / 2]
                end if
        end for 
        
end for 

Cependant ajouter un if dans le code n'apporte rien pour la parallélisation. Voici donc une autre version de l'up-sweep qui sera plus simplement parallélisable:

// up-sweep - version 2
for d in range(1, log2(n)) do

        variable shift is integer = 2^d
        
        for i in parallel range(shift, n) step shift do
                a[i] = a[i] + a[i - shift / 2]
        end for 
        
end for 

b) phase down-sweep

Voici la partie la plus complexe de l'algorithme pour un scan exclusif :

// exclusive down sweep

a[n] = 0

for d in range(log2(n), 1) step -1 do

        variable shift is integer = 2^d
        
        for i in parallel range(0, n-1) step 2^(d+1) do
        
                variable tmp is integer = a[i + shift / 2 - 1]
                a[i + shift / 2 - 1 ] = a[i + shift - 1 ];
                a[i + shift - 1 ] += tmp;
                
        end for 
        
end for 

Dans le cadre du scan inclusif il faut procéder ainsi :

// inclusive down sweep 
for d in range(1, log2(n)) do 
		
		variable shift  is integer = 2^d;
		variable offset is integer = 2^(log2(n) -d - 1)
		
		for i in parallel range(n / shift, n) step  n/shift do
			  a[i + offset] += a[i];
		end for

end for

Up-Sweep

Au final la prallélisation de cet algorithme n'est pas efficace :

 Méthode   Complexité 
 Séquentiel   O(n) 
 Parallèle naïf   O(n log2(n)) 
 parallèle balayage (sweep)   O(n) 
Scan et complexité
 Méthode   Temps (s) 
 Séquentiel   1,75 
 Parallèle naïf (16 threads)   10,06 
 parallèle balayage (16 threads)   3,05 
Scan résultats pour un tableau de 2^30 éléments sur Intel Xeon Silver 4208

2.4. Les algorithmes de tri

Il est important de choisir des algorithmes qui seront parallélisables naturellement. Par exemple le tri à bulle est dificilement parallélisable.

Nous verrons trois types de tri :

2.4.1. Tri pair-impair (odd-even sort)

Le principe du tri pair-impair est de comparer les couples/paires d'éléments d'un tableau.

On recomence jusqu'à ce que le tableau soit trié.

Voici un exemple avec un tableau dont les valeurs sont trié en ordre inverse. On peut voir que les valeurs 5 et 12 aparaissent sur les diagonales lors des diférentes étapes de tri.

 étape/indice   0   1   2   3   4   5   6   7 
 initial   12   11   10   9   8   7   6   5 
 1.pair   11   12   9   10   7   8   5   6 
 1.impair   11   9   12   7   10   5   8   6 
 2.pair   9   11   7   12   5   10   6   8 
 2.impair   9   7   11   5   12   6   10   8 
 3.pair   7   9   5   11   6   12   8   10 
 3.impair   7   5   9   6   11   8   12   10 
 4.pair   5   7   6   9   8   11   10   12 
 4.impair   5   6   7   8   9   10   11   12 
Exemple tri pair-impair avec tableau trié en ordre inverse
  1. procedure odd_even_sort(t is array of integer)
  2.  
  3.     // tells whether array is sorted or not
  4.     variable is_sorted is boolean
  5.     do
  6.         is_sorted = true
  7.         // even index sort
  8.         for i in range(0,N-1) step 2 do
  9.             if t[i] > t[i+1] then
  10.                 swap(t[i], t[i+1])
  11.                 is_sorted = false
  12.             end if 
  13.         end for
  14.         // odd index sort
  15.         for i in range(1, N-2) step 2 do
  16.             if t[i] > t[i+1] then
  17.                 swap(t[i], t[i+1])
  18.                 is_sorted = false
  19.             end if 
  20.         end for
  21.     while (not is_sorted)
  22.        
  23. end procedure
  24.  

GeSHi Error: GeSHi could not find the language ezl (using path /home/jeanmichel.richer/public_html/php-geshi/geshi/) (code 2)

2.4.2. Tri fusion (merge sort)

Le tri fusion consiste :

merge sort

  1. #include <iostream>
  2. #include <cstring>  // for memcpy
  3. using namespace std;
  4.  
  5. /* ==================================================================
  6.  * SUBPROGRAM
  7.  *
  8.  *      void merge<T>(T *t, int a, int m, int b)
  9.  *
  10.  * WHAT
  11.  *   Merge two consecutive sorted subarrays into one from indices
  12.  *   [a,m-1] to [m,b-1].
  13.  *
  14.  * HOW
  15.  *   We use a temporary array of size b-a called tmp to store
  16.  *   the result and then we copy it to its destination in t
  17.  *   starting at index a.
  18.  *
  19.  * PARAMETERS
  20.  *
  21.  *  @param t array of generic type T
  22.  *  @param a index at the beginning of the array
  23.  *  @param m index on the middle of the array
  24.  *  @param b index at the end of the array
  25.  *  
  26.  *  ==================================================================
  27.  */
  28. template<class T>
  29. void merge(T *t, int a, int m, int b) {
  30.     // size of temporary array
  31.     int tmp_size = b-a;
  32.     // create temporary array to store sorted data
  33.     T *tmp = new T [ tmp_size ];
  34.    
  35.     // indices to go through subarrays
  36.     int i1, i2, l1, l2, k;
  37.    
  38.     i1 = a;
  39.     l1 = m;
  40.     i2 = m;
  41.     l2 = b;
  42.     k = 0;
  43.  
  44.     while ((i1 < l1) && (i2 < l2)) {
  45.         if (t[i1] < t[i2]) {
  46.             tmp[k++] = t[i1++];
  47.         } else if (t[i2] < t[i1]) {
  48.             tmp[k++] = t[i2++];
  49.         } else {
  50.             tmp[k++] = t[i1++];
  51.             tmp[k++] = t[i2++];
  52.         }
  53.     }
  54.  
  55.     while (i1 < l1) {
  56.         tmp[k++] = t[i1++];
  57.     }
  58.  
  59.     while (i2 < l2) {
  60.         tmp[k++] = t[i2++];
  61.     }
  62.    
  63.     memcpy( &t[a], tmp, sizeof(T) * tmp_size );
  64.    
  65.     delete [] tmp;
  66.    
  67. }
  68.  
  69.  
  70.  
  71. /* ==================================================================
  72.  * SUBPROGRAM
  73.  *  
  74.  *      void split(T *t, int a, int b);
  75.  *
  76.  * WHAT
  77.  *
  78.  *  Recursively divide an array into two subarrays and then sort
  79.  *  each subarray and merge the two sorted subarrays.
  80.  *
  81.  * HOW
  82.  *  We divide the initial array from index a to b-1 into two
  83.  *  subarrays in the middle, i.e (b+a)/2. Note that we don't
  84.  *  really divide the array, but rather use the indices to mark
  85.  *  the begining and end of the array.
  86.  *  If the array is of size 1, we stop and go up one level.
  87.  *  If the array is of size 2, we sort the two values
  88.  *  If the array is of size > 2 then we recursively divide the
  89.  *  array in two.
  90.  *
  91.  * PARAMETERS
  92.  *
  93.  *  @param t array to sort
  94.  *  @param a index at the beginning of the array
  95.  *  @param b index at the end of the array  
  96.  * 
  97.  * ==================================================================
  98.  */
  99. template<class T>
  100. void split(T *t, int a, int b) {
  101.  
  102.     if ((b - a) <= 2) {
  103.         if ((b - a) == 2) {
  104.             if (t[a] > t[a+1]) {
  105.                 swap(t[a], t[a+1]);
  106.             }
  107.         }
  108.     } else {
  109.         int middle = (b+a)/2;
  110.        
  111.         split(t, a, middle);
  112.         split(t, middle, b);
  113.        
  114.         merge(t, a, middle, b);
  115.     }
  116.  
  117. }
  118.  
  119.  
  120. /* ==================================================================
  121.  * SUBPROGRAM
  122.  *
  123.  *      merge_sort(T *t, int size);
  124.  *
  125.  * WHAT
  126.  *
  127.  *  Perform merge sort on an array @param(t) of size @param(size)
  128.  *
  129.  *
  130.  */
  131. template<class T>
  132. void merge_sort(T *t, int size) {
  133.  
  134.     split(t, 0, size);
  135.  
  136. }
  137.  
  138. /**
  139.  * Programme principal
  140.  */
  141. int main() {
  142.     int size = 17;
  143.     int *tab = new int [ size ];
  144.    
  145.     for (int i=0; i<size; ++i) tab[i] = size-i;
  146.    
  147.     merge_sort(tab, size);
  148.    
  149.     for (int i=0; i<size; ++i) cout << tab[i] << " ";
  150.     cout << endl;
  151.     return EXIT_SUCCESS;
  152. }
  153.  
  154.  
  155.  

2.4.3. Tri bitonique (bitonic sort)

Voir l'article Tri bitonique de Wikipédia en français.

bitonic sort
Exemple des comparaisons à réaliser (image issue de Wikipedia)

Le tableau doit avoir une taille égale à une puissance de 2.


// note that the array must have a size equal to a power of 2
variable dim is integer = 5
variable arr is array [0..2^dim-1] of integer

procedure bitonic_sort(t is output array of integer)

	variable log2_n is integer = log(size) / log(2)
	
	for i in range(0, log2_n-1) do
		for j in range(0, i) do
			compare(t, i, j, t.size())
		end for
	end for
	
end procedure

procedure compare(t is output array of integer, p, q, size are integer) {

	variable d is integer = 1 << (p-q)
	variable up is boolean

	for i in range(0, size-1) do
		if ((i >> p) & 2) = 0 then
			up = true
		else
			up = false

		if (i & d) = 0 then
			if (t[i] > t[i + d]) = up then
				swap(t[i], t[i + d])	
			end if
		end if
	end for
	
end procedure

2.5. Métriques : accélération, efficacité, loi de Amdhal

Voir ce document pour de plus amples explications.

2.5.1. Amélioration

Pour comparer deux implantations (implantation de REFérence et OPTimisée), on peut définir trois valeurs

Accélération et pourcentage d'amélioration par rapport à un temps de référence de 100
$T_{OPT}$100 90 80 70 60 50 40 30 20 10
accélération $(acc)$1.001.111.251.431.672.002.503.335.0010.00
pourcentage $(pct)$ 0%-10%-20%-30%-40%-50%-60%-70%-80%-90%

2.5.2. Loi de Amdahl

L'application de la loi de Amdahl (1967) en calcul parallèle permet de prédire l'accélération théorique que l'on peut espérer obtenir en utilisant plusieurs processeurs.

Cette loi peut être vue come un outil d'aide à la décision : conaissant le pourcentage de code parallélisable, on peut déterminer combien de processeurs/coeurs aporteront un gain maximal ou s'il est intéressant de passer de 2 à 4, de 4 à 8 processeurs pour diminuer significativement le temps de calcul.

Soit un programme qui s'exécute en $T$ secondes et soient $Seq$ et $Par$ respectivement le pourcentage de temps passé en séquentiel et en parallèle, alors le temps d'exécution par un seul processeur $(T_1)$ est défini par: $$T_1 = Seq × T + Par × T = (1 - Par) × T + Par × T $$

sachant que $Par + Seq = 1$. Par exemple si $Par=0.92$ alors 92% du temps est passé dans la section de code parallèle.

Si on utilise $K$ processeurs alors seule la partie parallèle en bénéficie. Le temps d'exécution $T_K$ pour $K$ processeurs est défini par : $$ T_K = Seq × T + Par × T / K $$

L'accélération pour $K$ processeurs est donnée par $acc = T_1 / T_K$ est alors : $$ {T_1}/{T_K} = {(1 - Par) × T + Par × T}/{(1 - Par) × T + Par × T / K} = 1 / {1 - Par + {Par}/ K} = 1 / {Seq + {1 - Seq}/ K} = 1/{Seq + {Par}/{K}} $$

Loi de Amdahl, $acc$ (Accélération) en fonction de $K$ (nombre de threads) pour $Par=0.92$
$K$ $Acc$ % $Δ$ %
21.85-4646
43.23-6923
85.13-8011
105.81-833
126.38-841
207.94-873

Notons que plus $Par$ est proche de 1, plus $acc$ est grand quand $K$ augmente, cf. l'image ci-dessous où $R=acc$ et $S=K$ le nombre de processeurs.

Amdhal's Law

K \ Par0.50.60.70.80.90.920.950.99
21.33 1.43 1.54 1.67 1.82 1.85 1.90 1.98
41.60 1.82 2.11 2.50 3.08 3.23 3.48 3.88
81.78 2.11 2.58 3.33 4.71 5.13 5.93 7.48
101.82 2.17 2.70 3.57 5.26 5.81 6.90 9.17
121.85 2.22 2.79 3.75 5.71 6.38 7.74 10.81
161.88 2.29 2.91 4.00 6.40 7.27 9.14 13.91
201.90 2.33 2.99 4.17 6.90 7.94 10.26 16.81
321.94 2.39 3.11 4.44 7.80 9.20 12.55 24.43
641.97 2.44 3.22 4.71 8.77 10.60 15.42 39.26
1281.98 2.47 3.27 4.85 9.34 11.47 17.41 56.39
2561.99 2.49 3.30 4.92 9.66 11.96 18.62 72.11
Tableau de valeurs pour la loi de Amdahl

Par exemple avec 99% du temps d'exécution parallélisable, on peut diviser le temps d'exécution par un facteur 16.81 lorsque l'on utilise 20 threads/processeurs.

Attention cependant, dans certains cas la limite théorique peut ne pas être atteinte car la taille des donées possède une influence sur le nombre de threads (exemple Maximum de Parcimonie $Par = 0.92$ limite atteinte pour $K=10$ à $12$)

2.5.3. Loi de Gustafson

La loi de Amdhal concerne l'accélération à travail ou charge constante. La loi de Gustafson prédit l’accélération théorique maximale obtenue en n'imposant pas que la taille du travail soit fixe.

D'après Wikipédia : Plus le nombre de données traitées en parallèles est grand, plus l’utilisation d'un grand nombre de processeurs est efficace. Mais surtout, il n'y a pas de limite théorique à l'accélération : on peut mettre autant de données que l'on veut, celles-ci sont toutes traitées par un processeur simultanément et le temps mis pour traiter s données sur s processeurs sera identique au temps mis pour traiter une donnée sur un seul processeur. Aucune limite n'existe concernant la quantité de données traitables simultanément, et donc au gain que l'on peut obtenir.

Elle traduit le fait que l'on peut traiter plus de données dans le même temps en augmentant le nombre de processeurs.

$$acc = Seq + K × Par = K + Seq × (1 - K)$$

K / Par0.50.60.70.80.90.920.950.99
21.50 1.60 1.70 1.80 1.90 1.92 1.95 1.99
42.50 2.80 3.10 3.40 3.70 3.76 3.85 3.97
84.50 5.20 5.90 6.60 7.30 7.44 7.65 7.93
105.50 6.40 7.30 8.20 9.10 9.28 9.55 9.91
126.50 7.60 8.70 9.80 10.90 11.12 11.45 11.89
2010.50 12.40 14.30 16.20 18.10 18.48 19.05 19.81
Tableau de valeurs pour la loi de Gustafson

2.5.4. Métrique de Karp–Flatt (1990)

Cette métrique permet de prendre en compte les couts de parallélisation.

Soit l'accélération $acc$ obtenue pour $K > 1$ processeurs, on calcule la métrique de Karp–Flatt $e$ comme suit : $$e= {{1}/{acc} - {1}/{K}}/{1 - {1}/{K}}$$

Plus $e$ est petit, meilleure est la parallélisation.

 #threads (K)   1   2   4   8   10   12   14   16   18   20   40 
 temps (s)   207   120   67   41   36   36   39   43   47   49   60 
 accélération   -   1.725   3.090   5.049   5.750   5.750   5.308   4.814   4.404   4.224   3.450 
 Karp–Flatt   -   0.159   0.098   0.084   0.082   0.099   0.126   0.155   0.182   0.197   0.272 
Temps d'exécution (s) sur Xeon E5 2670