【问题标题】:Is there a way to flush the entire CPU cache related to a program?有没有办法刷新与程序相关的整个 CPU 缓存?
【发布时间】:2018-07-09 16:17:52
【问题描述】:

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

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

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

void flush_cache() 
{
    // Contents
}

【问题讨论】:

  • 只是好奇,这个的用例是什么?
  • 我可能是错的,但我认为这不可能完全用 C++ 完成,即使是内在函数也是如此。您需要设置线程关联的能力,我不相信 C++ 有这个概念。您还需要一种方法来获取缓存大小或对缓存进行假设。所以我不确定这里的可移植性很容易实现。
  • 根据 x86-64 软件开发人员手册,使整个缓存无效的指令是特权的,所以我怀疑任何理智的操作系统都会允许从用户代码中调用它们。
  • 只需遍历所有内存并每隔 64 个(通常的缓存行大小)地址刷新一次。最后刷新循环所在的行,但在此指令之后,将再次加载此缓存行。您可能无法刷新保存程序数据的内核内存。无论如何,你为什么要这个?
  • 我认为这个问题并不明确。据我了解,CLFLUSH 指令可确保将缓存的数据写回内存,但问题听起来像是目标是清除缓存中的数据。这些是不同的操作。实际目标是什么?

标签: c++ assembly memory optimization cpu-cache


【解决方案1】:

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


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

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


例如,在 x86 上,您要查找的 asm 指令是 wbinvd 它在逐出之前回写所有脏行,这与 invd 不同(它会丢弃缓存 没有回写,useful when leaving cache-as-RAM mode)。所以理论上wbinvd 没有架构效果,只有微架构,但它太慢了,它是一个特权指令。正如Intel's insn ref manual entry for wbinvd 指出的那样,它会增加中断延迟,因为它本身不是可中断的,并且可能必须等待 8 MiB 或更多的脏 L3 缓存被刷新。即延迟这么长时间的中断可以被认为是一种架构效应,与大多数时序效应不同。这在多核系统上也很复杂,因为它必须为 所有 个内核刷新缓存。

我认为没有任何方法可以在 x86 的用户空间(环 3)中使用它。与 cli / stiin/out 不同,它不是由 IO 权限级别启用的(您可以在 Linux 上使用 iopl() system call 设置它)。所以wbinvd 仅在实际运行在环 0 中(即在内核代码中)时才有效。见Privileged Instructions and CPU Ring Levels

但是,如果您正在使用 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 具有与数据缓存一致的指令缓存,并且无需任何特殊同步指令即可支持self-modifying code。请参阅godbolt for x86 and AArch64,并注意__builtin__clear_cache 对于 x86 编译为零指令,但对周围代码有影响:没有它,gcc 可以在转换为函数指针和调用之前优化存储到缓冲区。 (它没有意识到数据被用作代码,所以它认为它们是死存储并消除它们。)

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

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


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

没有像_mm_prefetch 那样设计为性能提示的轻量级指令。


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

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

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

我认为没有暴露 wbinvd 的 Linux 系统调用,但您可以编写一个内核模块来添加一个。


最近的 x86 扩展引入了更多的缓存控制指令,但仍然只能通过地址来控制特定的缓存行。用例用于non-volatile memory attached directly to the CPU,例如Intel Optane DC Persistent Memory。如果您想提交持久存储而不使下一次读取变慢,您可以使用clwb。但请注意,clwb 并不能保证避免被驱逐,它只是允许。它可能与clflushopt 运行相同,例如may be the case on SKX

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

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


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

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

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

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

【讨论】:

  • 您能描述一下如何执行上述缓存驱逐吗?例如,如果我想驱逐给定(虚拟)内存地址 A 映射到的缓存行,我将只访问 A+i*64KiB 的随机地址,其中 i=1,..,N?
  • @Patrick:是的。对于 L2 缓存,使用大页面(保证连续的物理内存),因此虚拟步长 = 物理步长。对于 L1d 缓存,为同一个集合设置别名的必要步幅通常为 4kiB,因此 any 页面中的相同偏移量是可以的。缓存替换通常是伪 LRU 而不是真 LRU,因此触摸其他 8 行并不能保证驱逐最旧的行,但从概率上来说它可能没问题。 (如果这个策略对于任何给定的应用程序都值得考虑,因为生产者可以浪费大量额外的时间来帮助下一个读者)。
  • @Patrick:不一定是随机地址,顺序就可以。如果硬件预取注意到跨步读取访问模式,这实际上是一件好事,尽管如果你一次完成它们并不重要。下一个地址计算不依赖于先前的负载,因此 CPU 内核中的内存级并行性可以有 8 到 12 个未完成的需求负载。 (我假设负载与强制驱逐的存储一样好,除非缓存中存在一些偏向于驱逐干净行而不是脏行的偏见。)如果您有多个这样的生产者,它们都可以共享一个可以在 L3 中保持热的读取集。
  • 感谢您的解释!我试图从引用数组元素 X 的缓存中逐出某个地址。因此,如果我想将它从 L1D 中逐出,我只需计算此元素 X 相对于页面的偏移量,然后使用它来访问具有相同偏移量的许多其他页面,直到集合已满并且最旧的条目被逐出,对吗?
  • @Patrick:是的,但通常该集合总是充满一些数据。驱逐通常是pseudo-LRU 不是真正的LRU。 (algorithm LRU, how many bits needed for implement this algorithm?)
【解决方案2】:

答案是,没有标准的 C++ 方法可以做到这一点(即使有一些编译器内在函数)。 GCC__builtin__clear_cache and __builtin_prefetchClang 可能也有。

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

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


顺便说一句,你不应该在程序上推理,而应该在 processes 上推理。每个都有自己的virtual address space

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

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

不,optimizing compilers 正在重新排序和重新调度机器代码指令,并且经常混合与不同 C++ 语句相关的多个计算。他们被允许在编译时进行一些计算。阅读有关as-if rule 的更多信息。请参阅 CppCon 2017 演讲:Matt Godbolt “What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”

【讨论】:

  • __builtin__clear_cachecacheflush(2) 都采用一系列虚拟地址,因此与 _mm_clflush(void*) 相比,它们根本没有帮助。此外,__builtin__clear_cache 是 x86 上的空操作,因为它的语义含义是使 JITed 机器代码的缓冲区可以安全执行。我进行了编辑以使答案实际上没有错,但您可能应该删除第一部分并留下好的第二部分。 (我的回答涵盖了您的第一部分试图回答的细节。)
  • 在linux上也可以使用这个内核模块github.com/batmac/wbinvd
猜你喜欢
  • 2016-04-19
  • 2021-12-31
  • 1970-01-01
  • 1970-01-01
  • 2011-03-30
  • 2016-03-23
  • 2018-05-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多