有没有办法刷新与程序相关的整个CPU缓存?

Vin*_*ent 3 c++ memory optimization assembly cpu-cache

x86-64平台上,CLFLUSH汇编指令允许刷新对应于给定地址的缓存行.相反,冲洗与特定地址的缓存,会有一种通过使其充满了虚拟的内容,以刷新整个高速缓存(或者相关程序的缓存中执行,或整个高速缓存),例如(或任何我不会意识到的其他方法):

  • 仅使用标准C++ 17?
  • 如有必要,使用标准C++ 17和编译器内在函数?

以下函数的内容是什么:(无论编译器优化如何,该函数都应该工作)?

void flush_cache() 
{
    // Contents
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 10

有关清除缓存(尤其是在x86上)的相关问题的链接,请参阅有关WBINVD指令用法的第一个答案.


不,你不能用纯ISO C++ 17可靠或有效地做到这一点.它不知道或不关心CPU缓存.你能做的最好的事情就是触及大量的记忆,所以其他一切最终都会被驱逐1,但这不是你真正要求的.(当然,刷新所有缓存定义效率低下......)

CPU缓存管理函数/ intrinsics/asm指令是C++语言的特定于实现的扩展.但除了内联asm之外,我所知道的没有C或C++实现提供了一种刷新所有缓存的方法,而不是一系列地址.那是因为这不是正常的事情.


例如,在x86上,您正在寻找的asm指令是wbinvd. 它在驱逐之前写回任何脏线,不像invd(在没有回写丢弃缓存,在离开缓存为RAM模式时很有用).所以理论上wbinvd没有架构效应,只有微架构,但它太慢了,这是一个特权指令.由于英特尔的insn ref手动输入wbinvd指出,它会增加中断延迟,因为它本身不可中断,可能需要等待8 MiB或更多的脏L3缓存被刷新.即延迟那么长的中断可以被认为是一种架构效应,与大多数时序效应不同.在具有共享缓存的多核系统上,它也很复杂.

我认为没有办法在x86上的用户空间(环3)中使用它.与cli/ stiin/ 不同out,它不是由IO权限级别启用的(您可以通过iopl()系统调用在Linux上设置).因此wbinvd只有在实际在环0中运行时(即在内核代码中)才有效.请参阅特权指令和CPU环级别.

但是如果你在GNU C或C++中编写内核(或在ring0中运行的独立程序),你可以使用asm("wbinvd" ::: "memory");.在运行实际DOS的计算机上,正常程序在环0中运行,因此这将是运行需要在ring0中运行的微基准测试的另一种方式(以避免内核< - >用户空间转换开销wbinvd),而且还具有运行的便利性在操作系统下,您可以使用文件系统.但是,将微基准标记放入Linux内核模块可能比从USB记忆棒或其他东西启动FreeDOS更容易.


我可以想到你可能想要的唯一原因是进行某种实验来弄清楚如何设计特定CPU的内部结构.因此,具体如何完成的细节至关重要.即使我想要一种便携/通用的方式来做这件事也没有意义.

或者在重新配置物理内存布局之前可能在内核中,例如,现在有一个以太网卡的MMIO区域,其中曾经是普通的DRAM.但在这种情况下,您的代码已经完全针对特定于Arch.

通常,当您希望/需要为正确原因刷新缓存时,您知道哪个地址范围需要刷新.例如,在具有非缓存一致性的DMA的体系结构上编写驱动程序时,因此在DMA读取之前发生回写,并且不执行DMA写入.(并且驱逐部分对DMA读取也很重要:您不需要旧的缓存值).但是现在x86具有缓存一致性DMA,因为现代设计将内存控制器构建到核心中,因此系统流量可以在从PCIe到内存的途中窥探L3.

您需要担心缓存的驱动程序之外的主要情况是在具有非连贯指令缓存的体系结构上生成JIT代码.如果您(或JIT库)将一些机器代码写入char[]缓冲区并将其转换为函数指针,那么像ARM这样的架构并不能保证代码获取会"看到"新写入的数据.

这就是gcc提供的原因__builtin__clear_cache.它不一定会刷新任何东西,只能确保将该内存作为代码执行是安全的.x86具有与数据高速缓存一致的指令高速缓存,甚至支持自修改代码而无需任何特殊的同步指令.请参阅godbolt for x86和AArch64,并注意这__builtin__clear_cache实际上并不是x86的无操作:没有它,gcc会在转换为函数指针并调用之前将存储优化到缓冲区.(它没有意识到数据被用作代码,因此它认为它们是死存储并消除它们.)

尽管这个名字, __builtin__clear_cache完全无关wbinvd.它需要一个地址范围作为args,它编译为x86上的任何指令,它只会使用clflush,或者clflushopt,或者clwb.

无论如何,当您需要刷新一些缓存以获得正确性时,您只需要刷新一系列地址,而不是通过刷新所有缓存来降低系统速度.

由于性能原因故意冲洗缓存很少有意义,至少在x86上.有时您可以使用污染最小化预取来读取数据而不会有太多的缓存污染,或者使用NT存储来写入缓存.但做正常的事情,然后clflushopt在最后一次触摸一些记忆后,在正常情况下是不值得的.就像商店一样,它必须一直通过内存层次结构,以确保它在任何地方找到并刷新该行的任何副本.没有一个轻量级的指令是出于性能原因而设计的,就像它的相反_mm_prefetch.


您可以在x86上的用户空间中执行的唯一缓存刷新(冲突驱逐除外)是clflush/ clflushopt.(或者使用NT存储,如果手头很热,它也会逐出高速缓存行).

有一个英特尔固有[ _mm_clflush(void const *p)][6]包装器clflush(和另一个clflushopt),但这些只能通过(虚拟)地址刷新缓存行.你可以遍历你的进程映射的所有页面中的所有缓存行...(但是这只能刷新你自己的内存,而不是缓存内核数据的缓存行,比如你的进程或它的内核堆栈task_struct,所以第一次系统调用仍然会比你刷新所有内容更快.

有一个Linux系统调用包装器可以移植一系列地址:cacheflush(char *addr, int nbytes, int flags).假设x86上的实现使用clflushclflushopt在循环中,如果在x86上完全支持它.该手册页首次出现在MIPS Linux中"但是现在,Linux在其他一些架构上提供了一个cacheflush()系统调用,但是有不同的参数."

我认为没有公开的Linux系统调用wbinvd,但你可以编写一个添加一个的内核模块.


最近的x86扩展引入了更多缓存控制指令,但仍然只能通过地址来控制特定的缓存行.用例适用于直接连接到CPU的非易失性存储器,例如Intel Optane DC Persistent Memory.如果要在不使下一次读取缓慢的情况下提交持久存储,则可以使用clwb.但请注意,clwb不能保证避免驱逐,它只是被允许.它可能会clflushoptSKX上的情况一样运行.

请参阅https://danluu.com/clwb-pcommit/,但请注意,这pcommit不是必需的:英特尔决定在发布任何需要它的芯片之前简化ISA,因此clwb或者clflushopt+ sfence就足够了.请参阅https://software.intel.com/en-us/blogs/2016/09/12/deprecate-pcommit-instruction.

无论如何,这是一种与现代CPU相关的缓存控制.无论你做什么实验,都需要在x86上进行ring0和汇编.


脚注1:触及大量内存:纯ISO C++ 17

可以分配一个非常大的缓冲区然后memset它(因此这些写入将污染所有(数据)缓存与该数据),然后取消映射它.如果delete或者free实际上将内存立即返回给操作系统,那么它将不再是进程地址空间的一部分,因此只有少数其他数据的高速缓存行仍然很热:可能是一行或两行堆栈(假设你'重温使用堆栈的C++实现,以及在OS下运行程序......).当然,这只会污染数据缓存,而不是指令缓存,而Basile指出,某些级别的缓存是每个核心的私有,并且操作系统可以在CPU之间迁移进程.

此外,请注意使用实际memsetstd::fill函数调用,或优化的循环,可以优化使用缓存绕过或减少污染的存储.我还隐含地假设您的代码在具有写分配缓存的CPU上运行,而不是在存储未命中时写入(因为所有现代CPU都是以这种方式设计的).

做一些无法优化并触及大量内存的东西(例如带有long阵列而不是位图的主筛)会更可靠,但当然仍然依赖于缓存污染来驱逐其他数据.只读大量数据也不可靠; 一些CPU实现了自适应替换策略,可以减少顺序访问造成的污染,因此循环遍历大型阵列有望不会驱逐大量有用的数据.例如英特尔IvyBridge中的L3缓存,后来就这样做了.