Por que a soma agrupada é mais lenta com grupos classificados do que grupos não classificados?

27

Eu tenho 2 colunas de números inteiros delimitados por tabulação, a primeira das quais é um número inteiro aleatório, a segunda é um número inteiro que identifica o grupo, que pode ser gerado por este programa. ( 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;
}

Em seguida, uso um segundo programa ( sum_groups.cc) para calcular as somas por grupo.

#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;
}

Se eu executar esses programas em um conjunto de dados de determinado tamanho e, em seguida, embaralhar a ordem das linhas do mesmo conjunto de dados, os dados embaralhados calcularão as somas ~ 2x ou mais rápido que os dados ordenados.

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

Eu esperava que os dados originais, classificados por grupo, tivessem uma melhor localização de dados e fossem mais rápidos, mas observo o comportamento oposto. Fiquei me perguntando se alguém pode hipótese a razão?

Jim
fonte
11
Eu não sei, mas você está escrevendo para elementos fora do intervalo do vetor somas - se você fez a coisa normal e passou referências a vetores em vez de ponteiros para os elementos de dados e depois usou .at()ou um modo de depuração operator[]que limita verificando você veria.
Shawn
Você verificou que o arquivo "groups2" possui todos os seus dados e que todos estão sendo lidos e processados? Existe talvez um personagem EOF no meio em algum lugar?
1201ProgramAlarm
2
O programa tem um comportamento indefinido porque você nunca redimensiona sum. Em vez de sums.reserve(n_groups);você deve ligar sums.resize(n_groups);- era o que @Shawn estava sugerindo.
Eugene
11
Observe (veja, por exemplo, aqui ou aqui ) que um vetor de pares, em vez de dois vetores (valores e grupo), se comporta conforme o esperado.
Bob__
11
Você classificou os dados nos valores, certo? Mas então isso também classifica os grupos, e isso tem um impacto na xpressão p_out[p_g[i]] += p_x[i];. Talvez na ordem codificada original, os grupos estejam exibindo um bom agrupamento em relação ao acesso à p_outmatriz. A classificação dos valores talvez cause um padrão de acesso indexado a grupo ruim p_out.
Kaz

Respostas:

33

Configurar / tornar lento

Primeiro de tudo, o programa é executado aproximadamente na mesma hora, independentemente:

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

A maior parte do tempo é gasta no loop de entrada. Mas, como estamos interessados ​​no grouped_sum()assunto, vamos ignorar isso.

A alteração do loop de referência de 10 para 1000 iterações grouped_sum()começa a dominar o tempo de execução:

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

diff diff

Agora podemos usar perfpara encontrar os pontos mais quentes do nosso programa.

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) ]

E a diferença entre eles:

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
[...]

Mais tempo main(), o que provavelmente foi grouped_sum()inline. Ótimo, muito obrigado, perf.

anotação perf

Existe alguma diferença em onde o tempo é gasto lá dentro main() ?

Aleatório:

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)
[...]

Ordenado:

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)
[...]

Não, são as mesmas duas instruções dominantes. Portanto, eles demoram muito tempo em ambos os casos, mas são ainda piores quando os dados são classificados.

estatísticas de desempenho

OK. Mas devemos executá-los o mesmo número de vezes, para que cada instrução esteja ficando mais lenta por algum motivo. Vamos ver o que perf statdiz.

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

Apenas uma coisa se destaca: front-cycles-stalled .

Ok, o pipeline de instruções está parado. No frontend. Exatamente o que isso significa provavelmente varia entre microarquiteturas.

Eu tenho um palpite, no entanto. Se você é generoso, pode até chamá-lo de hipótese.

Hipótese

Ao classificar a entrada, você aumenta a localidade das gravações. De fato, eles serão muito locais; quase todas as adições feitas serão gravadas no mesmo local da anterior.

Isso é ótimo para o cache, mas não para o pipeline. Você está introduzindo dependências de dados, impedindo que a próxima instrução de adição prossiga até que a adição anterior seja concluída (ou disponibilizou o resultado para as instruções seguintes )

Esse é o seu problema.

Eu acho que.

Consertando-o

Vetores de soma múltipla

Na verdade, vamos tentar algo. E se usássemos vários vetores de soma, alternando entre eles para cada adição e depois os somamos no final? Custa-nos um pouco de localidade, mas deve remover as dependências de dados.

(o código não é bonito; não me julgue, 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;
}

(ah, e também corrigi o cálculo de n_groups; foi desativado por um).

Resultados

Depois de configurar meu makefile para fornecer um -DNSUMS=...argumento ao compilador, eu poderia fazer isso:

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   

O número ideal de vetores de soma provavelmente dependerá da profundidade do pipeline da sua CPU. Minha CPU ultrabook de 7 anos de idade provavelmente pode maximizar o pipeline com menos vetores do que uma nova CPU de desktop precisa.

Claramente, mais não é necessariamente melhor; quando fiquei louco com 128 vetores de soma, começamos a sofrer mais com falhas de cache - como evidenciado pela entrada aleatória se tornando mais lenta do que classificada, como você esperava inicialmente. Nós fizemos um círculo completo! :)

