เหตุใดการรวมกลุ่มของกลุ่มจึงช้ากว่ากลุ่มที่เรียงลำดับมากกว่ากลุ่มที่ไม่เรียงลำดับ


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

ถ้าฉันเรียกใช้โปรแกรมเหล่านี้ในชุดข้อมูลที่มีขนาดที่กำหนดและจากนั้นสลับลำดับของแถวของชุดข้อมูลเดียวกันข้อมูลที่สับแล้วจะคำนวณจำนวนเงินที่ 2x หรือเร็วกว่าข้อมูลที่สั่ง

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[]ที่มีขอบเขต ตรวจสอบคุณจะเห็น
Shawn

คุณได้ตรวจสอบแล้วว่าไฟล์ "groups2" มีข้อมูลทั้งหมดอยู่ในนั้นและมันถูกอ่านและประมวลผลทั้งหมดหรือไม่ อาจมีตัวละคร EOF อยู่ตรงกลางบ้างไหม?
1201 โปรแกรม Alarm

2
sumโปรแกรมที่มีพฤติกรรมที่ไม่ได้กำหนดเพราะคุณไม่เคยปรับขนาด แทนที่จะsums.reserve(n_groups);ต้องโทรหาsums.resize(n_groups);- นั่นคือสิ่งที่ @Shawn กำลังบอกใบ้
ยูจีน

1
โปรดทราบ (ดูเช่นที่นี่หรือที่นี่ ) ว่าเวกเตอร์ของคู่แทนที่จะเป็นสองเวกเตอร์ (ค่าและกลุ่ม) ทำงานตามที่คาดไว้
Bob__

1
คุณจัดเรียงข้อมูลตามค่าใช่ไหม แต่แล้วที่ยังเรียงลำดับกลุ่มและที่มีผลกระทบต่อ p_out[p_g[i]] += p_x[i];XPRESSION อาจเป็นไปตามลำดับสัญญาณรบกวนเดิมกลุ่มต่าง ๆ กำลังแสดงการจัดกลุ่มที่ดีเกี่ยวกับการเข้าถึงp_outอาร์เรย์ p_outเรียงลำดับค่าที่อาจจะทำให้เกิดเป็นกลุ่มจัดทำดัชนีรูปแบบการเข้าถึงยากจน
Kaz

คำตอบ:


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 ถึง 1,000 การวนซ้ำ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() inlined เยี่ยมมากขอบคุณมาก ๆ

ใส่คำอธิบายประกอบ

มีความแตกต่างในเวลาที่ใช้ภายในหรือไม่ 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 สถิติ

ตกลง. แต่เราควรใช้จำนวนเท่ากันดังนั้นแต่ละคำสั่งจะต้องช้าลงด้วยเหตุผลบางอย่าง ลองดูสิ่งที่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

เพียงสิ่งเดียวที่ยืนออก: จนตรอกรอบ-ส่วนหน้า

โอเคท่อส่งคำสั่งหยุดทำงาน ในส่วนหน้า แน่นอนว่าหมายความว่าอาจจะแตกต่างกันระหว่าง microarchictectures

ฉันมีการคาดเดาแม้ว่า หากคุณใจกว้างคุณอาจเรียกมันว่าสมมติฐาน

สมมติฐาน

เมื่อเรียงลำดับอินพุตคุณจะเพิ่มตำแหน่งของการเขียน ในความเป็นจริงพวกเขาจะเป็นคนในท้องถิ่น การเพิ่มส่วนใหญ่ทั้งหมดที่คุณทำจะเขียนไปยังตำแหน่งเดียวกันกับที่เพิ่มไว้ก่อนหน้า

นั่นยอดเยี่ยมสำหรับแคช แต่ไม่ดีสำหรับไพพ์ไลน์ คุณกำลังแนะนำการพึ่งพาข้อมูลป้องกันไม่ให้คำสั่งการเติมถัดไปดำเนินการต่อจนกว่าการเติมก่อนหน้านี้จะเสร็จสมบูรณ์ (หรือทำให้ผลลัพธ์พร้อมใช้งานสำหรับคำแนะนำที่สำเร็จ )

นั่นคือปัญหาของคุณ

ฉันคิด.

แก้ไขมัน

เวกเตอร์รวมหลายตัว

ที่จริงลองทำอะไรซักอย่าง จะเกิดอะไรขึ้นถ้าเราใช้เวกเตอร์ผลรวมหลายตัว, สลับระหว่างพวกมันสำหรับการบวกแต่ละครั้ง, แล้วก็บวกพวกมันท้ายที่สุด? มีค่าใช้จ่ายเล็กน้อยสำหรับเรา แต่ควรลบการอ้างอิงข้อมูล

(รหัสไม่สวยอย่าตัดสินฉันอินเทอร์เน็ต !!)

#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   

จำนวนเวกเตอร์ผลรวมที่ดีที่สุดอาจขึ้นอยู่กับความลึกของขั้นตอนการทำงานของ CPU ของคุณ ซีพียู ultrabook อายุ 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

จำนวนของกลุ่มเล็ก ๆ จะช้ากว่าคนใหญ่ไม่กี่ดังนั้นหรือไม่ว่านี่คือการดำเนินการได้เร็วขึ้นจะจริงๆขึ้นอยู่กับข้อมูลของคุณที่นี่ และเช่นเคยกับรุ่น CPU ของคุณ

ผลรวมเวกเตอร์จำนวนมากที่มีการชดเชยแทนการปิดบังบิต

Sopelแนะนำการเพิ่มเติมสี่รายการที่ยังไม่ได้ควบคุมเป็นทางเลือกให้กับวิธีการปิดบังบิตของฉัน 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
มันสนุกมาก :)
Snild Dolkow

3
มันยอดเยี่ยมมาก! ไม่รู้เรื่องperfเลย
Tanveer Badar

1
ฉันสงสัยว่าในวิธีแรกของคุณด้วยตนเองในการปลด 4x ด้วยตัวสะสม 4 ตัวที่ต่างกันจะให้ประสิทธิภาพที่ดีขึ้นหรือไม่ บางอย่างเช่นgodbolt.org/z/S-PhFm
Sopel

ขอบคุณสำหรับคำแนะนำ ใช่ประสิทธิภาพนั้นเพิ่มขึ้นและฉันได้เพิ่มไว้ในคำตอบ
Snild Dolkow

ขอบคุณ! ฉันได้พิจารณาบางสิ่งเช่นนี้แล้วอาจเป็นไปได้ แต่ไม่รู้ว่าจะระบุได้อย่างไรขอบคุณสำหรับคำตอบโดยละเอียดของคุณ!
Jim

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]

เมื่อตัวประมวลผลประมวลผลคำสั่งนี้ก่อนมันจะออกคำขอหน่วยความจำ read (load) ไปยังที่อยู่ใน edx จากนั้นเพิ่มค่าของ ecx จากนั้นจึงออกคำร้องขอให้เขียน (store) สำหรับที่อยู่เดียวกัน

มีคุณสมบัติในการเรียงลำดับหน่วยความจำตัวเรียกของหน่วยประมวลผล

เพื่อให้การเพิ่มประสิทธิภาพประสิทธิภาพของการดำเนินการเรียนการสอนสถาปัตยกรรม IA-32 ช่วยให้ออกจากรูปแบบการจัดเรียงที่เรียกว่าโปรเซสเซอร์สั่งใน Pentium 4, Intel Xeon และ P6 ตระกูลโปรเซสเซอร์ ชุดรูปแบบตัวประมวลผลเหล่านี้ (เรียกว่าที่นี่เป็นแบบจำลองการสั่งซื้อหน่วยความจำ) ช่วยให้การดำเนินการเพิ่มประสิทธิภาพเช่นช่วยให้การอ่านสามารถดำเนินการก่อนการเขียนบัฟเฟอร์ เป้าหมายของรูปแบบใด ๆ เหล่านี้คือการเพิ่มความเร็วในการประมวลผลคำสั่งในขณะที่ยังคงความต่อเนื่องของหน่วยความจำไว้แม้ในระบบที่มีโปรเซสเซอร์หลายตัว

และมีกฎ

การอ่านอาจถูกจัดลำดับใหม่ด้วยการเขียนที่เก่ากว่าไปยังสถานที่ต่าง ๆ แต่ไม่ใช่การเขียนที่เก่ากว่าไปยังที่ตั้งเดียวกัน

ดังนั้นหากการวนซ้ำครั้งถัดไปมาถึงคำสั่งเพิ่มก่อนที่คำขอการเขียนจะเสร็จสมบูรณ์จะไม่รอถ้าที่อยู่ edx แตกต่างจากค่าก่อนหน้าและออกคำขอการอ่านและจะเรียงลำดับใหม่ตามคำขอการเขียนที่เก่ากว่า แต่ถ้าที่อยู่เดียวกันคำแนะนำการเพิ่มจะรอจนกว่าการเขียนเก่าจะเสร็จสิ้น

โปรดทราบว่าการวนซ้ำสั้นและโปรเซสเซอร์สามารถดำเนินการได้เร็วกว่าตัวควบคุมหน่วยความจำเสร็จสิ้นการร้องขอการเขียนไปยังหน่วยความจำ

ดังนั้นสำหรับกลุ่มที่เรียงลำดับคุณจะอ่านและเขียนจากที่อยู่เดียวกันหลายครั้งติดต่อกันดังนั้นมันจะสูญเสียการปรับปรุงประสิทธิภาพโดยใช้หน่วยความจำใหม่ ในขณะเดียวกันถ้ากลุ่มสุ่มใช้การทำซ้ำแต่ละครั้งอาจมีที่อยู่ที่แตกต่างกันดังนั้นการอ่านจะไม่รอการเขียนที่เก่ากว่าและจัดลำดับใหม่ก่อนหน้า เพิ่มคำสั่งจะไม่รอก่อนหน้านี้ที่จะไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.