Gruplandırılmış toplama, sıralı gruplarla sıralanmamış gruplardan neden daha yavaş?


27

Biri rasgele bir tamsayı, ikincisi bu program tarafından oluşturulabilir grubu tanımlayan bir tamsayı olan sekmeyle ayrılmış tamsayı 2 sütun var. ( 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;
}

Daha sonra sum_groups.ccgrup başına toplamları hesaplamak için ikinci bir program ( ) kullanırım.

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

Daha sonra bu programları verilen boyuttaki bir veri kümesinde çalıştırırsam ve sonra aynı veri kümesinin satırlarının sırasını karıştırırsam, karıştırılan veriler toplamları sıralanan verilerden ~ 2x veya daha hızlı hesaplar.

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

Gruba göre sıralanan orijinal verilerin daha iyi veri yerleşimine sahip olmasını ve daha hızlı olmasını beklerdim, ancak tam tersi davranışı gözlemliyorum. Birinin sebebi hipotez edip edemeyeceğini merak ediyordum.


1
Bilmiyorum, ancak toplamlar vektörünün aralık dışı öğelerine yazıyorsunuz - normal bir şey yaptıysanız ve veri öğelerine işaretçiler yerine vektörlere referanslar ilettiyseniz ve daha sonra kullanılan .at()veya operator[]sınır yapan bir hata ayıklama modu göreceksin.
Shawn

"Groups2" dosyasının içinde tüm verilerinizin bulunduğunu ve hepsinin okunduğunu ve işlendiğini doğruladınız mı? Ortada bir yerde EOF karakteri olabilir mi?
1201ProgramAlarm

2
Hiçbir zaman yeniden boyutlandırmadığınız için programın tanımlanmamış davranışı var sum. Onun yerine sums.reserve(n_groups);aramak zorundasın sums.resize(n_groups);- @Shawn'ın ima ettiği buydu.
Eugene

1
Not (örneğin buraya veya buraya bakınız)İki vektör (değerler ve grup) yerine bir çift vektörünün beklendiği gibi davrandığını ).
Bob__

1
Değerler üzerindeki verileri sıraladınız değil mi? Ama sonra bu da grupları sıralar ve bunun xpression üzerinde etkisi vardır p_out[p_g[i]] += p_x[i];. Belki de orijinal şifreli sırada, gruplar aslında p_outdiziye erişim konusunda iyi kümeleme sergilerler . Değerlerin sıralanması, grup indeksli erişim düzeninin zayıf olmasına neden olabilir p_out.
Kaz

Yanıtlar:


33

Kurulum / yavaşlatma

Her şeyden önce, program ne olursa olsun yaklaşık aynı zamanda çalışır:

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

Çoğu zaman giriş döngüsünde geçirilir. Ama ilgilendiğimiz için grouped_sum()bunu görmezden gelelim.

Karşılaştırma döngüsünün 10'dan 1000 yinelemeye değiştirilmesi, grouped_sum()çalışma süresine hakim olmaya başlar:

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

mükemmel fark

Şimdi kullanabiliriz perf programımızdaki en sıcak noktaları bulmak için .

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

Ve aralarındaki fark:

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

Daha fazla zaman main()Muhtemelen grouped_sum()inline olan . Harika, çok teşekkürler, perf.

mükemmel açıklama

İçinde zamanın geçtiği yerde bir fark var mı main() mı?

Shuffled:

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

sıralama:

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

Hayır, aynı iki talimat hakimdir. Bu nedenle, her iki durumda da uzun zaman alırlar, ancak veriler sıralandığında daha da kötüdür.

mükemmel stat

Tamam. Ancak onları aynı sayıda çalıştırmalıyız, bu nedenle her talimatın bir nedenle yavaşlaması gerekiyor. Bakalım ne perf statdiyor.

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

Sadece bir şey göze çarpıyor: stalled-cycles-frontend .

Tamam, talimat boru hattı duruyor. Ön uçta. Bunun ne anlama geldiği muhtemelen mikro mimari nesneler arasında değişir.

Yine de bir tahminim var. Eğer cömertseniz, buna hipotez bile diyebilirsiniz.

Hipotez

Girdileri sıralayarak, yazıların yerini artırabilirsiniz. Aslında, çok yerel olacaklar ; yaptığınız neredeyse tüm eklemeler bir öncekiyle aynı konuma yazılır.

Bu önbellek için harika, ancak boru hattı için harika değil. Veri bağımlılıkları sunuyorsunuz, bir sonraki ekleme talimatının önceki ekleme tamamlanıncaya (veya sonucu sonraki talimatlar için kullanılabilir hale getirene kadar) ilerlemesini engelliyorsunuz ) devam

Bu senin sorunun.

Bence.

Düzeltiyorum

Çoklu toplam vektörleri

Aslında bir şey deneyelim. Ya her toplama için aralarında geçiş yaparak birden fazla toplam vektör kullandı ve sonra bunları topladık. Bize biraz yer maliyeti var, ancak veri bağımlılıklarını kaldırması gerekiyor.

(kod güzel değil; beni yargılama, 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, ve ben de n_groups hesaplamasını düzelttim; birer birer kapalıydı.)

Sonuçlar

Derleme için bir -DNSUMS=...arg vermek için benim makefile yapılandırdıktan sonra , bunu yapabilirdi:

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   

Optimum toplam vektör sayısı muhtemelen CPU'nuzun boru hattı derinliğine bağlı olacaktır. 7 yaşındaki ultrabook CPU'm, muhtemelen yeni bir süslü masaüstü CPU'nun gerektirdiğinden daha az vektörle boru hattını maksimum düzeye çıkarabilir.

