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

Performance 在Skylake(SKL)上,为什么在超过L3大小的只读工作负载中存在L2写回?,performance,x86,cpu-cache,perf,intel-pmu,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 (arg

考虑以下简单代码:

#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;
}
#包括
#包括
#包括
#包括
#包括
int cpu_ms(){
返回(int)(时钟()*1000/时钟/秒);
}
int main(int argc,字符**argv){
如果(argc<2)errx(EXIT_FAILURE,“在命令行上提供以KB为单位的数组大小”);
大小=原子(argv[1])*1024;
无符号字符*p=malloc(大小);
如果(!p)errx(退出_失败,%zu字节的malloc失败),则为size;
int fill=argv[2]?argv[2][0]:'x';
膜组(p、填充、大小);
int startms=cpu_ms();
printf(“在%p处分配了%zu个字节,并在%d ms\n中将其设置为%d”,大小,p,填充,起始TMS);
//等待500毫秒,以便perf获得读取阶段
而(cpu_ms()-startms<500){}
startms=cpu_ms();
//我们从这里开始测量perf
无符号字符和=0;
用于(大小\u t off=0;off<64;off++){
对于(大小i=0;i
这将分配一个
size
字节数组(在命令行上以KiB形式传入),将所有字节设置为相同的值(调用
memset
),最后以只读方式在数组上循环,跨过一条缓存线(64字节),并重复64次,以便每个字节访问一次

如果我们关闭预取1,如果
size
适合缓存,则在给定的缓存级别上,预取将达到100%,否则在该级别上将大部分丢失

我对两个事件感兴趣
l2\u line\u out.silent
l2\u line\u out.non\u silent
(以及
l2\u trans.l2\u wb
)但值最终与
non\u silent
)相同,这两个事件统计的是从l2悄悄删除的行,而不是

如果我们从16千兆位运行到1千兆位,并仅为最终循环测量这两个事件(加上
l2\u line\u in.all
),我们得到:

这里的y轴是事件数,标准化为循环中的访问数。例如,16kib测试分配16kib区域,并对该区域进行16384次访问,因此值为0.5表示每次访问平均发生0.5次给定事件

所有
中的
l2\u行的行为几乎与我们预期的一样。它大约从零开始,当大小超过L2大小时,它上升到1.0并保持不变:每次访问都会带来一条线

另外两条线的表现很奇怪。在测试适用于L3(但不适用于L2)的区域,逐出几乎都是无声的。然而,一旦该区域移动到主内存中,逐出操作都是非静默的

是什么解释了这种行为?很难理解为什么从L2中逐出取决于底层区域是否适合主内存

如果执行存储而不是加载,则几乎所有内容都是预期的非静默写回,因为更新值必须传播到外部缓存:

我们还可以使用
mem_inst_retired.l1_hit
和相关事件查看访问命中的缓存级别:

如果忽略L1命中计数器,它在几个点(每个访问超过1次L1命中?)处似乎高得不可思议,那么结果看起来或多或少与预期一致:当区域完全适合L2时,大部分是L2命中,而L3区域(我的CPU上最多有6个MiB)的大部分是L3命中,然后错过DRAM

你可以找到代码。有关构建和运行的详细信息可以在自述文件中找到

我在Skylake客户端i7-6700HQ CPU上观察到这种行为。哈斯韦尔2号似乎不存在同样的效果。在Skylake-X上,正如预期的那样,这种行为是因为三级缓存设计已更改为类似于二级缓存的受害者缓存


1您可以在最近的英特尔上使用
wrmsr-a 0x1a4“$((2#1111))”
来实现这一点。事实上,打开预取时,图形几乎完全相同,因此关闭它主要是为了消除混淆因素


2有关更多详细信息,请参阅,但这里不存在简单的
l2\u行输出。(非)无声的
,但
l2\u行输出。demand\u(clean | dirty)
有类似的定义。更重要的是,Haswell上也存在主要反映Skylake上非静音的
l2_trans.l2_wb
,它似乎反映了Haswell上的
demand_dirty
,也没有表现出Haswell的效果。

,Haswell上以这些名称存在着
L2_线_出。DEMAND_CLEAN
L2_线_出。DEMAND_DIRTY
。只是UMASK不同。在Haswell上,
l2_trans.l2_wb
l2_line\u out。对于所有数组大小,非静音的
几乎都是零
l2\u lines\u out。当阵列大于l2时,静音
变为平坦,每次访问约0.8次<代码>第二行。所有的
都是预期的。@BeeOnRope:您问题中的数据是来自Skylake客户端(包括L3)还是Skylake SP(L3)?如果二级驱逐在三级缓存中还不是很热的话,那么干净的二级驱逐会像受害者缓存一样写回三级缓存吗?@PeterCordes-Skylake client(i7-6700HQ)。@HadiBrais-对不起,你是对的,这些事件在Haswell上不存在。我检查并复制了您的结果:如果您使用demand_dirty和demand_clean代替non_silent和silent,Haswell不会显示相同的效果。因此,要么事件的计数有所不同(即,这不仅仅是一个名称的改变),要么Skylake的行为有所不同。在Haswell上,l2_trans.l2_wb事件确实有相同的名称,但不以Skylake的方式运行(它只比demand_dirty略高,比clean低得多),这是事实上“Skylake不同”的一些证据。