Pourquoi la sommation groupée est-elle plus lente avec les groupes triés que les groupes non triés?

27

J'ai 2 colonnes d'entiers délimités par des tabulations, dont la première est un entier aléatoire, la seconde un entier identifiant le groupe, qui peut être généré par ce programme. ( generate_groups.cc)

#include <cstdlib>
#include <iostream>
#include <ctime>

int main(int argc, char* argv[]) {
  int num_values = atoi(argv[1]);
  int num_groups = atoi(argv[2]);

  int group_size = num_values / num_groups;
  int group = -1;

  std::srand(42);

  for (int i = 0; i < num_values; ++i) {
    if (i % group_size == 0) {
      ++group;
    }
    std::cout << std::rand() << '\t' << group << '\n';
  }

  return 0;
}

J'utilise ensuite un deuxième programme ( sum_groups.cc) pour calculer les sommes par groupe.

#include <iostream>
#include <chrono>
#include <vector>

// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
  for (size_t i = 0; i < n; ++i) {
    p_out[p_g[i]] += p_x[i];
  }
}

int main() {
  std::vector<int> values;
  std::vector<int> groups;
  std::vector<int> sums;

  int n_groups = 0;

  // Read in the values and calculate the max number of groups
  while(std::cin) {
    int value, group;
    std::cin >> value >> group;
    values.push_back(value);
    groups.push_back(group);
    if (group > n_groups) {
      n_groups = group;
    }
  }
  sums.resize(n_groups);

  // Time grouped sums
  std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
  for (int i = 0; i < 10; ++i) {
    grouped_sum(values.data(), groups.data(), values.size(), sums.data());
  }
  std::chrono::system_clock::time_point end = std::chrono::system_clock::now();

  std::cout << (end - start).count() << std::endl;

  return 0;
}

Si je lance ensuite ces programmes sur un ensemble de données de taille donnée, puis mélangez l'ordre des lignes du même ensemble de données, les données mélangées calculent les sommes ~ 2x ou plus rapidement que les données ordonnées.

g++ -O3 generate_groups.cc -o generate_groups
g++ -O3 sum_groups.cc -o sum_groups
generate_groups 1000000 100 > groups
shuf groups > groups2
sum_groups < groups
sum_groups < groups2
sum_groups < groups2
sum_groups < groups
20784
8854
8220
21006

Je m'attendais à ce que les données d'origine triées par groupe aient une meilleure localisation des données et soient plus rapides, mais j'observe le comportement opposé. Je me demandais si quelqu'un pouvait en faire l'hypothèse?

Jim
la source
1
Je ne sais pas, mais vous écrivez dans des éléments hors de portée du vecteur de sommes - si vous avez fait la chose normale et passé des références à des vecteurs au lieu de pointeurs vers les éléments de données, puis utilisé .at()ou un mode de débogage operator[]qui fait des limites vérifier que vous verriez.
Shawn
Avez-vous vérifié que le fichier "groups2" contient toutes vos données et qu'il est entièrement lu et traité? Y a-t-il peut-être un caractère EOF au milieu quelque part?
1201ProgramAlarm
2
Le programme a un comportement indéfini car vous ne redimensionnez jamais sum. Au lieu de sums.reserve(n_groups);vous, vous devez appeler sums.resize(n_groups);- c'est ce que @Shawn faisait allusion.
Eugene
1
Notez (voir par exemple ici ou ici ) qu'un vecteur de paires, au lieu de deux vecteurs (valeurs et groupe), se comporte comme prévu.
Bob__
1
Vous avez trié les données sur les valeurs, non? Mais cela trie également les groupes, ce qui a un impact sur l'xpression p_out[p_g[i]] += p_x[i];. Peut-être que dans l'ordre brouillé d'origine, les groupes présentent en fait un bon regroupement en ce qui concerne l'accès au p_outtableau. Le tri des valeurs peut entraîner un mauvais modèle d'accès indexé sur le groupe p_out.
Kaz

Réponses:

33

Installer / ralentir

Tout d'abord, le programme s'exécute à peu près au même moment, peu importe:

sumspeed$ time ./sum_groups < groups_shuffled 
11558358

real    0m0.705s
user    0m0.692s
sys 0m0.013s

sumspeed$ time ./sum_groups < groups_sorted
24986825

real    0m0.722s
user    0m0.711s
sys 0m0.012s

La plupart du temps est passé dans la boucle d'entrée. Mais puisque nous sommes intéressés par le grouped_sum(), ignorons cela.

