2016-09-26 83 views
9

我正在開發一個項目,我們必須實現一個算法,該算法在理論上證明是緩存友好的。簡單地說,如果N是輸入,而B是每次我們發生緩存未命中時在緩存和RAM之間傳輸的元素的數量,則算法將需要O(N/B)訪問RAM。爲什麼Perf和Papi爲L3緩存引用和未命中提供了不同的值?

我想證明這確實是實踐中的行爲。爲了更好地理解如何測量各種與緩存相關的硬件計數器,我決定使用不同的工具。一個是Perf,另一個是PAPI庫。不幸的是,我使用這些工具工作得越多,我就越不瞭解他們所做的工作。

我使用的是Intel(R)Core(TM)i5-3470 CPU @ 3.20GHz,8 GB內存,L1緩存256 KB,L2緩存1 MB,L3緩存6 MB。高速緩存行大小爲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高速緩存未命中。

未優化(無-O3),這是從PERF輸出:

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 

這是非常接近我們期待。現在假設我們使用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的源文件,以找出哪些性能計數器,他們實際上這些事件映射,但事實證明它們是相同的(假設英特爾酷睿i這裏):事件2E處理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默認只對用戶空間事件進行計數。然而Perf默認收集用戶和內核空間事件。

對於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次迭代,因此你會期望你的分析1,250,000,000高速緩存未命中。現在非常接近。偏差主要來自內核在頁面清除期間由內核加載到緩存的第一個循環。

隨着PAPI一些額外的熱身圈可以在櫃檯開始前被插入,所以結果更好的符合預期:

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

嗯。我也看到了數字的不同,這是正確的,但是內核中的什麼會導致這麼多的緩存未命中?該程序都是關於在用戶空間內玩弄內存,在我的系統上,它使用相同的55個系統調用,n爲1000000,n爲100000000,如果我們不計算程序加載它在內核中做的唯一事情是映射一個區域的記憶。頁面錯誤可能?但是這麼大的數字呢? –

+2

@RomanKhimov構成其中最大部分的內核符號是'clear_page_c_e'。所以我認爲這是因爲在傳遞給用戶空間之前,每個頁面都被內核置零。這可能不會在分配時發生,而是在第一次訪問時發生。我可能在那裏錯了。稍後我會通過一些更詳細的分析來更新我的答案。 – user4407569

+0

我忘記了把mmaped的'MAP_ANONYMOUS'記憶歸零,真實,並且實際上解釋了一切。使用'MAP_UNINITIALIZED'將數字與手動'mmap()'進行比較可能會比較有趣,它也應該顯示熱調零緩存和冷未初始化緩存之間的區別。 –

相關問題