为什么Perf和Papi对L3缓存引用和缺失给出不同的值
Why does Perf and Papi give different values for L3 cache references and misses?
我正在研究一个项目,在这个项目中,我们必须实现一个在理论上被证明是缓存友好的算法。简单来说,如果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缓存失败。
没有优化(没有-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): Event 2E
和umask 4F
用于引用,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
这些显然不匹配。让我们看看我们在哪里计算有限责任公司的引用。以下是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 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,000,000,迭代100次,因此根据分析,您将期望有1,250,000,000缓存失败。现在已经很接近了。这个偏差主要来自于内核在清理页面时加载到缓存中的第一个循环。
使用PAPI,可以在计数器开始之前插入一些额外的预热循环,因此结果更符合预期:
$ ./main2
L3 accesses: 1318699729
L3 misses: 1250684880
L3 miss/access ratio: 0.948423
- 将对象数组的引用传递给函数
- 什么时候在C++中返回常量引用是个好主意
- 我想将一个对T类型的非常量左值引用绑定到一个T类型的临时值
- 何时在引用或唯一指针上使用移动语义
- 如何在c++中使用引用实现类似python的行为
- 编译C++时未定义的引用
- Ctypes wstring通过引用传递
- c++r值引用应用于函数指针
- 理解c++中的引用
- C++取消引用指针.为什么会发生变化
- 如何修复此错误:未定义对"距离(浮点数,浮点数,浮点数,浮点数,浮点数)"的引用
- 我的项目不会像"undefined reference to `grpc::g_core_codegen_interface'"那样使用未定义的引用错误进行编译
- C++Boost Asio Pool线程,带有lambda函数和传递引用变量
- 强制转换为引用类型
- 引用一个已擦除类型(void*)的指针
- 向量元素的引用地址与它所指向的向量元素的地址不同.为什么
- 具有默认值的引用获取函数
- 如何使用基类指针引用派生类成员
- 使用取消引用的指针的多态性会产生意外的结果.为什么?
- 为什么Perf和Papi对L3缓存引用和缺失给出不同的值