21

考虑以下简单代码:

#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.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 上找到代码。有关构建和运行的详细信息可以在 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 上也没有表现出效果。

4

0 回答 0