Tại sao tổng kết nhóm chậm hơn với các nhóm được sắp xếp hơn các nhóm chưa sắp xếp?


27

Tôi có 2 cột số nguyên được phân tách bằng tab, đầu tiên là số nguyên ngẫu nhiên, số thứ hai là số nguyên xác định nhóm, có thể được tạo bởi chương trình này. ( 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;
}

Sau đó tôi sử dụng chương trình thứ hai ( sum_groups.cc) để tính tổng cho mỗi nhó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;
}

Sau đó, nếu tôi chạy các chương trình này trên một tập dữ liệu có kích thước nhất định, và sau đó xáo trộn thứ tự của các hàng của cùng một tập dữ liệu, dữ liệu được xáo trộn sẽ tính tổng ~ 2 hoặc nhanh hơn dữ liệu được đặt hàng.

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

Tôi đã mong đợi dữ liệu gốc được sắp xếp theo nhóm để có địa phương dữ liệu tốt hơn và nhanh hơn, nhưng tôi quan sát hành vi ngược lại. Tôi đã tự hỏi nếu có ai có thể đưa ra giả thuyết lý do?


1
Tôi không biết, nhưng bạn đang viết ra khỏi các phần tử phạm vi của vectơ tổng - nếu bạn đã làm điều bình thường và chuyển tham chiếu đến vectơ thay vì con trỏ đến các phần tử dữ liệu, sau đó sử dụng .at()hoặc chế độ gỡ lỗi operator[]giới hạn kiểm tra xem bạn có thấy không.
Shawn

Bạn đã xác minh rằng tệp "Groups2" có tất cả dữ liệu của bạn trong đó và tất cả đều được đọc và xử lý chưa? Có lẽ có một nhân vật EOF ở giữa đâu đó?
1201 Chương trình Chương trình

2
Chương trình có hành vi không xác định vì bạn không bao giờ thay đổi kích thước sum. Thay vì sums.reserve(n_groups);bạn phải gọi sums.resize(n_groups);- đó là những gì @Shawn đã gợi ý.
Eugene

1
Lưu ý (xem ví dụ ở đây hoặc ở đây ) rằng một vectơ của các cặp, thay vì hai vectơ (giá trị và nhóm), hoạt động như mong đợi.
Bob__

1
Bạn đã sắp xếp dữ liệu trên các giá trị, phải không? Nhưng sau đó cũng sắp xếp các nhóm, và điều đó có tác động đến xpression p_out[p_g[i]] += p_x[i];. Có thể theo thứ tự xáo trộn ban đầu, các nhóm thực sự đang thể hiện phân cụm tốt liên quan đến truy cập vào p_outmảng. Sắp xếp các giá trị có thể khiến mẫu truy cập được lập chỉ mục nhóm kém p_out.
Kaz

Câu trả lời:


33

Cài đặt / làm cho nó chậm

Trước hết, chương trình chạy trong cùng một lúc bất kể:

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

Hầu hết thời gian được dành trong vòng lặp đầu vào. Nhưng vì chúng tôi quan tâm đến grouped_sum(), hãy bỏ qua điều đó.

Thay đổi vòng lặp điểm chuẩn từ 10 đến 1000 lần lặp, grouped_sum()bắt đầu thống trị thời gian chạy:

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

khác biệt hoàn hảo

Bây giờ chúng tôi có thể sử dụng perfđể tìm những điểm nóng nhất trong chương trình của chúng tôi.

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

Và sự khác biệt giữa chúng:

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

Nhiều thời gian hơn main(), mà có lẽ đã grouped_sum()nội tuyến. Tuyệt vời, cảm ơn rất nhiều, hoàn hảo.

chú thích hoàn hảo

Có một sự khác biệt trong đó thời gian được dành bên trong main() ?

Xáo trộn:

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ắp xếp

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

Không, đó là hai hướng dẫn thống trị. Vì vậy, họ mất nhiều thời gian trong cả hai trường hợp, nhưng thậm chí còn tồi tệ hơn khi dữ liệu được sắp xếp.

stat hoàn hảo

Được chứ. Nhưng chúng ta nên chạy chúng cùng một số lần, vì vậy mỗi hướng dẫn phải chậm hơn vì một số lý do. Hãy xem những gì perf statnói.

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

Chỉ có một điều nổi bật: stalled-cycling-frontend .

Được rồi, đường ống chỉ dẫn đang bị đình trệ. Ở mặt tiền. Chính xác những gì có nghĩa là có thể khác nhau giữa các vi mô.

