Почему групповое суммирование медленнее с отсортированными группами, чем с несортированными группами?

27

У меня есть 2 столбца целых чисел с разделителями табуляции, первый из которых является случайным целым числом, второй - целым числом, идентифицирующим группу, которая может быть сгенерирована этой программой. ( 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;
}

Затем я использую вторую программу ( sum_groups.cc) для расчета сумм по группе.

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

Если затем я запускаю эти программы для набора данных заданного размера, а затем перетасовываю порядок строк в одном и том же наборе данных, то перемешанные данные вычисляют суммы в ~ 2 раза или более быстрее, чем упорядоченные данные.

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

Я ожидал, что исходные данные, отсортированные по группам, будут иметь лучшую локальность данных и будут быстрее, но я наблюдаю противоположное поведение. Мне было интересно, если кто-нибудь может предположить причину?

Джим
источник
1
Я не знаю, но вы пишете вне диапазона элементов вектора суммы - если вы сделали обычную вещь и передали ссылки на векторы вместо указателей на элементы данных, а затем использовали .at()или режим отладки, operator[]который делает границы проверяешь увидишь.
Шон,
Вы проверили, что в файле groups2 содержатся все ваши данные и что все они считываются и обрабатываются? Возможно, где-то посередине находится персонаж EOF?
1201ProgramAlarm
2
Программа имеет неопределенное поведение, потому что вы никогда не изменяете размер sum. Вместо того sums.reserve(n_groups);, чтобы звонить sums.resize(n_groups);- это то, на что намекал @Shawn.
Евгений
1
Обратите внимание (см., Например, здесь или здесь ), что вектор пар вместо двух векторов (значений и группы) ведет себя как ожидалось.
Bob__
1
Вы отсортировали данные по значениям, верно? Но тогда это также сортирует группы, и это оказывает влияние на выражение p_out[p_g[i]] += p_x[i];. Может быть, в первоначальном зашифрованном порядке группы фактически демонстрируют хорошую кластеризацию в отношении доступа к p_outмассиву. Сортировка значений может привести к неправильной схеме доступа с индексированием по группам p_out.
Каз

Ответы:

33

Настройка / сделать это медленно

Прежде всего, программа работает примерно в одно и то же время независимо от:

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

Большую часть времени проводят в цикле ввода. Но так как мы заинтересованы в этом grouped_sum(), давайте проигнорируем это.

Изменение цикла тестирования с 10 до 1000 итераций grouped_sum()начинает доминировать во время выполнения:

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самые горячие точки в нашей программе.

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

И разница между ними:

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

Больше времени в main(), что, вероятно, grouped_sum()указал. Отлично, большое спасибо, перф.

перф аннотировать

Есть ли разница в том, где время проводится внутри main() ?

перетасовал:

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

Сортировка:

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

Нет, доминируют одни и те же две инструкции. Таким образом, они занимают много времени в обоих случаях, но еще хуже, когда данные сортируются.

перф стат

Ладно. Но мы должны запускать их одинаковое количество раз, поэтому каждая инструкция по какой-то причине должна работать медленнее. Посмотрим что perf statговорит.

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

Выделяется только одно: цикл-фронт-интерфейс .

Хорошо, конвейер инструкций останавливается. На фронтенде. То, что это означает, вероятно, зависит от микроархитектуры.

У меня есть предположение, хотя. Если вы щедры, вы можете даже назвать это гипотезой.

гипотеза

Сортируя входные данные, вы увеличиваете локальность записей. На самом деле, они будут очень локальными; почти все добавленные вами записи будут записаны в ту же папку, что и предыдущая.

Это здорово для кеша, но не отлично для конвейера. Вы вводите зависимости данных, предотвращая выполнение следующей инструкции добавления до тех пор, пока предыдущее добавление не будет завершено (или иным образом не сделает результат доступным для последующих инструкций ).

Это твоя проблема.

Я думаю.

Исправляя это

Векторы с несколькими суммами

На самом деле, давайте попробуем что-нибудь. Что если мы использовали несколько векторов суммы, переключаясь между ними для каждого сложения, а затем суммировали их в конце? Это стоит нам немного локальности, но должно удалить зависимости данных.

(код не симпатичный; не суди меня, интернет !!)

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

(о, и я также исправил расчет n_groups; он был выключен одним.)

Результаты

После настройки моего make-файла для предоставления -DNSUMS=...аргумента компилятору я мог сделать это:

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   

Оптимальное количество векторов суммы, вероятно, будет зависеть от глубины конвейера вашего процессора. Мой 7-летний процессор для ультрабуков, вероятно, может максимально использовать конвейер с меньшим количеством векторов, чем потребуется новому модному настольному процессору.

Очевидно, что больше не обязательно лучше; когда я сошел с ума из-за 128 векторов сумм, мы стали больше страдать из-за промахов в кеше - о чем свидетельствует перемешанный ввод, становящийся медленнее, чем отсортированный, как вы изначально ожидали. Мы прошли полный круг! :)

Сумма по группам в реестре

(это было добавлено в редактировании)

Ах, ботаник ! Если вы знаете, что ваши входные данные будут отсортированы и вам нужна еще более высокая производительность, следующая перезапись функции (без дополнительных массивов сумм) будет еще быстрее, по крайней мере, на моем компьютере.

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

Хитрость в этом заключается в том, что он позволяет компилятору хранить gsumпеременную, сумму группы, в регистре. Я предполагаю (но, возможно, очень ошибочно), что это быстрее, потому что цикл обратной связи в конвейере может быть короче и / или меньше обращений к памяти. Хороший предсказатель ветвления сделает дополнительную проверку на равенство групп дешевой.

Результаты

Это ужасно для случайного ввода ...

sumspeed$ time ./sum_groups < groups_shuffled
2236354315

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

... но примерно на 40% быстрее, чем мое решение "много сумм" для сортированного ввода.

sumspeed$ time ./sum_groups < groups_sorted
809694018

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

Много небольших групп будет происходить медленнее , чем несколько больших, так ли это или нет , тем быстрее реализация будет действительно зависеть от ваших данных здесь. И, как всегда, на вашей модели процессора.

Векторы с несколькими суммами, со смещением вместо битовой маскировки

Сопель предложил четыре развернутых дополнения в качестве альтернативы моему подходу маскировки битов. Я реализовал обобщенную версию их предложения, которая может обрабатывать разные NSUMS. Я рассчитываю на то, что компилятор развернет для нас внутренний цикл (что он сделал, по крайней мере, для 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;
}

Результаты

Время измерять. Обратите внимание, что, поскольку я вчера работал в / tmp, у меня нет точно таких же входных данных. Следовательно, эти результаты не сопоставимы напрямую с предыдущими (но, вероятно, достаточно близки).

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   

Да, внутренний цикл с NSUMS=8является самым быстрым на моем компьютере. По сравнению с моим подходом "local gsum", он также имеет дополнительное преимущество, заключающееся в том, что он не становится ужасным для перемешанного ввода.

Интересно отметить: NSUMS=16хуже становится NSUMS=8. Это может быть потому, что мы начинаем видеть больше пропусков кэша, или потому что у нас недостаточно регистров для правильной развертки внутреннего цикла.

Snild Dolkow
источник
5
Это было весело :)
Snild Dolkow
3
Это было потрясающе! Не знал о perf.
Танвеер Бадар
1
Интересно, даст ли лучшая производительность в вашем первом подходе ручное развертывание 4х с 4 различными аккумуляторами? Что - то вроде godbolt.org/z/S-PhFm
Sopel
Спасибо за предложение. Да, это увеличило производительность, и я добавил это к ответу.
Snild Dolkow
Спасибо! Я подумал, что это возможно, но не знал, как это определить, спасибо за подробный ответ!
Джим
3

Вот почему сортированные группы медленнее, чем несортированные группы;

Сначала вот код сборки для цикла суммирования:

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)

Давайте посмотрим на инструкцию добавления, которая является основной причиной этой проблемы;

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

Когда процессор сначала выполняет эту инструкцию, он отправляет запрос чтения (загрузки) из памяти на адрес в edx, затем добавляет значение ecx, а затем выдает запрос записи (сохранения) для того же адреса.

есть функция переупорядочения памяти вызывающего процессора

Для оптимизации производительности выполнения команд архитектура IA-32 допускает отклонения от модели сильного порядка, называемой упорядочением процессоров в процессорах семейства Pentium 4, Intel Xeon и P6. Эти варианты упорядочения процессоров (называемые здесь моделью упорядочения памяти) позволяют повысить производительность операций, таких как чтение позволяет опережать буферизованные записи. Целью любого из этих вариантов является повышение скорости выполнения команд при сохранении согласованности памяти даже в многопроцессорных системах.

и есть правило

Чтения могут быть переупорядочены со старыми записями в разных местах, но не со старыми записями в том же месте.

Поэтому, если следующая итерация достигнет инструкции добавления до того, как запрос на запись завершится, она не будет ждать, если адрес edx отличается от предыдущего значения, и выдаст запрос на чтение, и он переупорядочится по старому запросу на запись, и инструкция добавления продолжится. но если адрес один и тот же, инструкция добавления будет ждать до завершения старой записи.

Обратите внимание, что цикл короткий, и процессор может выполнить его быстрее, чем контроллер памяти завершает запрос на запись в память.

таким образом, для отсортированных групп вы будете читать и писать с одного и того же адреса много раз подряд, так что это приведет к потере повышения производительности при переупорядочении памяти; Между тем, если используются случайные группы, то каждая итерация, вероятно, будет иметь другой адрес, поэтому чтение не будет ожидать более старой записи и переупорядочено перед ней; инструкция add не будет ждать предыдущую.

Ахмед Антер
источник