정렬되지 않은 그룹보다 정렬 된 그룹에서 그룹화 된 합계가 더 느린 이유는 무엇입니까?


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

그런 다음 주어진 크기의 데이터 세트에서 이러한 프로그램을 실행 한 다음 동일한 데이터 세트의 행 순서를 섞으면 순서 섞은 데이터가 정렬 된 데이터보다 약 2 배 이상 빠른 합계를 계산합니다.

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 캐릭터가 있습니까?
1201ProgramAlarm

2
크기를 조정하지 않기 때문에 프로그램에 정의되지 않은 동작이 있습니다 sum. 대신 sums.reserve(n_groups);당신은 전화해야합니다 sums.resize(n_groups);-그것은 @Shawn이 암시하는 것입니다.
Eugene

1
두 벡터 (값 및 그룹) 대신 쌍으로 구성된 벡터가 예상대로 동작한다는 점에 유의하십시오 (예 : 여기 또는 여기 참조 ).
Bob__

1
값을 기준으로 데이터를 정렬 했습니까? 그러나 그것은 또한 그룹들을 분류하고 그것은 xpression에 영향을 미칩니다 p_out[p_g[i]] += p_x[i];. 어쩌면 원래 스크램블 순서에서 그룹은 실제로 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 회에서 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. 캐시 누락이 더 많이 발생하기 시작했거나 내부 루프를 올바르게 풀기위한 레지스터가 충분하지 않기 때문일 수 있습니다.


5
재미있었습니다. :)
Snild Dolkow

3
대단 했어! 에 대해 몰랐습니다 perf.
Tanveer Badar

1
첫 번째 접근법에서 4 개의 다른 누산기로 4x를 수동으로 풀면 더 나은 성능을 얻을 수 있을지 궁금합니다. 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)

이 문제의 주요 원인 인 add 명령어를 살펴 보겠습니다.

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

프로세서가이 명령어를 먼저 실행하면 edx의 주소에 메모리 읽기 (로드) 요청을 발행 한 다음 ecx 값을 추가 한 다음 동일한 주소에 대한 쓰기 (저장) 요청을 발행합니다.

프로세서 호출자 메모리 재정렬에 기능이 있습니다

명령어 실행의 성능 최적화를 위해 IA-32 아키텍처는 Pentium 4, Intel Xeon 및 P6 제품군 프로세서의 프로세서 주문이라는 강력한 주문 모델에서 출발 할 수 있습니다. 이러한 프로세서 순서 변경 (여기서는 메모리 순서 모델이라고 함)을 통해 읽기가 버퍼 된 쓰기보다 앞서는 것과 같은 성능 향상 작업이 가능합니다. 이러한 변형의 목표는 다중 프로세서 시스템에서도 메모리 일관성을 유지하면서 명령 실행 속도를 높이는 것입니다.

그리고 규칙이있다

읽기는 다른 위치에 대한 오래된 쓰기로 정렬 될 수 있지만 같은 위치에 대한 오래된 쓰기로는 다시 정렬되지 않을 수 있습니다.

따라서 쓰기 요청이 완료되기 전에 다음 반복이 추가 명령에 도달하면 edx 주소가 이전 값과 다를 때까지 기다리지 않고 읽기 요청을 발행하고 이전 쓰기 요청에 대해 다시 정렬하고 추가 명령이 계속됩니다. 그러나 주소가 동일하면 추가 명령은 이전 쓰기가 완료 될 때까지 기다립니다.

루프가 짧고 메모리 컨트롤러가 메모리에 쓰기 요청을 완료하는 것보다 프로세서가 더 빠르게 루프를 실행할 수 있습니다.

따라서 정렬 된 그룹의 경우 동일한 주소에서 여러 번 연속해서 읽고 쓸 수 있으므로 메모리 재정렬을 사용하여 성능이 향상되지 않습니다. 반면 임의의 그룹이 사용되면 각 반복마다 주소가 다를 수 있으므로 읽기는 이전 쓰기를 기다리지 않고 순서를 다시 정렬하지 않습니다. add 명령어는 이전 명령어를 기다리지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.