Tôi có một dự đoán, mặc dù. Nếu bạn hào phóng, bạn thậm chí có thể gọi nó là một giả thuyết.

Giả thuyết

Bằng cách sắp xếp đầu vào, bạn sẽ tăng tính cục bộ của ghi. Trong thực tế, họ sẽ rất địa phương; hầu như tất cả các bổ sung bạn làm sẽ ghi vào cùng một vị trí như trước đó.

Điều đó thật tuyệt vời cho bộ đệm, nhưng không tuyệt vời cho đường ống dẫn. Bạn đang giới thiệu các phụ thuộc dữ liệu, ngăn không cho hướng dẫn bổ sung tiếp theo cho đến khi bổ sung trước đó hoàn thành (hoặc đã đưa ra kết quả có sẵn cho các hướng dẫn thành công )

Đó là vấn đề của bạn.

Tôi nghĩ.

Sửa nó

Nhiều vectơ tổng

Trên thực tế, chúng ta hãy thử một cái gì đó. Điều gì sẽ xảy ra nếu chúng ta sử dụng nhiều vectơ tổng, chuyển đổi giữa chúng cho mỗi lần cộng, và sau đó tóm tắt chúng ở cuối? Nó chi phí cho chúng tôi một chút địa phương, nhưng nên loại bỏ các phụ thuộc dữ liệu.

(mã không đẹp; đừng phán xét tôi, 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, và tôi cũng đã sửa tính toán n_groups; nó bị tắt bởi một.)

Các kết quả

Sau khi cấu hình tệp tạo tệp của tôi để cung cấp một -DNSUMS=...đối số cho trình biên dịch, tôi có thể làm điều này:

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   

Số lượng vectơ tổng hợp tối ưu có thể sẽ phụ thuộc vào độ sâu đường ống của CPU của bạn. CPU ultrabook 7 năm tuổi của tôi có thể có thể tối đa hóa đường ống với ít vectơ hơn so với CPU máy tính để bàn ưa thích mới.

Rõ ràng, nhiều hơn không nhất thiết phải tốt hơn; Khi tôi phát điên với các vectơ tổng cộng 128, chúng tôi bắt đầu chịu đựng nhiều hơn từ các lỗi bộ nhớ cache - bằng chứng là đầu vào bị xáo trộn trở nên chậm hơn so với sắp xếp, như bạn dự kiến ​​ban đầu. Chúng tôi đã đến vòng tròn đầy đủ! :)

Tổng số nhóm trong đăng ký

(điều này đã được thêm vào trong một chỉnh sửa)

Agh, mọt sách bắn tỉa ! Nếu bạn biết đầu vào của bạn sẽ được sắp xếp và đang tìm kiếm hiệu năng cao hơn nữa, thì việc viết lại hàm sau (không có mảng tổng thêm) thậm chí còn nhanh hơn, ít nhất là trên máy tính của tôi.

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

Thủ thuật trong cái này là nó cho phép trình biên dịch giữ gsumbiến, tổng của nhóm, trong một thanh ghi. Tôi đoán (nhưng có thể rất sai) rằng điều này nhanh hơn vì vòng phản hồi trong đường ống có thể ngắn hơn ở đây và / hoặc ít truy cập bộ nhớ hơn. Một người dự đoán chi nhánh tốt sẽ làm cho việc kiểm tra thêm cho sự bình đẳng của nhóm trở nên rẻ.

Các kết quả

Thật tồi tệ cho đầu vào xáo trộn ...

sumspeed$ time ./sum_groups < groups_shuffled
2236354315

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

... nhưng nhanh hơn khoảng 40% so với giải pháp "nhiều khoản tiền" của tôi cho đầu vào được sắp xếp.

sumspeed$ time ./sum_groups < groups_sorted
809694018

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

Rất nhiều nhóm nhỏ sẽ chậm hơn so với một vài người lớn, vì vậy có hay không này là việc thực hiện nhanh hơn sẽ thực sự phụ thuộc vào dữ liệu của bạn ở đây. Và, như mọi khi, trên mô hình CPU của bạn.

Nhiều vectơ tổng, có bù thay vì mặt nạ bit

Sopel đề nghị bốn bổ sung không được kiểm soát như là một thay thế cho phương pháp mặt nạ bit của tôi. Tôi đã triển khai một phiên bản tổng quát về đề xuất của họ, có thể xử lý khác nhau NSUMS. Tôi đang trông chờ vào trình biên dịch hủy kiểm soát vòng lặp bên trong cho chúng tôi (mà nó đã làm, ít nhất là cho 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;
}

Các kết quả

