2016-09-26 17 views
9

저는 이론적으로 캐시 친화적 인 것으로 입증 된 알고리즘을 구현해야하는 프로젝트를 진행 중입니다. 간단히 말해서 N이 입력이고 B이 캐시 미스가 발생할 때마다 캐시와 RAM간에 전송되는 요소의 수인 경우 알고리즘에 O(N/B)이 RAM에 액세스해야합니다.왜 Perf와 Papi는 L3 캐시 참조 및 누락에 대해 다른 값을 제공합니까?

저는 이것이 실제로 실제로 나타나는 행동임을 보여 드리고자합니다. 다양한 캐시 관련 하드웨어 카운터를 측정하는 방법을 더 잘 이해하기 위해 다른 도구를 사용하기로 결정했습니다. 하나는 Perf이고 다른 하나는 PAPI 라이브러리입니다. 불행히도, 이러한 도구로 작업할수록 그들이하는 일을 정확히 이해하지 못합니다.

RAM (8GB RAM, L1 캐시 256KB, L2 캐시 1MB, L3 캐시 6MB)이 장착 된 Intel (R) Core ™ i5-3470 CPU @ 3.20GHz를 사용 중입니다. 캐시 라인 크기는 64 바이트입니다. 블록 크기는 B이어야합니다.

#include <iostream> 

using namespace std; 

struct node{ 
    int l, r; 
}; 

int main(int argc, char* argv[]){ 

    int n = 1000000; 

    node* A = new node[n]; 

    int i; 
    for(i=0;i<n;i++){ 
     A[i].l = 1; 
     A[i].r = 4; 
    } 

    return 0; 
} 

각 노드는 캐시 라인이 8 개 노드를 들어갈 수 있음을 의미하는 8 바이트가 필요합니다, 그래서 나는 약 1000000/8 = 125000 L3 캐시 미스를 기대해야합니다

은의 다음 예를 살펴 보자.

perf stat -B -e cache-references,cache-misses ./cachetests 

Performance counter stats for './cachetests': 

     162,813  cache-references            
     142,247  cache-misses    # 87.368 % of all cache refs  

    0.007163021 seconds time elapsed 

그것은 우리가 기대하는 것과 아주 가까이 있습니다 :

최적화없이

(NO -O3)는,이 반환 한 출력입니다. 이제 우리가 PAPI 라이브러리를 사용한다고 가정 해보십시오.

#include <iostream> 
#include <papi.h> 

using namespace std; 

struct node{ 
    int l, r; 
}; 

void handle_error(int err){ 
    std::cerr << "PAPI error: " << err << std::endl; 
} 

int main(int argc, char* argv[]){ 

    int numEvents = 2; 
    long long values[2]; 
    int events[2] = {PAPI_L3_TCA,PAPI_L3_TCM}; 

    if (PAPI_start_counters(events, numEvents) != PAPI_OK) 
     handle_error(1); 

    int n = 1000000; 
    node* A = new node[n]; 
    int i; 
    for(i=0;i<n;i++){ 
     A[i].l = 1; 
     A[i].r = 4; 
    } 

    if (PAPI_stop_counters(values, numEvents) != PAPI_OK) 
     handle_error(1); 

    cout<<"L3 accesses: "<<values[0]<<endl; 
    cout<<"L3 misses: "<<values[1]<<endl; 
    cout<<"L3 miss/access ratio: "<<(double)values[1]/values[0]<<endl; 

    return 0; 
} 

이것은 내가 얻을 출력은 다음과 같습니다

L3 accesses: 3335 
L3 misses: 848 
L3 miss/access ratio: 0.254273 

이유는 두 가지 도구와 같은 큰 차이?

+0

은 전년 동기 대비 되세요 시도 PAPI_L3_DCA 및 PAPI_L3_DCM를 사용하여 데이터 미스를 계산? – HazemGomaa

+0

PAPI_L3_DCA 만 사용할 수 있으며 같은 번호를주고있는 것 같습니다 – jsguy

답변

6

perf와 PAPI의 소스 파일을 통해 실제로이 이벤트를 매핑하는 성능 카운터를 찾을 수 있지만 동일하다고 가정합니다 (Intel Core i를 가정 할 때). 2E with umask 4F 참조는 41입니다. the Intel 64 and IA-32 Architectures Developer's Manual에서 이러한 이벤트는 설명으로 :

2EH 4FH LONGEST_LAT_CACHE.REFERENCE 마지막 레벨 캐시의 캐시 라인을 참조하는 코어에서 발생하는이 이벤트 카운트 요청.

2EH 41H LONGEST_LAT_CACHE.MISS이 이벤트는 마지막 레벨 캐시에 대한 참조에 대한 각 캐시 누락 조건을 계산합니다.

괜찮을 것 같습니다. 문제는 다른 곳에서 발생합니다.

여기에 제 재생산 된 숫자가 있습니다. 배열 길이를 100 배 증가 시켰을뿐입니다. 그렇지 않은 경우 타이밍 결과에 큰 변동이 있고 배열 길이가 1,000,000 인 경우 L3 캐시에 거의 맞습니다. main1은 PAPI가없는 첫 번째 코드 예이며 main2은 PAPI가있는 두 번째 코드 예입니다.

$ perf stat -e cache-references,cache-misses ./main1 

Performance counter stats for './main1': 

     27.148.932  cache-references            
     22.233.713  cache-misses    # 81,895 % of all cache refs 

     0,885166681 seconds time elapsed 

$ ./main2 
L3 accesses: 7084911 
L3 misses: 2750883 
L3 miss/access ratio: 0.388273 

분명히 일치하지 않습니다. 우리가 실제로 LLC 참조를 계산하는 곳을 봅시다. 여기 perf report의 처음 몇 줄이 perf record -e cache-references ./main1 후 다음과 같습니다

31,22% main1 [kernel]   [k] 0xffffffff813fdd87                                 ▒ 
    16,79% main1 main1    [.] main                                     ▒ 
    6,22% main1 [kernel]   [k] 0xffffffff8182dd24                                 ▒ 
    5,72% main1 [kernel]   [k] 0xffffffff811b541d                                 ▒ 
    3,11% main1 [kernel]   [k] 0xffffffff811947e9                                 ▒ 
    1,53% main1 [kernel]   [k] 0xffffffff811b5454                                 ▒ 
    1,28% main1 [kernel]   [k] 0xffffffff811b638a            
    1,24% main1 [kernel]   [k] 0xffffffff811b6381                                 ▒ 
    1,20% main1 [kernel]   [k] 0xffffffff811b5417                                 ▒ 
    1,20% main1 [kernel]   [k] 0xffffffff811947c9                                 ▒ 
    1,07% main1 [kernel]   [k] 0xffffffff811947ab                                 ▒ 
    0,96% main1 [kernel]   [k] 0xffffffff81194799                                 ▒ 
    0,87% main1 [kernel]   [k] 0xffffffff811947dc 

그래서 당신이 여기에서 볼 수있는 것은 실제로 캐시 참조의 16.79 %가 실제로 사용자 공간에서 일어날 것입니다, 나머지는 커널에 기인한다.

그리고 여기에 문제가 있습니다. PAPI 결과를 PAPI 결과와 비교하는 것은 불공평합니다. PAPI는 기본적으로 사용자 공간 이벤트 만 계산하기 때문입니다. 그러나 기본적으로 Perf는 사용자 및 커널 공간 이벤트를 수집합니다.

반환 한 동안 우리는 쉽게 사용자 만의 공간 컬렉션에 줄일 수

$ perf stat -e cache-references:u,cache-misses:u ./main1 

Performance counter stats for './main1': 

     7.170.190  cache-references:u           
     2.764.248  cache-misses:u   # 38,552 % of all cache refs  

     0,658690600 seconds time elapsed 

이 꽤 잘 일치하는 것으로 보인다.

편집 :

59,64% main1 [kernel]  [k] clear_page_c_e 
    23,25% main1 main1   [.] main 
    2,71% main1 [kernel]  [k] compaction_alloc 
    2,70% main1 [kernel]  [k] pageblock_pfn_to_page 
    2,38% main1 [kernel]  [k] get_pfnblock_flags_mask 
    1,57% main1 [kernel]  [k] _raw_spin_lock 
    1,23% main1 [kernel]  [k] clear_huge_page 
    1,00% main1 [kernel]  [k] get_page_from_freelist 
    0,89% main1 [kernel]  [k] free_pages_prepare 

