[英]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.