ตั้งค่า / ทำให้ช้า
ก่อนอื่นโปรแกรมจะทำงานในเวลาเดียวกันโดยไม่คำนึงถึง:
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
อาจเป็นเพราะเราเริ่มเห็นแคชหายไปมากกว่านี้หรือเนื่องจากเรามีการลงทะเบียนไม่เพียงพอในการปลดลูปภายในอย่างถูกต้อง
.at()
หรือโหมดดีบักoperator[]
ที่มีขอบเขต ตรวจสอบคุณจะเห็น