우리가 가장 캐시가 실제로 일어날 미스 볼 수 있듯이 :

는, 디버그 기호 및 캐시 이번에 대신 참조 그리워 커널이하는 일에 조금 더 가까이 볼 수 있습니다 clear_page_c_e. 프로그램에서 새 페이지에 액세스 할 때 호출됩니다. 주석에서 설명한 것처럼 새로운 페이지는 액세스를 허용하기 전에 커널에 의해 제로화되므로 캐시 미스는 이미 여기에서 발생합니다.

커널 공간에서 캐시의 좋은 부분을 놓치기 때문에 분석 결과가 엉망입니다. 그러나 커널이 실제로 메모리에 액세스하는 정확한 상황을 보장 할 수 없으므로 코드에 의해 예상되는 동작과 다를 수 있습니다.

이 빌드를 방지하려면 어레이 주위에 루프를 추가로 작성하십시오. 내부 루프의 첫 번째 반복 만 커널 오버 헤드를 발생시킵니다. 배열의 모든 페이지에 액세스하자마자 기여도가 남지 않아야합니다. 여기에 외부 루프의 100 반복 내 결과는 다음과 같습니다

$ perf stat -e cache-references:u,cache-references:k,cache-misses:u,cache-misses:k ./main1 

Performance counter stats for './main1': 

    1.327.599.357  cache-references:u           
     23.678.135  cache-references:k           
    1.242.836.730  cache-misses:u   # 93,615 % of all cache refs  
     22.572.764  cache-misses:k   # 95,332 % of all cache refs  

     38,286354681 seconds time elapsed 

배열 길이 100 반복과 100,000,000이고, 따라서 당신이 당신의 분석에 의해 1,250,000,000 캐시 미스를 기대했을 것이다. 이것은 지금 아주 가깝다. 이탈은 대부분 페이지 지우기 중 커널에 의해 캐시로로드되는 첫 번째 루프에서 발생합니다. PAPI와

은 몇 가지 추가 워밍업 루프는 카운터가 시작되기 전에 삽입 할 수 있습니다, 그래서 결과는 더 나은 기대에 맞는 :

$ ./main2 
L3 accesses: 1318699729 
L3 misses: 1250684880 
L3 miss/access ratio: 0.948423 
+0

흠. 숫자의 차이도 보았습니다. 맞습니다. 그러나 커널에서이 많은 캐시 누락을 일으킬 수있는 것은 무엇입니까? 이 프로그램은 사용자 공간에서 메모리를 저글링하는 것에 관한 것이고, 시스템에서 1000000의 n과 100000000의 n에 대해 동일한 55 syscalls를 사용합니다. 프로그램로드를 계산하지 않으면 커널에서 유일한 작업은 영역 매핑입니다. 메모리의. 어쩌면 페이지 결함? 하지만 그만큼 큰 숫자입니까? –

+2

@RomanKhimov 이들 중 가장 큰 부분을 차지하는 커널 심볼은'clear_page_c_e'입니다. 그래서 저는 각 페이지가 사용자 공간으로 전달되기 전에 커널에 의해 제로화되기 때문에 그것이라고 생각합니다. 이것은 아마도 할당 시간에 발생하는 것이 아니라 처음 액세스 할 때 발생합니다.나는 거기에서 잘못했을 수도있다. 나중에 좀 더 상세한 분석으로 대답을 업데이트 할 것입니다. – user4407569

+0

mmaped'MAP_ANONYMOUS' 메모리를 초기화하는 것을 잊어 버렸습니다. 실제로는 모든 것을 설명합니다. 숫자를 MAP_UNINITIALIZED를 사용하여 수동으로'mmap() '과 비교하는 것은 흥미로울 것인데, 이것은 또한 워밍업에 의한 영점 조정 캐시와 콜드 초기화되지 않은 캐시 간의 차이점을 보여 주어야한다. –