Чому групується підсумовування повільніше з відсортованими групами, ніж несортовані групи?


27

У мене є два стовпчики з цілими цілими числами, перша з яких є випадковим цілим числом, друга - цілим числом, що ідентифікує групу, яка може бути згенерована цією програмою. ( 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
Зауважте (див., Наприклад, тут чи тут ), що вектор пар замість двох векторів (значення та група) поводиться так, як очікувалося.
Боб__

1
Ви сортували дані за значеннями, правда? Але тоді це також сортує групи, і це впливає на xpression 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()підкреслило. Чудово, спасибі велике, парф.

perf annotate

Чи є різниця в тому, де проводиться час всередині 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

Добре. Але нам слід виконувати їх однаково, тому кожна інструкція з якоїсь причини повинна ставати повільнішою. Подивимось, що 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

Виділяється лише одне: staled-cycles-frontend .

Гаразд, інструкційний конвеєр затримується. У передній частині. Точно те, що це означає, можливо, варіюється між мікроархітектурами.

Я маю здогад, хоча. Якщо ви щедрий, ви можете навіть назвати це гіпотезою.

Гіпотеза

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

Це чудово підходить для кешу, але не чудово для конвеєра. Ви вводите залежності даних, запобігаючи виконанню наступної інструкції щодо доповнення, поки попереднє додавання не завершиться (або іншим чином не зробить результат доступним для наступних інструкцій )

Це ваша проблема.

Я думаю.

Виправлення

Вектори з множинною сумою

Власне, спробуємо щось. Що робити, якщо ми використовували кілька векторів суми, перемикаючи їх між собою для кожного додавання, а потім підсумовуючи їх у кінці? Це коштує нам небагато місцевості, але повинно усунути залежність від даних.

(код не гарний; не судіть мене, Інтернет !!)

#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; це було вимкнено одним.)

Результати

Налаштувавши makefile для надання -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- це найшвидший на моєму комп’ютері. У порівнянні з моїм підходом "локальний gsum", він також має додаткову перевагу, що він не стає жахливим для перетасованих даних.

Цікаво зазначити: NSUMS=16стає гірше, ніж NSUMS=8. Це може бути тому, що ми починаємо бачити більше пропусків кешу, або тому, що у нас недостатньо регістрів, щоб правильно розгортати внутрішню петлю.


5
Це було весело. :)
Снілд Долков

3
Це було неперевершено! Не знав про perf.
Tanveer Badar

1
Цікаво, якби у вашому першому підході розгортання 4x вручну з 4 різними акумуляторами дало б кращі показники. Щось на кшталт godbolt.org/z/S-PhFm
Сопель

Дякую за пропозицію. Так, це підвищило продуктивність, і я додав це до відповіді.
Снілд Долків

Дякую! Я вважав, що щось подібне може бути можливістю, але не знаю, як це визначити, дякую за вашу детальну відповідь!
Джим

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 відрізняється від попереднього значення та видасть запит на читання, і він упорядкував старіший запит запису, а інструкція додавання продовжується. але якщо адреса однакова, інструкція додати буде чекати, поки не буде виконано старе записування.

Зауважте, що цикл короткий, і процесор може виконати його швидше, ніж контролер пам'яті завершує запит запису в пам'ять.

тому для відсортованих груп ви будете читати та писати з однієї адреси багато разів поспіль, так що це втратить підвищення продуктивності за допомогою переупорядкування пам’яті; тим часом, якщо випадкові групи, що використовуються, то кожна ітерація матиме, ймовірно, різну адресу, тому прочитане не чекатиме старішого запису та переупорядкування перед ним; додати інструкцію не чекатиме попереднього переходу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.