代码之家  ›  专栏  ›  技术社区  ›  Andrej Kesely

Cachegrind:为什么这么多缓存丢失?

  •  7
  • Andrej Kesely  · 技术社区  · 5 年前

    我目前正在学习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字节长)。

    我在ubuntulinux 18.04.1,kernel4.19,g++7.3.0上运行这个程序。 计算机是AMD 2400G。

    2 回复  |  直到 5 年前
        1
  •  4
  •   Hadi Brais    5 年前

    首先检查生成的汇编代码是很重要的,因为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基于运行它的机器模拟了一个非常简化的缓存层次结构。在本例中,它是amd2400g,在所有缓存级别上都有64字节的行大小。此外,L3的大小为4MB。但由于总的数组大小是8MB,因此以下循环:

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

    将只在LLC中保留数组的后半部分。现在在第二个循环的第一个迭代中 counter 计算后,访问的第一行将不在L1或LLC中。这解释了中的1 D1mr DLmr 柱。然后在 计数器+=v[i+6]; 计数器+=v[i+6]; 将错过和有125000这样的访问(100万/8)。

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

        2
  •  2
  •   user7860670    5 年前

    我怀疑这是因为向量缓冲区没有在缓存线边界上对齐。当我们继续下一行时,缓存中的突然跳转未命中标记了一个点。所以我建议你检查一下 v.data() 价值观。

        3
  •  1
  •   user2713607    5 年前

    在我看来,如果我们忘记了前1百万次的后推(8Mb。。。嗯,也许你没有足够的空间在L2)。因此,如果我们假设数据不在任何级别的缓存中,那么每次读取8倍的数据时,就必须向RAM请求下一个L1行。所以总体来说你的数据看起来不错。由于simplet顺序访问模式,您正在调用QWORD reads 1M次,并向RAM生成125k个请求。