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

时间:2018-01-30 17:24:40

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

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

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

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

void flush_cache() 
{
    // Contents
}

2 个答案:

答案 0 :(得分:9)

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

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

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

例如,在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上设置{{3 }})。因此wbinvd仅在实际在环0中运行时(即在内核代码中)才有效。请参阅iopl() system call

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

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

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

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

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

这就是gcc提供Privileged Instructions and CPU Ring Levels的原因。它不一定要冲洗任何东西,只能确保将该内存作为代码执行是安全的。 x86具有与数据缓存一致的指令缓存,甚至支持__builtin__clear_cache而没有任何特殊的同步指令。请参阅self-modifying code,并注意__builtin__clear_cache实际上并不是x86的无操作:没有它,gcc会在转换为函数指针并调用之前将存储优化到缓冲区。 (它没有意识到数据被用作代码,所以它认为它们已经死了并且消除它们。)

尽管名称如此,但__builtin__clear_cachewbinvd完全无关。它需要一个地址范围作为args,并且它编译为x86上的任何指令,它只使用clflushclflushoptclwb

无论如何,当你需要刷新一些缓存以获得正确性时,你只需要刷新一系列地址,通过刷新所有缓存来减慢系统速度。

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

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

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

Linux系统调用包装器可以移植一系列地址:clflushopt。据推测,x86上的实现在循环中使用task_structclflush,如果它在x86上完全支持的话。该手册页说它最初出现在MIPS Linux"但是        如今,Linux在其他方面提供了cacheflush()系统调用        架构,但有不同的论点。"

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

最近的x86扩展引入了更多缓存控制指令,但仍然只能通过地址来控制特定的缓存行。用例适用于cacheflush(char *addr, int nbytes, int flags),例如non-volatile memory attached directly to the CPU。如果要在不使下一次读取变慢的情况下提交持久存储,则可以使用Intel Optane DC Persistent Memory。但请注意,wbinvd不是保证以避免驱逐,它只是允许。它可能与clwb一样运行,例如clwb

请参阅may be the case on SKX,但请注意clflushopt并非必需:英特尔决定在发布任何需要它的芯片之前简化ISA,因此pcommit或{{1} } + clwb就足够了。请参阅https://danluu.com/clwb-pcommit/

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

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

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

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

做一些无法优化并触及大量内存的东西(例如带有memset数组而不是位图的主筛)会更可靠,但当然仍然依赖于缓存污染驱逐其他数据。只读大量数据也不可靠;一些CPU实现了自适应替换策略,可以减少顺序访问造成的污染,因此循环使用大型阵列希望不会驱逐大量有用的数据。例如。 https://software.intel.com/en-us/blogs/2016/09/12/deprecate-pcommit-instruction这样做。

答案 1 :(得分:1)

答案是没有,没有标准的C ++方法(即使使用一些编译器内在函数)。 GCC__builtin__clear_cache and __builtin_prefetchClang也可能有。{/ p>

正如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”