2017-03-02 19 views
12

다음 예제 코드는 N 크기의 행렬을 생성하고 SAMPLES 횟수만큼 이항합니다. N = 512 일 때, 조옮김 작업의 평균 실행 시간은 2144 μs (coliru link)입니다. 첫번째보기에서 오른쪽? ... 여기왜 이러한 매트릭스 전이 시간은 역 직관적입니까?

  • N = 5131451 μs
  • N = 519600 μs
  • N = 530486 μs
  • N = 540에 대한 결과가

    잘있다, 아무것도 특별 없다 → 492 μs (마침내 이론이 시작됩니다 :).

그럼 왜 이러한 간단한 계산이 이론과 너무 다른가요? 이 동작은 CPU 캐시 일관성 또는 캐시 누락과 관련이 있습니까? 그렇다면 설명해주십시오.

#include <algorithm> 
#include <iostream> 
#include <chrono> 

constexpr int N  = 512; // Why is 512 specifically slower (as of 2016) 
constexpr int SAMPLES = 1000; 
using us = std::chrono::microseconds; 

int A[N][N]; 

void transpose() 
{ 
    for (int i = 0 ; i < N ; i++) 
    for (int j = 0 ; j < i ; j++) 
     std::swap(A[i][j], A[j][i]); 
} 

int main() 
{ 
    // initialize matrix 
    for (int i = 0 ; i < N ; i++) 
    for (int j = 0 ; j < N ; j++) 
     A[i][j] = i+j; 

    auto t1 = std::chrono::system_clock::now(); 
    for (int i = 0 ; i < SAMPLES ; i++) 
     transpose(); 
    auto t2 = std::chrono::system_clock::now(); 

    std::cout << "Average for size " << N << ": " << std::chrono::duration_cast<us>(t2 - t1).count()/SAMPLES << " (us)"; 
} 
+0

스 니펫을 몇 번 실행 했습니까? 실행 시간은 시스템에서 수행 할 수있는 작업의 수에 따라 실행마다 크게 다를 수 있습니다. 이 평균 시간은 약 10 ~ 20 회입니까? 아니면 한 번 실행 한 것입니까? – JGroven

+1

아마도 512는 캐시에 끔찍하게 매치되는 마술 크기이므로 많은 캐시 누락이 발생합니다. 다른 크기는 더 잘 맞아서 더 적은 미스를 얻습니다. – NathanOliver

+0

틀린 길 @NathanOliver - 512는 513보다 많이 느립니다 * –

답변

6

캐시 미스입니다. valgrind --tool=cachegrind을 사용하여 누락 된 부분을 확인할 수 있습니다.

Average for size 512: 13052 (us)==21803== 
==21803== I refs:  1,054,721,935 
==21803== I1 misses:   1,640 
==21803== LLi misses:   1,550 
==21803== I1 miss rate:   0.00% 
==21803== LLi miss rate:   0.00% 
==21803== 
==21803== D refs:  524,278,606 (262,185,156 rd + 262,093,450 wr) 
==21803== D1 misses:  139,388,226 (139,369,492 rd +  18,734 wr) 
==21803== LLd misses:   25,828 (  7,959 rd +  17,869 wr) 
==21803== D1 miss rate:   26.6% (  53.2%  +   0.0% ) 
==21803== LLd miss rate:   0.0% (  0.0%  +   0.0% ) 
==21803== 
==21803== LL refs:   139,389,866 (139,371,132 rd +  18,734 wr) 
==21803== LL misses:   27,378 (  9,509 rd +  17,869 wr) 
==21803== LL miss rate:   0.0% (  0.0%  +   0.0% ) 

동안, N=530을 사용하여 다음과 같은 출력을 가지고 : 당신이 볼 수 있듯이, D1 (512)에 그리워

Average for size 530: 13264 (us)==22783== 
==22783== I refs:  1,129,929,859 
==22783== I1 misses:   1,640 
==22783== LLi misses:   1,550 
==22783== I1 miss rate:   0.00% 
==22783== LLi miss rate:   0.00% 
==22783== 
==22783== D refs:  561,773,362 (280,923,156 rd + 280,850,206 wr) 
==22783== D1 misses:  32,899,398 (32,879,492 rd +  19,906 wr) 
==22783== LLd misses:   26,999 (  7,958 rd +  19,041 wr) 
==22783== D1 miss rate:   5.9% (  11.7%  +   0.0% ) 
==22783== LLd miss rate:   0.0% (  0.0%  +   0.0% ) 
==22783== 
==22783== LL refs:   32,901,038 (32,881,132 rd +  19,906 wr) 
==22783== LL misses:   28,549 (  9,508 rd +  19,041 wr) 
==22783== LL miss rate:   0.0% (  0.0%  +   0.0% ) 

(530)에 비해 약 3.5 배 더 크다을 N = 512을 사용하여 다음과 같은 결과를 얻었다

+0

그래서 해결책은 사용되지 않는 열 (경우에 따라서는 행)을 남겨두고 "캐시 친숙한"행렬에 다음 큰 크기의 행렬을 사용하는 것이지만 더 빠를 것입니다. – rcgldr

+0

예, 그리고 높은 비율의 누락은 연관성이 특정 메모리 액세스 패턴 하에서 총 캐시의 일부만 사용되도록 허용하기 때문입니다. –

+1

@rcgldr : 더 나은 해결책은 메모리 액세스 순서를 변경하는 것입니다. 단일 요소를 교환하는 대신 4x4 블록을 스왑합니다. 이렇게하면 스왑의 양쪽 끝에 대해 ​​동일한 캐시 행의 모든 ​​요소에 액세스 할 수 있습니다. –