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 stat
nó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ữ gsum
biế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=8
là 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=16
trở 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.
.at()
hoặc chế độ gỡ lỗioperator[]
giới hạn kiểm tra xem bạn có thấy không.