Linux 嗯,clflush真的会刷新缓存吗?
我试图通过编写和运行测试程序来了解硬件缓存的工作原理:Linux 嗯,clflush真的会刷新缓存吗?,linux,x86-64,cpu-architecture,cpu-cache,cache-locality,Linux,X86 64,Cpu Architecture,Cpu Cache,Cache Locality,我试图通过编写和运行测试程序来了解硬件缓存的工作原理: #include <stdio.h> #include <stdint.h> #include <x86intrin.h> #define LINE_SIZE 64 #define L1_WAYS 8 #define L1_SETS 64 #define L1_LINES 512 // 32K memory for filling in L1 cache uint8_t d
#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#define LINE_SIZE 64
#define L1_WAYS 8
#define L1_SETS 64
#define L1_LINES 512
// 32K memory for filling in L1 cache
uint8_t data[L1_LINES*LINE_SIZE];
int main()
{
volatile uint8_t *addr;
register uint64_t i;
int junk = 0;
register uint64_t t1, t2;
printf("data: %p\n", data);
//_mm_clflush(data);
printf("accessing 16 bytes in a cache line:\n");
for (i = 0; i < 16; i++) {
t1 = __rdtscp(&junk);
addr = &data[i];
junk = *addr;
t2 = __rdtscp(&junk) - t1;
printf("i = %2d, cycles: %ld\n", i, t2);
}
}
不带\u mm\u clflush
:
$ ./l1
data: 0x700c00
accessing 16 bytes in a cache line:
i = 0, cycles: 280
i = 1, cycles: 84
i = 2, cycles: 91
i = 3, cycles: 77
i = 4, cycles: 91
$ ./l1
data: 0x700c00
accessing 16 bytes in a cache line:
i = 0, cycles: 3899
i = 1, cycles: 91
i = 2, cycles: 105
i = 3, cycles: 77
i = 4, cycles: 84
刷新缓存线是没有意义的,但实际上会更快吗?有人能解释为什么会发生这种情况吗?谢谢
----------------进一步试验-------------------
假设3899次循环是由TLB未命中引起的。为了证明我对缓存命中/未命中的了解,我稍微修改了这段代码,以比较L1缓存命中
和L1缓存未命中
情况下的内存访问时间
这一次,代码跳过缓存线大小(64字节)并访问下一个内存地址
*data = 1;
_mm_clflush(data);
printf("accessing 16 bytes in a cache line:\n");
for (i = 0; i < 16; i++) {
t1 = __rdtscp(&junk);
addr = &data[i];
junk = *addr;
t2 = __rdtscp(&junk) - t1;
printf("i = %2d, cycles: %ld\n", i, t2);
}
// Invalidate and flush the cache line that contains p from all levels of the cache hierarchy.
_mm_clflush(data);
printf("accessing 16 bytes in different cache lines:\n");
for (i = 0; i < 16; i++) {
t1 = __rdtscp(&junk);
addr = &data[i*LINE_SIZE];
junk = *addr;
t2 = __rdtscp(&junk) - t1;
printf("i = %2d, cycles: %ld\n", i, t2);
}
这是由预取引起的吗?还是我的理解有什么问题?谢谢我想这可能是因为一开始TLB未命中造成的?而且_mm_clflush实际上将这个虚拟地址缓存到TLB中,我可能是对的吗?如何证明它?我修改了代码,在
\u mm\u clflush(data)
之前添加了一个write,它显示clflush确实刷新了缓存线。修改后的代码:
#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#define LINE_SIZE 64
#define L1_LINES 512
// 32K memory for filling in L1 cache
uint8_t data[L1_LINES*LINE_SIZE];
int main()
{
volatile uint8_t *addr;
register uint64_t i;
unsigned int junk = 0;
register uint64_t t1, t2;
data[0] = 1; //write before cflush
//_mm_clflush(data);
printf("accessing 16 bytes in a cache line:\n");
for (i = 0; i < 16; i++) {
t1 = __rdtscp(&junk);
addr = &data[i];
junk = *addr;
t2 = __rdtscp(&junk) - t1;
printf("i = %2d, cycles: %ld\n", i, t2);
}
}
使用clflush:
data: 0000000000407980
accessing 16 bytes in a cache line:
i = 0, cycles: 214
i = 1, cycles: 41
i = 2, cycles: 40
i = 3, cycles: 42
i = 4, cycles: 40
如果没有
clflush
,第一次加载大约需要3899个周期,这大约是处理小页面错误所需的时间rdtscp
序列化加载操作,从而确保所有稍后加载到一级缓存中的同一行。现在,当您在循环之前添加clflush
时,页面错误将在循环外部触发和处理。当页面错误处理程序返回并且重新执行clflush
时,目标缓存线将被刷新。在英特尔处理器上,rdtscp
确保在发出循环中的第一次加载之前刷新该行。因此,现金层次结构中的第一次加载未命中,其延迟大约是内存访问的延迟。与前一种情况一样,后面的加载由rdtscp
序列化,因此它们都在L1D中命中
测量的L1D命中潜伏期太高,即使我们考虑了<代码> RDTSCP < /代码>的开销。你是用
-O3
编译的吗
当静态分配缓存线时,我无法在Linux 4.4.0-154上使用gcc 5.5.0再现您的结果(即,小页面错误),但仅当我使用mmap
时。如果你告诉我你的编译器版本和内核版本,也许我可以进一步调查
关于第二个问题,您测量负载延迟的方式无法区分L1D命中和L2命中,因为测量中的错误可能与延迟的差异一样大。您可以使用
MEM\u LOAD\u UOPS\u RETIRED.L1\u HIT
和MEM\u LOAD\u UOPS\u RETIRED.L2\u HIT
性能计数器进行检查。一级和二级硬件预取器很容易检测到顺序访问模式,因此,如果不关闭预取器,则获得命中率并不奇怪。使用rdtsc(甚至rdtscp风格)无法测量几个周期的粒度,您可能对性能的影响大于负载本身。您需要在多行上摊销。是的,clflush
会在任何缓存中刷新缓存线。有关测量缓存命中与L3未命中延迟的工作程序,请参阅。或者,您的意思是,由于rdtscp函数调用使用的周期,此测量不准确?实际上,我正在研究缓存侧通道。我想这可能是区分一级命中、二级命中和缓存未命中的明显结果。谢谢,@PeterCordes。我更新了我的问题,以了解缓存线命中和缓存线未命中之间的区别。只是有点难以解释结果。您能回答这个问题吗?有关缓存读取端通道,请参阅:。如果我稍后再讨论,我会更详细地阅读整个问题。我现在没有时间。是的,如果需要找到正确的物理地址,clflush将导致页面漫游。它不像预取那样是一种暗示;CPU不允许像某些用于软件预取的旧CPU那样在TLB未命中时将其丢弃。您可以使用alignas(64)volatile uint8\t data[64]
或其他什么;您不需要一个单独的指向volatile的指针。此外,缓存命中的~47和~41个周期(加上rdtscp开销)可能是由于没有将CPU预热到max turbo。无论初始访问是否丢失,缓存命中率应该大致相同。而你printf
在循环之前的存储之后,这样存储转发就不会发生。
data: 0000000000407980
accessing 16 bytes in a cache line:
i = 0, cycles: 64
i = 1, cycles: 46
i = 2, cycles: 49
i = 3, cycles: 48
i = 4, cycles: 46
data: 0000000000407980
accessing 16 bytes in a cache line:
i = 0, cycles: 214
i = 1, cycles: 41
i = 2, cycles: 40
i = 3, cycles: 42
i = 4, cycles: 40