【发布时间】:2021-09-09 07:22:25
【问题描述】:
我正在尝试强制用户应用程序从所有级别的缓存中刷新所有保存数组(由它自己创建)的缓存行。
在阅读了这篇文章 (Cflush to invalidate cache line via C function) 并得到了 @PeterCordes 的大力指导后,我尝试用 C 语言编写一个函数来完成此任务。
#include <x86intrin.h>
#include <stdint.h>
inline void flush_cache_range(uint64_t *ptr, size_t len){
size_t i;
// prevent any load or store to be scheduled across
// this point due to CPU Out of Order execution.
_mm_mfence();
for(i=0; i<len; i++)
// flush the cache line that contains ptr+i from
// all cache levels
_mm_clflushopt(ptr+i);
_mm_mfence();
}
int main(){
size_t plen = 131072; // or much bigger
uint64_t *p = calloc(plen,sizeof(uint64_t));
for(size_t i=0; i<plen; i++){
p[i] = i;
}
flush_cache_range(p,plen);
// at this point, accessing any element of p should
// cause a cache miss. As I access them, adjacent
// elements and perhaps pre-fetched ones will come
// along.
(...)
return 0;
}
我在运行内核 5.11.14 (Fedora 33) 的 AMD Zen2 处理器中使用 gcc -O3 -march=native source.c -o exec.bin 进行编译。
我不完全理解mfence/sfence/lfence 之间的区别,或者当一个或另一个足够时,所以我只使用了mfence,因为我相信它施加了最强的限制(我对吧?)。
我的问题是:我是否缺少此实现的某些内容?它会像我想象的那样做吗? (我的想象是在调用flush_cache_range函数后的评论中)
谢谢。
编辑 1:每行刷新一次,并移除栅栏。
在@PeterCordes 的回答之后,我正在做一些调整:
-
首先,该函数接收一个指向 char 的指针及其以 char 为单位的大小,因为它们是 1 个字节长,所以我可以控制从一个刷新到下一个刷新的跳转量。
-
然后,我需要确认缓存行的大小。我可以使用程序
cpuid:cpuid -1 | grep -A12 -e "--- cache [0-9] ---"
获取该信息 对于 L1i、L1d、L2 和 L3,我得到line size in bytes = 0x40 (64)所以这是每次刷新后我必须跳过的字节数。 -
然后我通过添加
ptr + len - 1来确定指向最后一个字符的指针。 -
然后遍历所有地址,每个缓存行一个,包括最后一个 (
ptr_end)。
这是代码的更新版本:
#include <stdio.h>
#include <x86intrin.h>
#include <stdint.h>
inline void flush_cache_range(char *ptr, size_t len);
void flush_cache_range(char *ptr, size_t len){
const unsigned char cacheline = 64;
char *ptr_end = ptr + len - 1;
while(ptr <= ptr_end){
_mm_clflushopt(ptr);
ptr += cacheline;
}
}
int main(){
size_t i, sum=0, plen = 131072000; // or much bigger
uint64_t *p = calloc(plen,sizeof(uint64_t));
for(i=0; i<plen; i++){
p[i] = i;
}
flush_cache_range((char*)p, sizeof(p[0])*plen);
// there should be many cache misses now
for(i=0; i<plen; i++){
sum += p[i];
}
printf("sum is:%lu\n", sum);
return 0;
}
现在当我编译并运行perf时:
gcc -O3 -march=native src/source.c -o exec.bin && perf stat -e cache-misses,cache-references ./exec.bin
我明白了:
sum is:8589934526464000
Performance counter stats for './exec.bin':
1,202,714 cache-misses:u # 1.570 % of all cache refs
76,612,476 cache-references:u
0.377100534 seconds time elapsed
0.170473000 seconds user
0.205574000 seconds sys
如果我评论调用flush_cache_range 的行,我得到的几乎相同:
sum is:8589934526464000
Performance counter stats for './exec.bin':
1,211,462 cache-misses:u # 1.590 % of all cache refs
76,202,685 cache-references:u
0.356544645 seconds time elapsed
0.160227000 seconds user
0.195305000 seconds sys
我错过了什么?
编辑 2:添加sfence,并修复循环限制
- 我按照@prl 的建议添加了 sfence
- 将 ptr_end 更改为指向其缓存行的最后一个字节。
void flush_cache_range(char *ptr, size_t len){
const unsigned char cacheline = 64;
char *ptr_end = (char*)(((size_t)ptr + len - 1) | (cacheline - 1));
while(ptr <= ptr_end){
_mm_clflushopt(ptr);
ptr += cacheline;
}
_mm_sfence();
}
我在性能方面仍然得到同样的意外结果。
【问题讨论】:
-
您的编辑看起来应该是答案,而不是问题的一部分。
-
嗨@PeterCordes。这些版本并没有解决问题,而是提供了更多的上下文以及我认为存在问题的原因。这就是为什么我不认为它们是答案。
-
您的编译命令中的
-o3是不是拼写错误?应该是-O3,大写O。 -
// there should be many cache misses now- 你没有做任何事情来破坏循环内的硬件预取,或者来自同一行的所有 4 个向量加载都一起去。 IDK 如果 Zen2 有足够的带宽用于硬件预取以跟上每个时钟周期 16 个字节(或者如果 GCC 的循环不是 5 微秒或更少,则每多于 1 个字节),但如果是这样,那可以解释它。使用-march=native可能会有所帮助(对于 AVX2)和-funroll-all-loops。或者使用clang-O3 -march=native;它可能使用多个向量累加器来隐藏 1/clockvpaddq延迟。
标签: c x86 intrinsics cpu-cache memory-barriers