我正在做一个项目,我们必须实现一个理论上证明是缓存友好的算法。简单来说,如果N
是输入,B
是每次缓存未命中时在缓存和 RAM 之间传输的元素数,算法将需要O(N/B)
访问 RAM。
我想证明这确实是实践中的行为。为了更好地理解如何测量各种与缓存相关的硬件计数器,我决定使用不同的工具。一个是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 缓存未命中。
没有优化(no -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
为什么这两种工具之间有如此大的差异?