Soma por grupo no registro

(isso foi adicionado em uma edição)

Agh, nerd cortou ! Se você sabe que sua entrada será classificada e procura desempenho ainda maior, a seguinte reescrita da função (sem matrizes de soma extra) é ainda mais rápida, pelo menos no meu computador.

// 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;
  }
}

O truque deste é que ele permite que o compilador mantenha a gsumvariável, a soma do grupo, em um registro. Estou supondo (mas pode estar muito errado) que isso seja mais rápido porque o loop de feedback no pipeline pode ser mais curto aqui e / ou menos acessos à memória. Um bom preditor de filial tornará barata a verificação extra da igualdade de grupo.

Resultados

É terrível para entrada aleatória ...

sumspeed$ time ./sum_groups < groups_shuffled
2236354315

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

... mas é cerca de 40% mais rápido que a minha solução "muitas somas" para entrada classificada.

sumspeed$ time ./sum_groups < groups_sorted
809694018

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

Muitos grupos pequenos serão mais lentos que alguns grandes; portanto, se essa é a implementação mais rápida, dependerá realmente dos seus dados aqui. E, como sempre, no seu modelo de CPU.

Vários vetores de somas, com deslocamento em vez de mascaramento de bits

Sopel sugeriu quatro adições desenroladas como alternativa à minha abordagem de mascaramento de bits. Eu implementei uma versão generalizada de sua sugestão, que pode lidar com diferentes NSUMS. Estou contando com o compilador desenrolando o loop interno para nós (o que aconteceu, pelo menos para 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;
}

Resultados

Hora de medir. Observe que desde que eu estava trabalhando no / tmp ontem, não tenho exatamente os mesmos dados de entrada. Portanto, esses resultados não são diretamente comparáveis ​​aos anteriores (mas provavelmente próximos o suficiente).

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   

Sim, o loop interno NSUMS=8é o mais rápido do meu computador. Comparado à minha abordagem "gsum local", ela também tem o benefício adicional de não se tornar terrível para as informações embaralhadas.

Interessante notar: NSUMS=16torna - se pior que NSUMS=8. Isso pode ser porque estamos começando a ver mais falhas de cache ou porque não temos registros suficientes para desenrolar o loop interno corretamente.

Snild Dolkow
fonte
5
Isso foi divertido. :)
Snild Dolkow
3
Foi demais! Não sabia perf.
precisa
11
Gostaria de saber se, em sua primeira abordagem, desenrolar manualmente 4x com 4 acumuladores diferentes produziria melhor desempenho. Algo como godbolt.org/z/S-PhFm
Sopel
Obrigado pela sugestão. Sim, isso aumentou o desempenho e eu o adicionei à resposta.
Snild Dolkow
Obrigado! Eu tinha considerado que algo assim poderia ser a possibilidade, mas não sabia como determiná-la, obrigado pela resposta detalhada!
21419 Jim
3

Aqui está o porquê de grupos classificados serem mais lentos que grupos não-gravados;

Primeiro, aqui está o código de montagem para o loop de soma:

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)

Vamos analisar a instrução add, que é o principal motivo desse problema;

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

Quando o processador executar esta instrução primeiro, ele emitirá uma solicitação de leitura de memória (carga) para o endereço no edx, em seguida, adicionará o valor de ecx e emitirá uma solicitação de gravação (armazenamento) para o mesmo endereço.

há um recurso no reordenamento de memória do chamador do processador

Para permitir a otimização do desempenho da execução das instruções, a arquitetura IA-32 permite desvios do modelo de ordens fortes chamado pedido de processador nos processadores das famílias Pentium 4, Intel Xeon e P6. Essas variações de ordem do processador (chamadas aqui de modelo de pedido de memória) permitem operações de aprimoramento de desempenho, como permitem que as leituras avancem nas gravações em buffer. O objetivo de qualquer uma dessas variações é aumentar a velocidade de execução das instruções, mantendo a coerência da memória, mesmo em sistemas com múltiplos processadores.

e existe uma regra

As leituras podem ser reordenadas com gravações mais antigas em locais diferentes, mas não com gravações mais antigas no mesmo local.

Portanto, se a próxima iteração atingir a instrução add antes que a solicitação de gravação seja concluída, ela não esperará se o endereço edx for diferente do valor anterior e emitirá a solicitação de leitura, reordenando a solicitação de gravação mais antiga e a instrução add continuará. mas se o endereço for o mesmo, a instrução add aguardará até que a gravação antiga seja concluída.

Observe que o loop é curto e o processador pode executá-lo mais rapidamente do que o controlador de memória conclui a solicitação de gravação na memória.

portanto, para grupos classificados, você lerá e gravará do mesmo endereço várias vezes consecutivas, a fim de perder o aprimoramento de desempenho usando o reordenamento de memória; enquanto isso, se forem utilizados grupos aleatórios, cada iteração provavelmente terá um endereço diferente, de modo que a leitura não espere a gravação mais antiga e reordenada antes dela; A instrução add não aguardará a anterior.

Ahmed Anter
fonte