簡體   English   中英

為什么Perf和Papi為L3緩存引用和未命中提供不同的值?

[英]Why does Perf and Papi give different values for L3 cache references and misses?

我正在開發一個項目,我們必須實現一個在理論上被證明是緩存友好的算法。 簡單來說,如果N是輸入, B是每次我們有高速緩存未命中時在高速緩存和RAM之間傳輸的元素數,則該算法將需要對RAM進行O(N/B)訪問。

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

我正在使用Intel(R)Core(TM)i5-3470 CPU @ 3.20GHz,8 GB RAM,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

為什么兩個工具之間有這么大的差異?

你可以瀏覽perf和PAPI的源文件,找出他們實際映射這些事件的性能計數器,但事實證明它們是相同的(假設Intel Core i在這里):帶有umask 4F事件2E用於引用和41因為未命中 Intel 64和IA-32架構開發人員手冊中,這些事件描述為:

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 record -e cache-references ./main1之后的幾行perf report

  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 當我們的程序訪問新頁面時調用此方法。 正如評論中所解釋的,在允許訪問之前,內核將新頁面歸零,因此緩存未命中已在此處發生。

這與您的分析混淆,因為您期望在內核空間中發生的緩存未命中的很大一部分。 但是,您無法保證內核實際訪問內存的確切情況,因此可能會偏離代碼所期望的行為。

為了避免這種情況,在數組填充周圍建立一個額外的循環。 只有內部循環的第一次迭代才會產生內核開銷。 一旦訪問了數組中的每個頁面,就不會有任何貢獻。 這是我重復外循環的結果:

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

使用PAPI,可以在計數器啟動之前插入一些額外的預熱循環,因此結果更符合預期:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM