【问题标题】:Cachegrind: Why so many cache misses?Cachegrind:为什么有这么多缓存未命中?
【发布时间】:2019-04-13 08:59:41
【问题描述】:

我目前正在学习 Linux 下的各种分析和性能实用程序,尤其是 valgrind/cachegrind。

我有以下玩具程序:

#include <iostream>
#include <vector>

int
main() {
    const unsigned int COUNT = 1000000;

    std::vector<double> v;

    for(int i=0;i<COUNT;i++) {
        v.push_back(i);
    }

    double counter = 0;
    for(int i=0;i<COUNT;i+=8) {
        counter += v[i+0];
        counter += v[i+1];
        counter += v[i+2];
        counter += v[i+3];
        counter += v[i+4];
        counter += v[i+5];
        counter += v[i+6];
        counter += v[i+7];
    }

    std::cout << counter << std::endl;
}

用g++ -O2 -g main.cpp编译这个程序并运行valgrind --tool=cachegrind ./a.out,然后cg_annotate cachegrind.out.31694 --auto=yes产生如下结果:

    --------------------------------------------------------------------------------
-- Auto-annotated source: /home/andrej/Data/projects/pokusy/dod.cpp
--------------------------------------------------------------------------------
       Ir I1mr ILmr        Dr    D1mr    DLmr        Dw D1mw DLmw 

        .    .    .         .       .       .         .    .    .  #include <iostream>
        .    .    .         .       .       .         .    .    .  #include <vector>
        .    .    .         .       .       .         .    .    .  
        .    .    .         .       .       .         .    .    .  int
        7    1    1         1       0       0         4    0    0  main() {
        .    .    .         .       .       .         .    .    .      const unsigned int COUNT = 1000000;
        .    .    .         .       .       .         .    .    .  
        .    .    .         .       .       .         .    .    .      std::vector<double> v;
        .    .    .         .       .       .         .    .    .  
5,000,000    0    0 1,999,999       0       0         0    0    0      for(int i=0;i<COUNT;i++) {
3,000,000    0    0         0       0       0 1,000,000    0    0          v.push_back(i);
        .    .    .         .       .       .         .    .    .      }
        .    .    .         .       .       .         .    .    .  
        3    0    0         0       0       0         0    0    0      double counter = 0;
  250,000    0    0         0       0       0         0    0    0      for(int i=0;i<COUNT;i+=8) {
  250,000    0    0   125,000       1       1         0    0    0          counter += v[i+0];
  125,000    0    0   125,000       0       0         0    0    0          counter += v[i+1];
  125,000    1    1   125,000       0       0         0    0    0          counter += v[i+2];
  125,000    0    0   125,000       0       0         0    0    0          counter += v[i+3];
  125,000    0    0   125,000       0       0         0    0    0          counter += v[i+4];
  125,000    0    0   125,000       0       0         0    0    0          counter += v[i+5];
  125,000    0    0   125,000 125,000 125,000         0    0    0          counter += v[i+6];
  125,000    0    0   125,000       0       0         0    0    0          counter += v[i+7];
        .    .    .         .       .       .         .    .    .      }
        .    .    .         .       .       .         .    .    .  
        .    .    .         .       .       .         .    .    .      std::cout << counter << std::endl;
       11    0    0         6       1       1         0    0    0  }

我担心的是这条线:

125,000    0    0   125,000 125,000 125,000         0    0    0          counter += v[i+6];

为什么这条线有这么多缓存未命中?数据在连续的内存中,每次迭代我都读取 64 字节的数据(假设缓存线的长度为 64 字节)。

我在 Ubuntu Linux 18.04.1、内核 4.19、g++ 7.3.0 上运行这个程序。 电脑是 AMD 2400G。

【问题讨论】:

  • 什么是列图例? D1mr 是什么?
  • @VTT 来自valgrind.org/docs/manual/cg-manual.html:I cache reads (Ir, which equals the number of instructions executed), I1 cache read misses (I1mr) and LL cache instruction read misses (ILmr). D cache reads (Dr, which equals the number of memory reads), D1 cache read misses (D1mr), and LL cache data read misses (DLmr). D cache writes (Dw, which equals the number of memory writes), D1 cache write misses (D1mw), and LL cache data write misses (DLmw).
  • 我在这里没有看到很多错过的阅读。那一行......在循环这么多次之后,你一定会得到一个错过的缓存(接近结尾对我来说很有说服力)。与读取次数相比,未命中次数似乎很糟糕(125000/8 百万+?)我读对了吗?
  • @Galik 虽然看起来还不错,但它不是 0,所以还不够好;)
  • 出于兴趣,您是否尝试仅通过 1 增加循环并让优化器自行展开循环以查看其执行情况? (还有 -O3 )?

标签: c++ performance profiling cpu-cache cachegrind


【解决方案1】:

首先检查生成的汇编代码很重要,因为这就是 cachegrind 将要模拟的内容。您感兴趣的循环被编译成以下代码:

.L28:
addsd xmm0, QWORD PTR [rax]
add rax, 64
addsd xmm0, QWORD PTR [rax-56]
addsd xmm0, QWORD PTR [rax-48]
addsd xmm0, QWORD PTR [rax-40]
addsd xmm0, QWORD PTR [rax-32]
addsd xmm0, QWORD PTR [rax-24]
addsd xmm0, QWORD PTR [rax-16]
addsd xmm0, QWORD PTR [rax-8]
cmp rdx, rax
jne .L28

每次迭代有 8 次读取访问,每个读取访问大小为 8 字节。在 C++ 中,保证每个元素都是 8 字节对齐的,但每次迭代最多可以访问两个缓存行,具体取决于v 向量的数组地址。 cachegrind 使用动态二进制检测来获取每个内存访问的地址,并应用其缓存层次模型来确定访问在层次结构中的每个级别是命中还是未命中(尽管它仅支持 L1 和 LLC)。在这个特定的实例中,碰巧在counter += v[i+6]; 访问了一个新的高速缓存行。然后,接下来的 7 次访问将访问相同的 64 字节高速缓存行。访问新缓存行的源代码行不会影响 cachegrind 报告的未命中总数。它只会告诉您,不同的源代码行会导致很多错误。

请注意,cachegrind 会根据运行它的机器模拟一个非常简化的缓存层次结构。在这种情况下,它是 AMD 2400G,它在所有高速缓存级别都有 64 字节的行大小。此外,L3 的大小为 4MB。但是由于数组总大小是8MB,那么下面的循环:

for(int i=0;i<COUNT;i++) {
    v.push_back(i);
}

将只在 LLC 中留下数组的后半部分。现在在计算counter 的第二个循环的第一次迭代中,访问的第一行将不在L1 或LLC 中。这解释了 D1mr 和 DLmr 列中的 1。然后在counter += v[i+6];,访问了另一行,这也是两级缓存中的未命中。但是,在这种情况下,接下来的 7 次访问都将是命中。此时,只有来自counter += v[i+6]; 的访问会丢失,并且有 125,000 次这样的访问(100 万/8)。

请注意,cachegrind 只是一个模拟器,在真实处理器上实际发生的情况可能而且很可能非常不同。例如,在我的 Haswell 处理器上,通过使用 perf,所有代码(两个循环)的 L1D 未命中总数仅为 65,796。因此,cachegrind 可能会显着高估或低估未命中和命中数。

【讨论】:

  • 是的,你说得对。当我使用反向迭代器:for(auto it = v.rbegin(); it != v.rend(); ++it) { counter += *it; } 时,cachegrind 只报告一半的未命中。我还尝试了perf stat -e L1-dcache-load-misses ./a.out,它在我的计算机上振荡约 105,000 次(对于两个循环)。这是否意味着 AMD 的预取器比 Intel 更差?我也不知道cachegrind只是模拟器......
  • @AndrejKesely 这不仅取决于硬件缓存预取器,还取决于缓存替换策略和访问位置的物理地址(cachegrind 仅使用虚拟地址)。如果我在 Haswell 上禁用预取器,整个程序会出现 191,997 次 L1D 未命中(启用预取器时为 65,796 次)。
【解决方案2】:

我怀疑这是因为向量缓冲区未在缓存行边界上对齐。那是缓存未命中的突然跳跃标志着我们进入下一行时的一个点。所以我建议检查v.data()值。

【讨论】:

  • 除了编写自定义分配器之外,OP 有什么方法可以获得正确对齐的向量?
  • @NathanOliver 我认为 OP 可以分配一个字节数组并选择一个对齐的起始位置。或者只是在双精度向量中选择一个对齐的起始位置。
  • 不是将向量对齐到缓存行边界只会使缓存未命中的位置移动到不同的位置吗?您是否仍会像遍历内存一样经常出现缓存未命中?
  • @Galik 是的,现在我尝试使用double* v = (double *)aligned_alloc(64, COUNT * 8); 而不是std::vector 分配向量,并且缓存未命中在线counter += v[i+0];。我认为 CPU 预取器会随时获取数据。
  • @Galik 是的,但是这种错位解释了为什么缓存未命中发生在循环中间。
【解决方案3】:

在我看来,如果我们忘记前 100 万次后推(8Mb ......好吧,也许你在 L2 中没有足够的空间来处理它),这看起来绝对没问题。因此,如果我们假设我们的数据不在任何级别的缓存中,那么每次您读取 8 个双精度数据时,您都必须向 RAM 询问下一条 L1 行。所以总的来说你的统计数据看起来不错。由于简单的顺序访问模式,您正在调用 QWORD 读取 1M 次并生成 125k 对 RAM 的请求。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-09-04
    • 2013-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    相关资源
    最近更新 更多