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

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

本文关键字:CPU 缓存 刷新 程序 有没有      更新时间:2023-10-16

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

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

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

void flush_cache() 
{
// Contents
}

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


不,使用纯ISO C++无法可靠或有效地做到这一点。 它不知道也不关心 CPU 缓存。 你能做的最好的事情就是触摸大量内存,这样其他所有内容最终都会被驱逐1,但这不是你真正要求的。 (当然,根据定义,刷新所有缓存是低效的...... 请参阅刷新缓存以防止基准测试波动,了解有关实现详细信息的一些提示(如果采用该路线)。

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

例如,

在 x86 上,您要查找的 asm 指令是wbinvd它在逐出之前写回任何脏行,不像invd(它删除缓存而不写回,在离开缓存即 RAM 模式时很有用)。 所以从理论上讲,wbinvd没有建筑效果,只有微观建筑效果,但它太慢了,这是一个特权指令。 正如英特尔的 insn ref 手册条目wbinvd指出的那样,它会增加中断延迟,因为它本身不可中断,并且可能需要等待 8 MiB 或更多的脏 L3 缓存被刷新。 也就是说,与大多数时序效应不同,延迟这么长时间的中断可以被认为是一种架构效应。 在多核系统上也很复杂,因为它必须刷新所有内核的缓存。

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

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


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

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


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

在驱动程序之外,您需要担心缓存的主要情况是在具有非相干指令缓存的非 x86 体系结构上使用 JIT 代码生成。 如果您(或 JIT 库)将一些机器代码写入char[]缓冲区并将其强制转换为函数指针,则 ARM 等体系结构不能保证代码提取将"看到"新写入的数据。

这就是为什么 gcc 提供__builtin__clear_cache. 它不一定刷新任何内容,只确保将该内存作为代码执行是安全的。 x86 具有与数据缓存一致的指令缓存,并支持自修改代码,无需任何特殊同步指令。 请参阅 x86 和 AArch64 的 godbolt,并注意__builtin__clear_cache编译为 x86 的零指令,但对周围的代码有影响:没有它,gcc 可以在强制转换为函数指针和调用之前将存储优化为缓冲区。 (它没有意识到数据被用作代码,所以它认为它们是死存储并消除它们。

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

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


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

没有一个轻量级的指令被设计为性能提示,就像_mm_prefetch相反。


您可以在x86上的用户空间中执行的唯一缓存刷新是使用clflush/clflushopt。 (或者使用 NT 存储,如果事先很热,它们也会逐出缓存行)。 或者当然会为已知的 L1d 大小和关联性创建冲突逐出,例如以 4kiB 的倍数写入多行,这些线路都映射到 32k/8 路 L1d 中的同一集合。

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

有一个 Linux 系统调用包装器可以移植地逐出一系列地址:cacheflush(char *addr, int nbytes, int flags)。 据推测,x86 上的实现在循环中使用clflushclflushopt,如果 x86 完全支持的话。 手册页说它首先出现在MIPS Linux中,"但是 如今,Linux 在其他一些系统上提供了 cacheflush() 系统调用 架构,但有不同的论点。

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


最近的 x86 扩展引入了更多的缓存控制指令,但仍然只能通过地址来控制特定的缓存行。 该用例适用于直接连接到 CPU 的非易失性内存,例如英特尔傲腾 DC 持久内存。 如果要在不减慢下一次读取的情况下提交持久存储,可以使用clwb。 但请注意,clwb不能保证避免被驱逐,它只是被允许的。 它可能与clflushopt运行相同,就像在SKX上一样。

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

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


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

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

另外,请注意,使用实际的memsetstd::fill函数调用或优化该调用的循环可以优化为使用缓存绕过或减少污染的存储。 我还隐含地假设您的代码在具有写入分配缓存的 CPU 上运行,而不是在存储未命中时直写(因为所有现代 CPU 都是这样设计的)。 x86 支持每页 WT 内存区域,但主流操作系统对所有"普通"内存使用 WB 页面。

做一些不能优化并接触大量内存的事情(例如,使用long数组而不是位图的素筛)会更可靠,但当然仍然依赖于缓存污染来驱逐其他数据。 仅仅读取大量数据也不可靠;一些 CPU 实施自适应替换策略,以减少顺序访问造成的污染,因此循环使用大型阵列希望不会驱逐大量有用的数据。 例如,英特尔 IvyBridge 中的 L3 高速缓存,稍后执行此操作。

答案是否定的,没有标准的C++方法可以做到这一点(即使有一些编译器内部函数)。海湾合作委员会有__builtin__clear_cache__builtin_prefetch,Clang可能也有。

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

在 Linux 上,你可以(也许)使用 cacheflush(2) Linux 特定的系统调用。我从来没有用过它,我不知道它是否在 x86-64 上实现。


顺便说一句,你不应该在程序上推理,而应该在流程上推理。每个都有自己的虚拟地址空间。

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

(无论编译器优化如何,该函数都应该工作)?

不,优化编译器是重新排序和重新调度机器代码指令,并且经常混合与不同C++语句相关的多个计算。允许它们在编译时进行一些计算。详细了解假设规则。参见CppCon 2017演讲:Matt Godbolt "我的编译器最近为我做了什么?打开编译器的盖子"。