linux 切换索引顺序后,2d矩阵上的迭代会在perf中生成更多缓存引用

ndh0cuux  于 2022-11-22  发布在  Linux
关注(0)|答案(1)|浏览(76)

我偶然发现了this GitHub repo,它链接到this关于缓存感知编程的博客文章。它比较perf输出中缓存引用和缓存未命中的数量,这取决于数组中数据的分布与该高速缓存行大小的关系。我想自己做一个类似的实验。我得到的代码如下所示:

// depth_first.c
#include <stdint.h>
#include <stdlib.h>
#define ROWS 100000
#define COLS 64

int main() {
    uint8_t (*mat)[COLS] = malloc(sizeof(uint8_t[ROWS][COLS])); // matrix

    uint8_t sum = 0;

    for(int row_idx = 0; row_idx < ROWS; row_idx++) {
        for(int col_idx = 0; col_idx < COLS; col_idx++) {
            sum += mat[row_idx][col_idx];
        }
    }

    free(mat);
}

(我知道我不会在malloc(即UB)之后初始化数组,但我对实际值不感兴趣,我认为它不会影响该高速缓存性能)
我动态地分配了一个uint8_t的二维数组,称为mat。它的维数是100000行,每行64列。据我所知,这意味着数组的内存布局如下:

[ Row 0, 64 bytes ][ Row 1, 64 bytes ][ Row 2, 64 bytes ]...[ Row 99999, 64 bytes ]

每一行都占据了一个连续的内存区域。我还特意选择了64字节,因为这是我的CPU上的一个缓存行的大小。这意味着一行应该完全适合一个缓存行。
现在,我的实验如下:我通过访问第一行内的每一列来“深度优先”遍历数组,然后移动到第二行,等等,如上面的原始代码所示。然后我修改代码,通过遍历每行的第一个元素,然后每行的第二个元素,等等来“宽度优先”访问数组:

// breadth_first.c for loop
for(int col_idx = 0; col_idx < COLS; col_idx++) { // col_idx in outer loop
    for(int row_idx = 0; row_idx < ROWS; row_idx++) { // row_dix in inner
        sum += mat[row_idx][col_idx];
    }
}

我编译这两个版本时都没有进行优化:

gcc -O0 breadth_first.c -o breadth_first
gcc -O0 depth_first.c -o depth_first

并使用perf进行测试:

perf stat -e cache-references,cache-misses ./breadth_first
perf stat -e cache-references,cache-misses ./depth_first

我收到的输出如下所示(迭代之间的数字和百分比变化很小):

Performance counter stats for './breadth_first':

        12 654 452      cache-references:u                                          
           106 456      cache-misses:u            #    0,841 % of all cache refs    

       0,015068004 seconds time elapsed

       0,015102000 seconds user
       0,000000000 seconds sys


 Performance counter stats for './depth_first':

           213 178      cache-references:u                                          
             5 901      cache-misses:u            #    2,768 % of all cache refs    

       0,026617312 seconds time elapsed

       0,026690000 seconds user
       0,000000000 seconds sys

现在,我期望看到的是两者中相似的缓存引用数量,在breadth_first的情况下,缓存未命中的百分比/数量更大。我期望两者中相似的引用数量,因为两个版本的代码做相同的“事情”,并对2d数组执行相同数量的访问。
相反,cache-references的数量急剧增长。虽然cache-miss也在增长,但在breadth_first的情况下,百分比实际上更好(可能是由于大量的引用,所以百分比本身并不能说明任何问题)。
现在我的问题是,是什么导致缓存引用的增加?

更新

我正在AMD CPU上测试这一点,以防万一。
我在内核源代码中找到了从事件ID到硬件事件ID的Map。我现在正在尝试浏览AMD PPR第2.1.14节,以找到底层硬件事件的描述。我仍然希望您能对该问题给出明智的回答。

zdwk9cvp

zdwk9cvp1#

在我有更多的时间来研究这个主题之后,我自己回答了我的问题。感谢@Ladislus和@Peter Cordes的评论,因为他们的提示把我带到了正确的方向。答案会有点长,因为这个问题是为了解释一个现象,而不是解决问题,我想给予一个很好的解释发生了什么。
开始,我目前正在测试的CPU是AMD 4650 U移动的APU。/proc/cpuinfo的摘录澄清了一些重要的细节:

vendor_id   : AuthenticAMD
cpu family  : 23
model       : 96
model name  : AMD Ryzen 5 PRO 4650U with Radeon Graphics

为了记录在案,这可能是一个不同的CPU从一个我正在使用的问题,但这并不重要。CPU系列是23,所以17 h在十六进制和型号是96,这是60 h。这是重要的决定在哪里寻找信息。
由于我是AMD的,我开始在AMD PPR中寻找。在写这个答案的时候,有多个版本的PPR针对不同的家庭和型号范围在这些家庭。我显然咨询了PPR意味着家庭17 h和型号60 h。大多数信息应该在那里,主要在2.1.14“MSR寄存器”和2.1.15“性能监视计数器”中。2它感觉像是这类信息最“官方”的来源,正如man perf-list在“原始硬件事件描述符”部分所暗示的那样:

For instance on x86 CPUs, N is a hexadecimal value that represents the
raw register encoding with the layout of IA32_PERFEVTSELx MSRs (see
[Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume
3B: System Programming Guide] Figure 30-1 Layout of IA32_PERFEVTSELx
MSRs) or AMD’s PERF_CTL MSRs (see the Core Complex (CCX) → Processor
x86 Core → MSR Registers section of the [AMD Processor Programming
Reference (PPR)] relevant to the family, model and stepping of the
processor being used).

可悲的是,我发现PPR是很难导航,不能真正解码任何信息。
最后,我发现Linux内核源代码中使用最多的是/arch/x86/events/amd/core.c文件(mirror)包含从事件到事件ID的Map,以及十六进制的Umask,而/tools/perf/pmu-events/arch/x86/amdzen2/中的文件(特别是core.jsoncache.jsonmemory.json)列出了所有的事件。选择目录amdzen 2是因为我的CPU的微拱是zen 2,但是也可以通过检查mapfile.csv来找到它,该mapfile.csv基于来自/proc/cpuinfo和正则表达式的信息(看起来像什么)与特定目录匹配。
对于事件,它们被编码为4位十六进制数。根据我收集的信息,前2位是UMask,而第二对是Event id。在core.c中,我找到了从Linux事件名称到硬件事件id的Map(对于系列17 h):

static const u64 amd_f17h_perfmon_event_map[PERF_COUNT_HW_MAX] =
{
    [PERF_COUNT_HW_CPU_CYCLES]      = 0x0076,
    [PERF_COUNT_HW_INSTRUCTIONS]        = 0x00c0,
    [PERF_COUNT_HW_CACHE_REFERENCES]    = 0xff60,
    [PERF_COUNT_HW_CACHE_MISSES]        = 0x0964,
    [PERF_COUNT_HW_BRANCH_INSTRUCTIONS] = 0x00c2,
    [PERF_COUNT_HW_BRANCH_MISSES]       = 0x00c3,
    [PERF_COUNT_HW_STALLED_CYCLES_FRONTEND] = 0x0287,
    [PERF_COUNT_HW_STALLED_CYCLES_BACKEND]  = 0x0187,
};

看起来缓存引用Map到id 0xff 60,缓存未命中Map到0x 0964。这可以通过运行perf stat来验证,同时使用事件的友好名称和硬件id,计数是相同的:

perf stat -e cache-references,rff60,cache-misses,r0964 ./breadth

 Performance counter stats for './breadth':

     1 260 682 257      cache-references:u                                                 
     1 260 682 257      rff60:u                                                            
        10 728 501      cache-misses:u                   #    0,851 % of all cache refs    
        10 728 501      r0964:u

这也解释了为什么深度优先搜索和广度优先搜索之间的引用计数会增加。正如注解中所猜测的,cache-references事件包括该高速缓存的读写,以及一些其他指标。具体来说,cache.json以事件ID为0x 60的事件列表开始。每个事件都有一个单位UMask:

[
  {
    "EventName": "l2_request_g1.rd_blk_l",
    "EventCode": "0x60",
    "BriefDescription": "All L2 Cache Requests (Breakdown 1 - Common). Data cache reads (including hardware and software prefetch).",
    "UMask": "0x80"
  },
  {
    "EventName": "l2_request_g1.rd_blk_x",
    "EventCode": "0x60",
    "BriefDescription": "All L2 Cache Requests (Breakdown 1 - Common). Data cache stores.",
    "UMask": "0x40"
  }, // Many more events here...
]

没有一个事件id为0x 60的单个事件会有一个掩码0xff,这使我假设掩码是多个一位掩码的AND组合。总的来说,这意味着缓存引用事件是所有事件id为0x 60的事件的总和。其中一些事件包括存储、共享访问甚至指令缓存度量。这回答了为什么引用计数在两个程序之间变化。
之后我决定寻找更适合实验的指标。由于这些评论,我决定使用L1-dcache-loads和L1-dcache-load-misses。这些产生的结果看起来更接近我的预期:

perf stat -e L1-dcache-loads,L1-dcache-load-misses,r0040,rc860 ./depth

 Performance counter stats for './depth':

     1 280 108 192      L1-dcache-loads:u                                                  
            14 834      L1-dcache-load-misses:u          #    0,00% of all L1-dcache accesses
     1 280 108 192      r0040:u                                                            
            14 834      rc860:u                                                            

       0,340287009 seconds time elapsed

       0,340152000 seconds user
       0,000000000 seconds sys

perf stat -e L1-dcache-loads,L1-dcache-load-misses,r0040,rc860 ./breadth

 Performance counter stats for './breadth':

     1 281 993 990      L1-dcache-loads:u                                                  
         1 635 879      L1-dcache-load-misses:u          #    0,13% of all L1-dcache accesses
     1 281 993 990      r0040:u                                                            
         1 635 879      rc860:u                                                            

       0,464484543 seconds time elapsed

       0,464455000 seconds user
       0,000000000 seconds sys

至于我在哪里找到了所述事件的硬件id,0x 0040和0xc 860的id在core.c的第131行找到:

[C(L1D)] = {
    [C(OP_READ)] = {
        [C(RESULT_ACCESS)] = 0x0040, /* Data Cache Accesses */
        [C(RESULT_MISS)]   = 0xc860, /* L2$ access from DC Miss */
    }
// Many more elements
}

0xc 860只包含事件id为0x 60的三个事件,其U掩码为0x 80、0x 40和0x 08。这基本上是0xff 60事件的较小子集。0x 0040在内核中也被命名为“ls_dc_accesses”,可以在memory.json的第87行找到。
虽然我对目前的结果很满意,我知道这两个事件并不完全是最准确的。0xc 860仍然包括一些写操作,0x 0040被描述为“推测”(我还不确定它在这个上下文中的含义)。总的来说,我可能会更多地使用各种度量,看看哪种度量能最准确地表示我想知道该高速缓存。尽管如此,这个问题主要是关于缓存引用的峰值,我决定我有足够的信息和来源来回答这个问题。

相关问题