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

Is there a way to flush the entire CPU cache related to a program?

x86-64 平台上,CLFLUSH 汇编指令允许刷新与给定地址对应的缓存行。除了刷新与特定地址相关的缓存,是否有一种方法可以刷新整个缓存(与正在执行的程序相关的缓存,或整个缓存),例如使它充满虚拟内容(或任何我不知道的其他方法):

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

void flush_cache() 
{
    // Contents
}

答案是,没有标准的 C++ 方法可以做到这一点(即使使用一些编译器内部函数)。 GCC has __builtin__clear_cache and __builtin_prefetch and Clang 可能也有。

正如 Johan 评论的那样,x86-64 有一个特权指令可以做你想做的事,但 __builtin__clear_cache 不使用它(并且在 x86-64 上是一个空操作,因为指令缓存是连贯的在该架构上使用数据缓存,因此硬件会在将其作为代码执行之前负责同步最近存储的数据。

在 Linux 上,您可能(也许)使用 cacheflush(2) Linux 特定系统调用。没用过,不知道x86-64上有没有实现。


顺便说一句,你不应该在程序上推理,而应该在 processes. Each has its own virtual address space.

上推理

你的问题缺乏一些动机。如果您关心微基准测试,请注意内核调度程序可以在任意机器代码指令下重新安排您的线程或进程并将其移动到其他内核(但是请注意 processor affinity)。

(the function should work regardless of compiler optimizations)?

没有,optimizing compilers are reordering and rescheduling machine code instructions and often mix several computations related to different C++ statements. They are allowed to do some computations at compile-time. Read more about the as-if rule. See CppCon 2017 talk: Matt Godbolt “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”

有关清除缓存(尤其是在 x86 上)的相关问题的链接,请参阅 WBINVD instruction usage 上的第一个答案。


不,您无法使用纯 ISO C++17 可靠或高效地执行此操作。它不知道也不关心 CPU 缓存。你能做的最好的事情就是接触大量内存,这样其他所有东西最终都会被驱逐1,但这并不是你真正想要的。 (当然,刷新 all 缓存根据定义是低效的...)

CPU 缓存管理函数/内在函数/asm 指令是对 C++ 语言的特定实现扩展。但除了内联 asm 之外,据我所知,没有任何 C 或 C++ 实现提供刷新 all 缓存的方法,而不是地址范围。那是因为这不是一件正常的事情。


例如,在 x86 上,您要查找的 asm 指令是 wbinvd 它会在逐出之前写回所有脏行,这与 [=11 不同=](在没有回写的情况下丢弃缓存). So in theory wbinvd has no architectural effect, only microarchitectural, but it's so slow that's it's a privileged instruction. As Intel's insn ref manual entry for wbinvd指出,它会增加中断延迟,因为它本身不是可中断的,可能必须等待8 MiB或更多的脏 L3 缓存被刷新。即延迟那么长时间的中断可以被认为是一种架构效果,与大多数时序效果不同。它在多核系统上也很复杂,因为它必须为 all[ 刷新缓存=142=] 核心。

我不认为有任何方法可以在 x86 上的 user-space (ring 3) 中使用它。与 cli / stiin/out 不同,它不是由 IO 权限级别启用的(您可以在 Linux 上使用 iopl() system call). So wbinvd only works when actually running in ring 0 (i.e. in kernel code). See Privileged Instructions and CPU Ring Levels.

但是,如果您正在使用 GNU C 或 C++ 编写内核(或 运行 在 ring0 中的独立程序),则可以使用 asm("wbinvd" ::: "memory");。在计算机 运行ning 实际 DOS 上,正常程序 运行 在实模式下(没有任何较低的特权级别;一切都是有效的内核)。这将是 运行 需要 运行 特权指令以避免 wbinvd 的内核<->用户space 转换开销的微基准测试的另一种方式,并且还具有便利性运行ning 在 OS 下,因此您可以使用文件系统。不过,将你的微基准测试放入 Linux 内核模块可能比从 USB 记忆棒或其他东西引导 FreeDOS 更容易。特别是如果你想要控制涡轮频率的东西。


我能想到您可能想要这个的唯一原因是为了某种实验来弄清楚特定 CPU 的内部结构是如何设计的。因此,具体如何完成的细节至关重要。我什至想要一种便携/通用的方式来做这件事对我来说没有意义。

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


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

在驱动程序之外,您需要担心缓存的主要情况是在非 x86 架构上使用非一致性指令缓存生成 JIT 代码。如果您(或 JIT 库)将一些机器代码写入 char[] 缓冲区并将其转换为函数指针,像 ARM 这样的架构不保证代码获取将“看到”新写入的数据。

这就是 gcc 提供 __builtin__clear_cache. It doesn't necessarily flush anything, only makes sure it's safe to execute that memory as code. x86 has instruction caches that are coherent with data caches and supports self-modifying code without any special syncing instructions. See godbolt for x86 and AArch64 的原因,请注意 __builtin__clear_cache 编译为 x86 的零指令,但对周围代码有影响:没有它,gcc 可以优化存储到缓冲区在转换为函数指针并调用之前。 (它没有意识到数据被用作代码,所以它认为它们是死存储并消除了它们。)

尽管名称如此,__builtin__clear_cachewbinvd 完全无关。它需要一个地址范围作为参数,因此它不会刷新整个缓存并使其无效。它也不使用 clflushclflushoptclwb 从缓存中实际写回(并可选地逐出)数据。

当您需要刷新某些缓存以确保正确性时,您只想刷新一个地址范围,不会通过刷新所有缓存来减慢系统速度。


出于性能原因故意刷新缓存很少有意义,至少在 x86 上是这样。有时您可以使用污染最小化预取来读取数据而不会造成太多缓存污染,或者使用 NT 存储来绕过缓存写入。但是在最后一次接触一些内存之后做“正常”的事情然后 clflushopt 在正常情况下通常是不值得的。就像商店一样,它必须一直遍历内存层次结构,以确保它在任何地方找到并刷新该行的任何副本。

没有像_mm_prefetch相反的性能提示设计的轻量级指令。


您可以在 x86 上的 user-space 中执行的唯一缓存刷新是使用 clflush / clflushopt。 (或者对于 NT 商店,如果它之前很热,它也会驱逐缓存行)。或者当然为已知的 L1d 大小和关联性创建冲突驱逐,比如以 4kiB 的倍数写入多行,它们都映射到 32k / 8 路 L1d 中的同一组。

有一个 Intel intrinsic _mm_clflush(void const *p) wrapper for clflush (and another for clflushopt),但它们只能通过(虚拟)地址刷新缓存行。您可以遍历进程映射的所有页面中的所有缓存行...(但这只能刷新您自己的内存,而不是缓存内核数据的缓存行,例如进程的内核堆栈或其 task_struct, 所以第一个系统调用仍然比你刷新所有东西要快)。

有一个 Linux 系统调用包装器可移植地逐出地址范围:cacheflush(char *addr, int nbytes, int flags)。假设 x86 上的实现在循环中使用 clflushclflushopt,如果它在 x86 上完全受支持的话。手册页说它首先出现在 MIPS Linux “但是 现在,Linux 提供了一个 cacheflush() 系统调用 体系结构,但有不同的论据。"

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


最近的 x86 扩展引入了更多的缓存控制指令,但仍然只能通过地址来控制特定的缓存行。用例是 non-volatile memory attached directly to the CPU, such as Intel Optane DC Persistent Memory. If you want to commit to persistent storage without making the next read slow, you can use clwb. But note that clwb is not guaranteed to avoid eviction, it's merely allowed to. It might run the same as clflushopt, like .

参见 https://danluu.com/clwb-pcommit/, but note that pcommit isn't required: Intel decided to simplify the ISA before releasing any chips that need it, so clwb or clflushopt + sfence are sufficient. See https://software.intel.com/en-us/blogs/2016/09/12/deprecate-pcommit-instruction

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


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

可以 分配一个非常大的缓冲区,然后 memset 它(因此这些写入将用该数据污染所有(数据)缓存),然后取消映射它。如果 deletefree 实际上 returns 立即将内存 OS ,那么它将不再是你进程地址的一部分 space,所以只有一个其他数据的一些缓存行仍然是热的:可能是一两行堆栈(假设您在使用堆栈的 C++ 实现上,以及 运行ning 程序在 OS 下。 ..).当然,这只会污染数据缓存,不会污染指令缓存,正如 Basile 指出的那样,某些级别的缓存是每个内核私有的,OSes 可以在 CPUs 之间迁移进程。

此外,请注意,使用实际的 memsetstd::fill 函数调用,或对此进行优化的循环,可以优化为使用缓存旁路或减少污染的存储。而且我还隐含地假设您的代码在 CPU 上 运行ning 具有写分配缓存,而不是在存储未命中时直写(因为所有现代 CPU 都是这样设计的). x86 支持基于每页的 WT 内存区域,但主流 OSes 将 WB 页面用于所有“正常”内存。

做一些无法优化并占用大量内存的事情(例如,使用 long 数组而不是位图的素筛)会更可靠,但当然仍然依赖于缓存污染驱逐其他数据。仅仅读取大量数据也不可靠;一些 CPUs 实现了自适应替换策略,减少了顺序访问造成的污染,因此循环遍历一个大数组有望不会驱逐大量有用的数据。例如。 the L3 cache in Intel IvyBridge and later 这样做。