설정 / 느리게
우선, 프로그램은 다음에 관계없이 거의 같은 시간에 실행됩니다.
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 회에서 1000 회 반복으로 변경하면 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()있습니다. 아마 인라인 되었을 것입니다 . 고마워요, 성능
성능에 주석을 달다
시간이 소요되는 경우에 차이가 있습니까 내부는 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 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
stalled-cycles-frontend 한 가지만 눈에 stands니다 .
자, 명령 파이프 라인이 멈추고 있습니다. 프론트 엔드에서. 정확하게 의미하는 것은 아마도 마이크로 아키텍처마다 다릅니다.
그래도 추측이 있습니다. 관대하다면 가설이라고 할 수도 있습니다.
가설
입력을 정렬하면 쓰기의 지역성이 높아집니다. 실제로, 그들은 매우 지역적 일 것입니다 . 거의 모든 추가 내용은 이전 위치와 동일한 위치에 기록됩니다.
캐시에는 좋지만 파이프 라인에는 좋지 않습니다. 당신은 이전의 추가가 완료 될 때까지 소송 절차에서 다음 추가 명령을 방지, 데이터 종속성을 도입하고 (또는 한 다른 지침을 연속으로 사용할 수있는 결과를 만들어 )
그게 네 문제 야
내 생각에
그것을 고치기
여러 합 벡터
실제로, 시도해 봅시다. 여러 합 벡터를 사용하여 각 추가에 대해 벡터를 전환 한 다음 마지막에 합산하면 어떻게 될까요? 약간의 지역성이 필요하지만 데이터 종속성을 제거해야합니다.
(코드는 예쁘지 않습니다. 인터넷으로 판단하지 마십시오!)
#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 계산을 수정했습니다. 그것은 하나에 의해 벗어났습니다.)
결과
-DNSUMS=...컴파일러에 인수 를 제공하도록 makefile을 구성한 후 다음을 수행 할 수 있습니다.
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의 파이프 라인 깊이에 따라 달라질 수 있습니다. 7 살짜리 울트라 북 CPU는 아마도 새로운 멋진 데스크탑 CPU보다 적은 수의 벡터로 파이프 라인을 최대한 활용할 수있을 것입니다.
분명히 더 많은 것이 반드시 더 좋은 것은 아닙니다. 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 모델에서도 마찬가지입니다.
비트 마스킹 대신 오프셋이있는 다중 합계 벡터
소펠 은 비트 마스킹 방식에 대한 대안으로 4 개의 언 롤링 추가를 제안했습니다. 다른 제안을 처리 할 수있는 일반화 된 버전의 제안을 구현했습니다 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[]당신이 볼 수 있습니다.