考虑以下简单代码:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <err.h>
int cpu_ms() {
return (int)(clock() * 1000 / CLOCKS_PER_SEC);
}
int main(int argc, char** argv) {
if (argc < 2) errx(EXIT_FAILURE, "provide the array size in KB on the command line");
size_t size = atol(argv[1]) * 1024;
unsigned char *p = malloc(size);
if (!p) errx(EXIT_FAILURE, "malloc of %zu bytes failed", size);
int fill = argv[2] ? argv[2][0] : 'x';
memset(p, fill, size);
int startms = cpu_ms();
printf("allocated %zu bytes at %p and set it to %d in %d ms\n", size, p, fill, startms);
// wait until 500ms has elapsed from start, so that perf gets the read phase
while (cpu_ms() - startms < 500) {}
startms = cpu_ms();
// we start measuring with perf here
unsigned char sum = 0;
for (size_t off = 0; off < 64; off++) {
for (size_t i = 0; i < size; i += 64) {
sum += p[i + off];
}
}
int delta = cpu_ms() - startms;
printf("sum was %u in %d ms \n", sum, delta);
return EXIT_SUCCESS;
}
这会分配一个字节数组size
(在命令行中传入,以 KiB 为单位),将所有字节设置为相同的值(memset
调用),最后以只读方式循环遍历数组,跨越一个缓存行(64 字节),并重复这 64 次,以便每个字节被访问一次。
如果我们关闭预取1size
,如果适合缓存,我们希望它在给定级别的缓存中达到 100%,否则在该级别大部分都未命中。
我对两个事件l2_lines_out.silent
和l2_lines_out.non_silent
(以及l2_trans.l2_wb
- 但值最终与 相同non_silent
)感兴趣,它计算从 l2 静默删除的行,而不是。
如果我们从 16 KiB 到 1 GiB 运行它,并l2_lines_in.all
仅在最后一个循环中测量这两个事件(加号),我们得到:
这里的 y 轴是事件的数量,归一化为循环中的访问次数。例如,16 KiB 测试分配了一个 16 KiB 区域,并对该区域进行了 16,384 次访问,因此值 0.5 意味着每次访问平均发生 0.5 个给定事件计数。
的l2_lines_in.all
行为几乎与我们预期的一样。它从零开始,当大小超过 L2 大小时,它会上升到 1.0 并保持在那里:每次访问都会产生一行。
另外两条线的行为很奇怪。在测试适合 L3 的区域(但不在 L2 中),驱逐几乎都是无声的。但是,一旦该区域移入主内存,驱逐都是非静默的。
什么解释了这种行为?很难理解为什么L2的驱逐取决于底层区域是否适合主内存。
如果您执行存储而不是加载,几乎所有内容都是预期的非静默写回,因为更新值必须传播到外部缓存:
mem_inst_retired.l1_hit
我们还可以使用和 相关事件查看访问所访问的缓存级别:
如果您忽略 L1 命中计数器,它在几个点上看起来高得不可思议(每次访问超过 1 个 L1 命中?),结果看起来或多或少符合预期:当该区域完全适合 L2 时,主要是 L2 命中,大多数情况下L3 命中 L3 区域(在我的 CPU 上高达 6 MiB),然后错过了 DRAM。
你可以在 GitHub 上找到代码。有关构建和运行的详细信息可以在 README 文件中找到。
我在我的 Skylake 客户端 i7-6700HQ CPU 上观察到了这种行为。Haswell 2上似乎不存在相同的效果。在 Skylake-X 上,行为完全不同,正如预期的那样,因为 L3 缓存设计已更改为类似于 L2 的受害者缓存。
1您可以在最近的 Intel 上使用wrmsr -a 0x1a4 "$((2#1111))"
. 事实上,预取打开时的图表几乎完全相同,所以关闭它主要是为了消除一个混淆因素。
2有关更多详细信息,请参阅评论l2_lines_out.(non_)silent
,但那里暂时不存在,但l2_lines_out.demand_(clean|dirty)
似乎有类似的定义。更重要的是,在 Skylake 上l2_trans.l2_wb
大部分镜像non_silent
的也存在于 Haswell 上,并且似乎是镜像demand_dirty
,并且在 Haswell 上也没有表现出效果。