Thời gian để đo lường. Lưu ý rằng vì tôi đã làm việc trong / tmp ngày hôm qua, tôi không có cùng dữ liệu đầu vào. Do đó, những kết quả này không thể so sánh trực tiếp với những kết quả trước đó (nhưng có lẽ đủ gần).

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   

Yup, vòng lặp bên trong NSUMS=8là nhanh nhất trên máy tính của tôi. So với phương pháp "gsum địa phương" của tôi, nó cũng có thêm lợi ích là không trở nên khủng khiếp đối với đầu vào bị xáo trộn.

Thú vị cần lưu ý: NSUMS=16trở nên tồi tệ hơn NSUMS=8. Điều này có thể là do chúng ta bắt đầu thấy nhiều lỗi bộ nhớ cache hơn hoặc do chúng ta không có đủ các thanh ghi để hủy kiểm soát vòng lặp bên trong một cách chính xác.


5
Đây là niềm vui :)
Snild Dolkow

3
Thật tuyệt vời! Không biết về perf.
Tanveer Badar

1
Tôi tự hỏi nếu trong cách tiếp cận đầu tiên của bạn, việc hủy 4x thủ công với 4 bộ tích lũy khác nhau sẽ mang lại hiệu suất tốt hơn. Một cái gì đó như godbolt.org/z/S-PhFm
Sopel

Cám ơn vì sự gợi ý. Vâng, hiệu suất tăng lên và tôi đã thêm nó vào câu trả lời.
Snild Dolkow

Cảm ơn! Tôi đã xem xét một cái gì đó như thế này có thể là khả năng nhưng không biết cách xác định nó, cảm ơn vì câu trả lời chi tiết của bạn!
Jim

3

Đây là lý do tại sao các nhóm được sắp xếp chậm hơn các nhóm không được bảo vệ;

Đầu tiên ở đây là mã lắp ráp cho vòng lặp tổng hợp:

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)

Hãy xem hướng dẫn thêm là lý do chính cho vấn đề này;

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

Khi bộ xử lý thực hiện lệnh này trước tiên, nó sẽ đưa ra yêu cầu đọc (tải) bộ nhớ đến địa chỉ trong edx sau đó thêm giá trị của ecx sau đó đưa ra yêu cầu write (store) cho cùng một địa chỉ.

Có một tính năng trong sắp xếp lại bộ nhớ người gọi bộ xử lý

Để cho phép tối ưu hóa hiệu suất thực thi lệnh, kiến ​​trúc IA-32 cho phép khởi hành từ mô hình mạnh mẽ được gọi là bộ xử lý đặt hàng trong bộ xử lý gia đình Pentium 4, Intel Xeon và P6. Các biến thể sắp xếp bộ xử lý này (được gọi ở đây là mô hình sắp xếp bộ nhớ) cho phép các hoạt động nâng cao hiệu suất như cho phép đọc đi trước ghi đệm. Mục tiêu của bất kỳ biến thể nào trong số này là tăng tốc độ thực thi lệnh, đồng thời duy trì sự kết hợp bộ nhớ, ngay cả trong các hệ thống nhiều bộ xử lý.

và có một quy tắc

Đọc có thể được sắp xếp lại với ghi cũ hơn đến các vị trí khác nhau nhưng không đọc với ghi cũ hơn đến cùng một vị trí.

Vì vậy, nếu lần lặp tiếp theo đạt được lệnh add trước khi yêu cầu ghi hoàn thành, nó sẽ không chờ nếu địa chỉ edx khác với giá trị trước đó và đưa ra yêu cầu đọc và nó được sắp xếp lại theo yêu cầu ghi cũ hơn và tiếp tục thêm lệnh. nhưng nếu địa chỉ giống nhau thì lệnh add sẽ đợi cho đến khi ghi xong.

Lưu ý rằng vòng lặp ngắn và bộ xử lý có thể thực thi nó nhanh hơn bộ điều khiển bộ nhớ hoàn thành yêu cầu ghi vào bộ nhớ.

Vì vậy, đối với các nhóm được sắp xếp, bạn sẽ đọc và ghi từ cùng một địa chỉ nhiều lần liên tục để nó sẽ mất khả năng nâng cao hiệu suất bằng cách sắp xếp lại bộ nhớ; trong khi đó nếu các nhóm ngẫu nhiên được sử dụng thì mỗi lần lặp lại có thể có địa chỉ khác nhau, vì vậy việc đọc sẽ không chờ viết cũ hơn và sắp xếp lại trước nó; thêm hướng dẫn sẽ không chờ đợi trước đó để đi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.