Cachegrind:为什么这么多缓存未命中?

时间:2018-11-09 18:48:34

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

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

我有以下玩具程序:

conv.data['currentIndex'] = parseInt(conv.data['currentIndex']) + 1;


conv.data['currentIndex'] = parseInt(conv.data['currentIndex'])++;


conv.data['currentIndex'] += 1;

使用#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  }

为什么此行有这么多的缓存缺失?数据位于连续的内存中,每次迭代我都读取64字节的数据(假设缓存行的长度为64字节)。

我正在Ubuntu Linux 18.04.1,内核4.19,g ++ 7.3.0上运行此程序。 电脑是AMD 2400G。

3 个答案:

答案 0 :(得分:4)

首先检查生成的汇编代码非常重要,因为那是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中。这说明了D1mrDLmr列中的1。然后在counter += v[i+6];,访问另一行,这也是高速缓存的两个级别中的未命中内容。但是,在这种情况下,接下来的7个访问都将被点击。此时,只有来自counter += v[i+6];的访问会丢失,并且有125,000个这样的访问(100万/ 8)。

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

答案 1 :(得分:2)

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

答案 2 :(得分:1)

在我看来,如果我们忘记了最初的1M后推功能(8Mb ...那么也许您在L2中没有足够的空间),这看起来绝对可以。因此,如果我们假设数据不在任何高速缓存中,那么每次读取8倍数据时,都必须向RAM请求下一个L1行。因此,总体而言,您的统计数据看起来不错。由于简单的顺序访问模式,您正在调用QWORD读取1M次并向RAM生成125k请求。