Açıkçası, daha fazlası mutlaka daha iyi değildir; 128 toplam vektör ile delirdiğimde, karıştırılmış girdinin sıradan daha yavaş hale geldiği gibi, beklediğiniz gibi önbellek hatalarından daha fazla acı çekmeye başladık. Tam bir daire çizdik! :)

Kayıttaki grup başına toplam

(bu bir düzenlemeye eklendi)

Agh, inek kesildi ! Girişinizin sıralanacağını ve daha da fazla performans arıyorsanız, en azından bilgisayarımda, işlevin aşağıdaki yeniden yazılması (ekstra toplam dizileri olmadan) daha da hızlıdır.

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

Buradaki hile, derleyicinin gsum grubun toplamını değişkeni bir kayıtta . Ben boru hattı geribildirim döngü burada daha kısa olabilir ve / veya daha az bellek erişim çünkü bu daha hızlı olduğunu tahmin (ama çok yanlış olabilir). İyi bir şube tahmincisi, grup eşitliği için ekstra kontrolü ucuz hale getirecektir.

Sonuçlar

Karışık girdi için korkunç ...

sumspeed$ time ./sum_groups < groups_shuffled
2236354315

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

... ancak sıralı girdi için "çok sayıda toplam" çözümümden% 40 daha hızlı

sumspeed$ time ./sum_groups < groups_sorted
809694018

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

Birçok küçük grup birkaç büyük gruptan daha yavaş olacaktır, bu yüzden bu uygulamanın daha hızlı olup olmadığı gerçekten verilerinize bağlıdır. Ve her zamanki gibi CPU modelinizde.

Bit maskeleme yerine ofsetli çoklu toplam vektörleri

Sopel, bit maskeleme yaklaşımımın alternatifi olarak dört adet açılmamış eklemeyi önerdi. Önerilerinin farklı bir şekilde ele alınabilecek genelleştirilmiş bir sürümünü uyguladım NSUMS. Bizim için iç döngüyü açıyor derleyiciye güveniyorum (ki en azından için 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;
}

Sonuçlar

Ölçme zamanı. Dün / tmp içinde çalıştığım için, aynı giriş verilerine sahip olmadığımı unutmayın. Bu nedenle, bu sonuçlar öncekilerle doğrudan karşılaştırılamaz (ancak muhtemelen yeterince yakındır).

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   

Evet, iç döngü NSUMS=8bilgisayarımdaki en hızlı. Benim "yerel gsum" yaklaşımım ile karşılaştırıldığında, karışık girdi için korkunç olmamasının da bir yararı var.

Dikkat çekmek ilginç: NSUMS=16daha kötü olur NSUMS=8. Bunun nedeni, daha fazla önbellek özlemi görmeye başlamamız veya iç döngüyü düzgün bir şekilde açmak için yeterli kaydımız olmaması olabilir.


5
Bu komikti. :)
Snild Dolkow

3
Harikaydı! Bilmiyordum perf.
Tanveer Badar

1
İlk yaklaşımınızda 4x'i 4 farklı akümülatörle manuel olarak açmanın daha iyi performans sağlayıp sağlamayacağını merak ediyorum. Godbolt.org/z/S-PhFm
Sopel

Önerin için teşekkürler. Evet, bu performansı artırdı ve cevaba ekledim.
Snild Dolkow

Teşekkürler! Bunun gibi bir şey olduğunu düşünmüştüm ama nasıl belirleyeceğimi bilmiyordum, detaylı cevabınız için teşekkürler!
Jim

3

Sıralanan grupları neden sıralanmamış gruplardan daha yavaş;

Birincisi, toplama döngüsünün montaj kodu:

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)

Bu sorunun ana nedeni olan ekleme talimatına bakalım;

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

İşlemci bu komutu ilk kez çalıştırdığında, edx'teki adrese bir bellek okuma (yükleme) isteği gönderir, ardından ecx değerini ekler ve sonra aynı adres için yazma (depola) isteği gönderir.

işlemci arayan belleği yeniden siparişinde bir özellik var

Talimat yürütmenin performans optimizasyonuna izin vermek için IA-32 mimarisi, Pentium 4, Intel Xeon ve P6 aile işlemcilerinde işlemci sıralaması adı verilen güçlü modelden ayrılmaya izin verir. Bu işlem sırası varyasyonları (burada bellek siparişi modeli olarak adlandırılır), okumaların arabelleğe alınmış yazmaların önüne geçmesine izin vermek gibi performans artırma işlemlerine izin verir. Bu varyasyonlardan herhangi birinin amacı, çok işlemcili sistemlerde bile bellek tutarlılığını korurken komut yürütme hızlarını artırmaktır.

ve bir kural var

Okumalar farklı konumlara daha eski yazmalarla yeniden sıralanabilir, ancak aynı konuma daha eski yazmalarla yeniden sıralanamaz.

Dolayısıyla, bir sonraki yineleme, yazma isteği tamamlanmadan ekleme talimatına ulaşırsa, edx adresi önceki değerden farklıysa ve okuma isteğini verirse ve eski yazma isteği üzerinde yeniden sıralanır ve ekleme talimatı devam eder. ancak adres aynıysa, ekleme talimatı eski yazma tamamlanana kadar bekleyecektir.

Döngünün kısa olduğunu ve işlemcinin bellek denetleyicisinin belleğe yazma isteğini tamamladığından daha hızlı çalıştırabileceğini unutmayın.

sıralı gruplar için art arda aynı adresten birçok kez okuyup yazacaksınız, böylece bellek yeniden siparişini kullanarak performans artışını kaybedecektir; bu arada rasgele gruplar kullanılırsa, her yineleme muhtemelen farklı bir adrese sahip olacaktır, bu nedenle okuma, daha eski yazma ve yeniden sıralamayı beklemeyecektir; talimat ekle önceki bir gitmek için beklemeyecek.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.