Changer la boucle de référence de 10 à 1000 itérations grouped_sum()commence à dominer le temps d'exécution:

sumspeed$ time ./sum_groups < groups_shuffled 
1131838420

real    0m1.828s
user    0m1.811s
sys 0m0.016s

sumspeed$ time ./sum_groups < groups_sorted
2494032110

real    0m3.189s
user    0m3.169s
sys 0m0.016s

perf diff

Maintenant, nous pouvons utiliser perfpour trouver les endroits les plus chauds de notre programme.

sumspeed$ perf record ./sum_groups < groups_shuffled
1166805982
[ perf record: Woken up 1 times to write data ]
[kernel.kallsyms] with build id 3a2171019937a2070663f3b6419330223bd64e96 not found, continuing without symbols
Warning:
Processed 4636 samples and lost 6.95% samples!

[ perf record: Captured and wrote 0.176 MB perf.data (4314 samples) ]

sumspeed$ perf record ./sum_groups < groups_sorted
2571547832
[ perf record: Woken up 2 times to write data ]
[kernel.kallsyms] with build id 3a2171019937a2070663f3b6419330223bd64e96 not found, continuing without symbols
[ perf record: Captured and wrote 0.420 MB perf.data (10775 samples) ]

Et la différence entre eux:

sumspeed$ perf diff
[...]
# Event 'cycles:uppp'
#
# Baseline  Delta Abs  Shared Object        Symbol                                                                  
# ........  .........  ...................  ........................................................................
#
    57.99%    +26.33%  sum_groups           [.] main
    12.10%     -7.41%  libc-2.23.so         [.] _IO_getc
     9.82%     -6.40%  libstdc++.so.6.0.21  [.] std::num_get<char, std::istreambuf_iterator<char, std::char_traits<c
     6.45%     -4.00%  libc-2.23.so         [.] _IO_ungetc
     2.40%     -1.32%  libc-2.23.so         [.] _IO_sputbackc
     1.65%     -1.21%  libstdc++.so.6.0.21  [.] 0x00000000000dc4a4
     1.57%     -1.20%  libc-2.23.so         [.] _IO_fflush
     1.71%     -1.07%  libstdc++.so.6.0.21  [.] std::istream::sentry::sentry
     1.22%     -0.77%  libstdc++.so.6.0.21  [.] std::istream::operator>>
     0.79%     -0.47%  libstdc++.so.6.0.21  [.] __gnu_cxx::stdio_sync_filebuf<char, std::char_traits<char> >::uflow
[...]

Plus de temps main(), ce qui a probablement grouped_sum()marqué. Génial, merci beaucoup, perf.

perf annoter

Y a-t-il une différence dans l'endroit où le temps est passé à l' intérieur main() ?

Mélangé:

sumspeed$ perf annotate -i perf.data.old
[...]
            // This is the function whose performance I am interested in
            void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
              for (size_t i = 0; i < n; ++i) {
       180:   xor    %eax,%eax
              test   %rdi,%rdi
             je     1a4
              nop
                p_out[p_g[i]] += p_x[i];
  6,88 190:   movslq (%r9,%rax,4),%rdx
 58,54        mov    (%r8,%rax,4),%esi
            #include <chrono>
            #include <vector>
       
            // This is the function whose performance I am interested in
            void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
              for (size_t i = 0; i < n; ++i) {
  3,86        add    $0x1,%rax
                p_out[p_g[i]] += p_x[i];
 29,61        add    %esi,(%rcx,%rdx,4)
[...]

Trié:

sumspeed$ perf annotate -i perf.data
[...]
            // This is the function whose performance I am interested in
            void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
              for (size_t i = 0; i < n; ++i) {
       180:   xor    %eax,%eax
              test   %rdi,%rdi
             je     1a4
              nop
                p_out[p_g[i]] += p_x[i];
  1,00 190:   movslq (%r9,%rax,4),%rdx
 55,12        mov    (%r8,%rax,4),%esi
            #include <chrono>
            #include <vector>
       
            // This is the function whose performance I am interested in
            void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
              for (size_t i = 0; i < n; ++i) {
  0,07        add    $0x1,%rax
                p_out[p_g[i]] += p_x[i];
 43,28        add    %esi,(%rcx,%rdx,4)
[...]

Non, ce sont les deux mêmes instructions qui dominent. Ils prennent donc beaucoup de temps dans les deux cas, mais sont encore pires lorsque les données sont triées.

perf stat

D'accord. Mais nous devrions les exécuter le même nombre de fois, donc chaque instruction doit ralentir pour une raison quelconque. Voyons voir ce qui se perf statdit.

sumspeed$ perf stat ./sum_groups < groups_shuffled 
1138880176

 Performance counter stats for './sum_groups':

       1826,232278      task-clock (msec)         #    0,999 CPUs utilized          
                72      context-switches          #    0,039 K/sec                  
                 1      cpu-migrations            #    0,001 K/sec                  
             4 076      page-faults               #    0,002 M/sec                  
     5 403 949 695      cycles                    #    2,959 GHz                    
       930 473 671      stalled-cycles-frontend   #   17,22% frontend cycles idle   
     9 827 685 690      instructions              #    1,82  insn per cycle         
                                                  #    0,09  stalled cycles per insn
     2 086 725 079      branches                  # 1142,639 M/sec                  
         2 069 655      branch-misses             #    0,10% of all branches        

       1,828334373 seconds time elapsed

sumspeed$ perf stat ./sum_groups < groups_sorted
2496546045

 Performance counter stats for './sum_groups':

       3186,100661      task-clock (msec)         #    1,000 CPUs utilized          
                 5      context-switches          #    0,002 K/sec                  
                 0      cpu-migrations            #    0,000 K/sec                  
             4 079      page-faults               #    0,001 M/sec                  
     9 424 565 623      cycles                    #    2,958 GHz                    
     4 955 937 177      stalled-cycles-frontend   #   52,59% frontend cycles idle   
     9 829 009 511      instructions              #    1,04  insn per cycle         
                                                  #    0,50  stalled cycles per insn
     2 086 942 109      branches                  #  655,014 M/sec                  
         2 078 204      branch-misses             #    0,10% of all branches        

       3,186768174 seconds time elapsed

Une seule chose ressort: les cycles bloqués-frontend .

D'accord, le pipeline d'instructions est au point mort. Dans le frontend. Exactement ce que cela signifie varie probablement entre les microarchictectures.

J'ai une supposition, cependant. Si vous êtes généreux, vous pourriez même appeler cela une hypothèse.

Hypothèse

En triant l'entrée, vous augmentez la localité des écritures. En fait, ils seront très locaux; presque tous les ajouts que vous effectuez écriront au même emplacement que le précédent.

C'est super pour le cache, mais pas super pour le pipeline. Vous introduisez des dépendances de données, empêchant la prochaine instruction d'ajout de continuer jusqu'à ce que l'ajout précédent soit terminé (ou a autrement rendu le résultat disponible pour les instructions suivantes )

C'est ton problème.

Je pense.

Le réparer

Vecteurs de somme multiples

En fait, essayons quelque chose. Que se passerait-il si nous utilisions plusieurs vecteurs de somme, en basculant entre eux pour chaque ajout, puis en les additionnant à la fin? Cela nous coûte un peu de localité, mais devrait supprimer les dépendances de données.

(le code n'est pas joli; ne me jugez pas, internet !!)

#include <iostream>
#include <chrono>
#include <vector>

#ifndef NSUMS
#define NSUMS (4) // must be power of 2 (for masking to work)
#endif

// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int** p_out) {
  for (size_t i = 0; i < n; ++i) {
    p_out[i & (NSUMS-1)][p_g[i]] += p_x[i];
  }
}

int main() {
  std::vector<int> values;
  std::vector<int> groups;
  std::vector<int> sums[NSUMS];

  int n_groups = 0;

  // Read in the values and calculate the max number of groups
  while(std::cin) {
    int value, group;
    std::cin >> value >> group;
    values.push_back(value);
    groups.push_back(group);
    if (group >= n_groups) {
      n_groups = group+1;
    }
  }
  for (int i=0; i<NSUMS; ++i) {
    sums[i].resize(n_groups);
  }

  // Time grouped sums
  std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
  int* sumdata[NSUMS];
  for (int i = 0; i < NSUMS; ++i) {
    sumdata[i] = sums[i].data();
  }
  for (int i = 0; i < 1000; ++i) {
    grouped_sum(values.data(), groups.data(), values.size(), sumdata);
  }
  for (int i = 1; i < NSUMS; ++i) {
    for (int j = 0; j < n_groups; ++j) {
      sumdata[0][j] += sumdata[i][j];
    }
  }
  std::chrono::system_clock::time_point end = std::chrono::system_clock::now();

  std::cout << (end - start).count() << " with NSUMS=" << NSUMS << std::endl;

  return 0;
}

(oh, et j'ai également corrigé le calcul de n_groups; il était désactivé par un.)

Résultats

Après avoir configuré mon makefile pour donner un -DNSUMS=...argument au compilateur, je pourrais faire ceci:

sumspeed$ for n in 1 2 4 8 128; do make -s clean && make -s NSUMS=$n && (perf stat ./sum_groups < groups_shuffled && perf stat ./sum_groups < groups_sorted)  2>&1 | egrep '^[0-9]|frontend'; done
1134557008 with NSUMS=1
       924 611 882      stalled-cycles-frontend   #   17,13% frontend cycles idle   
2513696351 with NSUMS=1
     4 998 203 130      stalled-cycles-frontend   #   52,79% frontend cycles idle   
1116188582 with NSUMS=2
       899 339 154      stalled-cycles-frontend   #   16,83% frontend cycles idle   
1365673326 with NSUMS=2
     1 845 914 269      stalled-cycles-frontend   #   29,97% frontend cycles idle   
1127172852 with NSUMS=4
       902 964 410      stalled-cycles-frontend   #   16,79% frontend cycles idle   
1171849032 with NSUMS=4
     1 007 807 580      stalled-cycles-frontend   #   18,29% frontend cycles idle   
1118732934 with NSUMS=8
       881 371 176      stalled-cycles-frontend   #   16,46% frontend cycles idle   
1129842892 with NSUMS=8
       905 473 182      stalled-cycles-frontend   #   16,80% frontend cycles idle   
1497803734 with NSUMS=128
     1 982 652 954      stalled-cycles-frontend   #   30,63% frontend cycles idle   
1180742299 with NSUMS=128
     1 075 507 514      stalled-cycles-frontend   #   19,39% frontend cycles idle   

Le nombre optimal de vecteurs de somme dépendra probablement de la profondeur du pipeline de votre CPU. Mon processeur ultrabook âgé de 7 ans peut probablement maximiser le pipeline avec moins de vecteurs qu'un nouveau processeur de bureau de luxe aurait besoin.

De toute évidence, plus n'est pas nécessairement mieux; quand je suis devenu fou avec 128 vecteurs de somme, nous avons commencé à souffrir davantage de ratés de cache - comme en témoigne l'entrée mélangée devenant plus lente que triée, comme vous l'aviez prévu à l'origine. Nous avons bouclé la boucle! :)

Somme par groupe dans le registre

(cela a été ajouté dans une édition)

Agh, ballot snipé ! Si vous savez que votre entrée sera triée et que vous recherchez encore plus de performances, la réécriture suivante de la fonction (sans tableau de somme supplémentaire) est encore plus rapide, au moins sur mon ordinateur.

// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int* p_out) {
  int i = n-1;
  while (i >= 0) {
    int g = p_g[i];
    int gsum = 0;
    do {
      gsum += p_x[i--];
    } while (i >= 0 && p_g[i] == g);
    p_out[g] += gsum;
  }
}

L'astuce dans celui-ci est qu'il permet au compilateur de conserver la gsumvariable, la somme du groupe, dans un registre. Je suppose (mais peut-être très mal) que cela est plus rapide car la boucle de rétroaction dans le pipeline peut être plus courte ici et / ou moins d'accès à la mémoire. Un bon prédicteur de branche rend le contrôle supplémentaire de l'égalité de groupe bon marché.

Résultats

C'est terrible pour une entrée mélangée ...

sumspeed$ time ./sum_groups < groups_shuffled
2236354315

real    0m2.932s
user    0m2.923s
sys 0m0.009s

... mais est environ 40% plus rapide que ma solution "plusieurs sommes" pour une entrée triée.

sumspeed$ time ./sum_groups < groups_sorted
809694018

real    0m1.501s
user    0m1.496s
sys 0m0.005s

Beaucoup de petits groupes seront plus lents que quelques grands, donc que ce soit ou non la mise en œuvre la plus rapide dépendra vraiment de vos données ici. Et, comme toujours, sur votre modèle de processeur.

Vecteurs de sommes multiples, avec décalage au lieu de masquer les bits

Sopel a proposé quatre ajouts déroulés comme alternative à mon approche de masquage de bits. J'ai implémenté une version généralisée de leur suggestion, qui peut gérer différentes NSUMS. Je compte sur le compilateur qui déroule la boucle interne pour nous (ce qu'il a fait, au moins pour NSUMS=4).

#include <iostream>
#include <chrono>
#include <vector>

#ifndef NSUMS
#define NSUMS (4) // must be power of 2 (for masking to work)
#endif

#ifndef INNER
#define INNER (0)
#endif
#if INNER
// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int** p_out) {
  size_t i = 0;
  int quadend = n & ~(NSUMS-1);
  for (; i < quadend; i += NSUMS) {
    for (int k=0; k<NSUMS; ++k) {
      p_out[k][p_g[i+k]] += p_x[i+k];
    }
  }
  for (; i < n; ++i) {
    p_out[0][p_g[i]] += p_x[i];
  }
}
#else
// This is the function whose performance I am interested in
void grouped_sum(int* p_x, int *p_g, int n, int** p_out) {
  for (size_t i = 0; i < n; ++i) {
    p_out[i & (NSUMS-1)][p_g[i]] += p_x[i];
  }
}
#endif


int main() {
  std::vector<int> values;
  std::vector<int> groups;
  std::vector<int> sums[NSUMS];

  int n_groups = 0;

  // Read in the values and calculate the max number of groups
  while(std::cin) {
    int value, group;
    std::cin >> value >> group;
    values.push_back(value);
    groups.push_back(group);
    if (group >= n_groups) {
      n_groups = group+1;
    }
  }
  for (int i=0; i<NSUMS; ++i) {
    sums[i].resize(n_groups);
  }

  // Time grouped sums
  std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
  int* sumdata[NSUMS];
  for (int i = 0; i < NSUMS; ++i) {
    sumdata[i] = sums[i].data();
  }
  for (int i = 0; i < 1000; ++i) {
    grouped_sum(values.data(), groups.data(), values.size(), sumdata);
  }
  for (int i = 1; i < NSUMS; ++i) {
    for (int j = 0; j < n_groups; ++j) {
      sumdata[0][j] += sumdata[i][j];
    }
  }
  std::chrono::system_clock::time_point end = std::chrono::system_clock::now();

  std::cout << (end - start).count() << " with NSUMS=" << NSUMS << ", INNER=" << INNER << std::endl;

  return 0;
}

Résultats

Il est temps de mesurer. Notez que depuis que je travaillais dans / tmp hier, je n'ai pas exactement les mêmes données d'entrée. Par conséquent, ces résultats ne sont pas directement comparables aux précédents (mais probablement assez proches).

sumspeed$ for n in 2 4 8 16; do for inner in 0 1; do make -s clean && make -s NSUMS=$n INNER=$inner && (perf stat ./sum_groups < groups_shuffled && perf stat ./sum_groups < groups_sorted)  2>&1 | egrep '^[0-9]|frontend'; done; done1130558787 with NSUMS=2, INNER=0
       915 158 411      stalled-cycles-frontend   #   16,96% frontend cycles idle   
1351420957 with NSUMS=2, INNER=0
     1 589 408 901      stalled-cycles-frontend   #   26,21% frontend cycles idle   
840071512 with NSUMS=2, INNER=1
     1 053 982 259      stalled-cycles-frontend   #   23,26% frontend cycles idle   
1391591981 with NSUMS=2, INNER=1
     2 830 348 854      stalled-cycles-frontend   #   45,35% frontend cycles idle   
1110302654 with NSUMS=4, INNER=0
       890 869 892      stalled-cycles-frontend   #   16,68% frontend cycles idle   
1145175062 with NSUMS=4, INNER=0
       948 879 882      stalled-cycles-frontend   #   17,40% frontend cycles idle   
822954895 with NSUMS=4, INNER=1
     1 253 110 503      stalled-cycles-frontend   #   28,01% frontend cycles idle   
929548505 with NSUMS=4, INNER=1
     1 422 753 793      stalled-cycles-frontend   #   30,32% frontend cycles idle   
1128735412 with NSUMS=8, INNER=0
       921 158 397      stalled-cycles-frontend   #   17,13% frontend cycles idle   
1120606464 with NSUMS=8, INNER=0
       891 960 711      stalled-cycles-frontend   #   16,59% frontend cycles idle   
800789776 with NSUMS=8, INNER=1
     1 204 516 303      stalled-cycles-frontend   #   27,25% frontend cycles idle   
805223528 with NSUMS=8, INNER=1
     1 222 383 317      stalled-cycles-frontend   #   27,52% frontend cycles idle   
1121644613 with NSUMS=16, INNER=0
       886 781 824      stalled-cycles-frontend   #   16,54% frontend cycles idle   
1108977946 with NSUMS=16, INNER=0
       860 600 975      stalled-cycles-frontend   #   16,13% frontend cycles idle   
911365998 with NSUMS=16, INNER=1
     1 494 671 476      stalled-cycles-frontend   #   31,54% frontend cycles idle   
898729229 with NSUMS=16, INNER=1
     1 474 745 548      stalled-cycles-frontend   #   31,24% frontend cycles idle   

Oui, la boucle interne avec NSUMS=8est la plus rapide de mon ordinateur. Par rapport à mon approche "gsum local", elle a également l'avantage supplémentaire de ne pas devenir terrible pour l'entrée mélangée.

Intéressant à noter: NSUMS=16devient pire que NSUMS=8. Cela pourrait être dû au fait que nous commençons à voir plus de ratés de cache, ou parce que nous n'avons pas suffisamment de registres pour dérouler correctement la boucle interne.

Snild Dolkow
la source
5
C'était amusant. :)
Snild Dolkow
3
C'était génial! Je ne savais pas perf.
Tanveer Badar
1
Je me demande si dans votre première approche, dérouler manuellement 4x avec 4 accumulateurs différents donnerait de meilleures performances. Quelque chose comme godbolt.org/z/S-PhFm
Sopel
Merci pour la suggestion. Oui, cela a amélioré les performances, et je l'ai ajouté à la réponse.
Snild Dolkow
Merci! J'avais considéré que quelque chose comme ça pourrait être la possibilité mais je ne savais pas comment le déterminer, merci pour votre réponse détaillée!
Jim
3

Voici pourquoi les groupes triés sont plus lents que les groupes non enregistrés;

Voici d'abord le code assembleur pour la boucle de sommation:

008512C3  mov         ecx,dword ptr [eax+ebx]
008512C6  lea         eax,[eax+4]
008512C9  lea         edx,[esi+ecx*4] // &sums[groups[i]]
008512CC  mov         ecx,dword ptr [eax-4] // values[i]
008512CF  add         dword ptr [edx],ecx // sums[groups[i]]+=values[i]
008512D1  sub         edi,1
008512D4  jne         main+163h (08512C3h)

Regardons l'instruction d'ajout qui est la principale raison de ce problème;

008512CF  add         dword ptr [edx],ecx // sums[groups[i]]+=values[i]

Lorsque le processeur exécute d'abord cette instruction, il émet une demande de lecture (chargement) en mémoire à l'adresse dans edx, puis ajoute la valeur ecx, puis émet une demande d'écriture (stockage) pour la même adresse.

il y a une fonction dans la réorganisation de la mémoire de l'appelant du processeur

Pour permettre l'optimisation des performances de l'exécution des instructions, l'architecture IA-32 permet de s'écarter du modèle de commande forte appelé ordonnancement des processeurs dans les processeurs de la famille Pentium 4, Intel Xeon et P6. Ces variations d’ordonnancement du processeur (appelées ici le modèle d’ordonnancement de la mémoire) permettent des opérations d’amélioration des performances telles que l’autorisation des lectures avant les écritures tamponnées. Le but de l'une de ces variations est d'augmenter les vitesses d'exécution des instructions, tout en maintenant la cohérence de la mémoire, même dans les systèmes multiprocesseurs.

et il y a une règle

Les lectures peuvent être réorganisées avec des écritures plus anciennes vers des emplacements différents mais pas avec des écritures plus anciennes vers le même emplacement.

Donc, si la prochaine itération atteint l'instruction add avant la fin de la demande d'écriture, elle n'attendra pas si l'adresse edx est différente de la valeur précédente et émet la demande de lecture et elle est réorganisée sur l'ancienne demande d'écriture et l'instruction add continue. mais si l'adresse est la même, l'instruction add attendra que l'ancienne écriture soit terminée.

Notez que la boucle est courte et que le processeur peut l'exécuter plus rapidement que le contrôleur de mémoire ne termine la demande d'écriture dans la mémoire.

ainsi, pour les groupes triés, vous lirez et écrirez à partir de la même adresse plusieurs fois de suite afin de perdre l'amélioration des performances à l'aide de la réorganisation de la mémoire; en attendant, si des groupes aléatoires sont utilisés, chaque itération aura probablement une adresse différente, de sorte que la lecture n'attendra pas une écriture plus ancienne et sera réorganisée avant elle; ajouter une instruction n'attendra pas la précédente pour continuer.

Ahmed Anter
la source