在Skylake(SKL),为什么只读工作负载中的L2回写超过L3大小?

Bee*_*ope 14 performance x86 cpu-cache perf intel-pmu

请考虑以下简单代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

这将分配一个size字节数组(在命令行中传入,在KiB中),将所有字节设置为相同的值(memset调用),最后以只读方式循环遍历数组,跨越一个高速缓存行(64字节),并重复64次,以便每个字节被访问一次.

如果我们将预取关闭1,我们预计如果size适合缓存,则在给定级别的缓存中达到100%,否则大多数会错过该级别.

我感兴趣的两个事件l2_lines_out.silentl2_lines_out.non_silent(也是l2_trans.l2_wb-但价值最终一致non_silent),它会统计默默地从L2下降,不在行.

如果我们从16 KiB运行到1 GiB,并l2_lines_in.all仅为最终循环测量这两个事件(加号),我们得到:

L2线路输入/输出

这里的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上找到代码.有关构建和运行的详细信息,请参阅自述文件.

我在Skylake客户端i7-6700HQ CPU上观察到了这种情况.Haswell 2似乎不存在相同的效果.在Skylake-X上,正如预期的那样,行为完全不同,因为L3缓存设计已经变为类似L2的受害者缓存.


1您可以在最近的英特尔上执行此操作wrmsr -a 0x1a4 "$((2#1111))".事实上,图表几乎与prefetch on 完全相同,因此将其关闭主要是为了消除混淆因素.

2有关详细信息,请参阅注释,但在l2_lines_out.(non_)silent那里暂时不存在,但l2_lines_out.demand_(clean|dirty)似乎有类似的定义.更重要的是l2_trans.l2_wb,主要反映non_silent在Skylake上的东西也存在于Haswell上并且看起来很镜像demand_dirty,它也没有对Haswell产生影响.