我偶然发现了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节,以找到底层硬件事件的描述。我仍然希望您能对该问题给出明智的回答。
1条答案
按热度按时间zdwk9cvp1#
在我有更多的时间来研究这个主题之后,我自己回答了我的问题。感谢@Ladislus和@Peter Cordes的评论,因为他们的提示把我带到了正确的方向。答案会有点长,因为这个问题是为了解释一个现象,而不是解决问题,我想给予一个很好的解释发生了什么。
开始,我目前正在测试的CPU是AMD 4650 U移动的APU。
/proc/cpuinfo
的摘录澄清了一些重要的细节:为了记录在案,这可能是一个不同的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
在“原始硬件事件描述符”部分所暗示的那样:可悲的是,我发现PPR是很难导航,不能真正解码任何信息。
最后,我发现Linux内核源代码中使用最多的是
/arch/x86/events/amd/core.c
文件(mirror)包含从事件到事件ID的Map,以及十六进制的Umask,而/tools/perf/pmu-events/arch/x86/amdzen2/
中的文件(特别是core.json,cache.json和memory.json)列出了所有的事件。选择目录amdzen 2是因为我的CPU的微拱是zen 2,但是也可以通过检查mapfile.csv来找到它,该mapfile.csv基于来自/proc/cpuinfo
和正则表达式的信息(看起来像什么)与特定目录匹配。对于事件,它们被编码为4位十六进制数。根据我收集的信息,前2位是UMask,而第二对是Event id。在core.c中,我找到了从Linux事件名称到硬件事件id的Map(对于系列17 h):
看起来缓存引用Map到id 0xff 60,缓存未命中Map到0x 0964。这可以通过运行
perf stat
来验证,同时使用事件的友好名称和硬件id,计数是相同的:这也解释了为什么深度优先搜索和广度优先搜索之间的引用计数会增加。正如注解中所猜测的,cache-references事件包括该高速缓存的读写,以及一些其他指标。具体来说,cache.json以事件ID为0x 60的事件列表开始。每个事件都有一个单位UMask:
没有一个事件id为0x 60的单个事件会有一个掩码0xff,这使我假设掩码是多个一位掩码的AND组合。总的来说,这意味着缓存引用事件是所有事件id为0x 60的事件的总和。其中一些事件包括存储、共享访问甚至指令缓存度量。这回答了为什么引用计数在两个程序之间变化。
之后我决定寻找更适合实验的指标。由于这些评论,我决定使用L1-dcache-loads和L1-dcache-load-misses。这些产生的结果看起来更接近我的预期:
至于我在哪里找到了所述事件的硬件id,0x 0040和0xc 860的id在core.c的第131行找到:
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被描述为“推测”(我还不确定它在这个上下文中的含义)。总的来说,我可能会更多地使用各种度量,看看哪种度量能最准确地表示我想知道该高速缓存。尽管如此,这个问题主要是关于缓存引用的峰值,我决定我有足够的信息和来源来回答这个问题。