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

标签 c++ assembly memory optimization cpu-cache

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

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

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

    最佳答案

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

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

    CPU缓存管理功能/内在函数/asm指令是C++语言特定于实现的扩展。但是除了内联汇编之外,我所知道的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上使用 iopl() system call进行设置)。因此,wbinvd仅在实际在环0(即内核代码)中运行时有效。参见Privileged Instructions and CPU Ring Levels

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

    我能想到的,您可能想要这样做的唯一原因是为了进行某种实验,以弄清楚特定CPU的内部结构是如何设计的。因此,确切的操作细节至关重要。我什至不想要一种便携式/通用的方法来做到这一点。

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

    通常,出于正确性原因,您希望/需要刷新缓存时,您知道哪个地址范围需要刷新。例如在具有不具有缓存一致性的DMA的体系结构上编写驱动程序时,写回操作会在DMA读取之前发生,并且不会逐步进行DMA写操作。 (逐出部分对于DMA读取也很重要:您不希望使用旧的缓存值)。但是x86如今已经具有与缓存相关的DMA,因为现代设计将内存 Controller 内置到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完全无关。它需要一个地址范围(如args),因此不会刷新并使整个缓存无效。它还不使用clflushclflushoptclwb从缓存中实际写回(或可选地逐出)数据。

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

    出于性能原因,至少在x86 上故意刷新缓存几乎是没有道理的。有时,您可以使用污染最小化的预取来读取数据而不会造成太多的缓存污染,或者使用NT存储区来写缓存。但是,在正常情况下,最后一次触摸某些内存后再执行“常规”操作,然后执行clflushopt通常是不值得的。就像存储一样,它必须一直遍历整个内存层次结构,以确保在任何地方都能找到并刷新该行的任何副本。

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

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

    有一个用于 _mm_clflush(void const *p)][6] 的Intel内在[clflush包装器(另一个是 clflushopt ),但它们只能按(虚拟)地址刷新缓存行。您可以遍历您的进程已映射的所有页面中的所有缓存行...(但是那只能刷新您自己的内存,而不能刷新正在缓存内核数据的缓存行,例如您的进程的内核堆栈或其task_struct,与您刷新所有内容相比,第一个系统调用仍然会更快。

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

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

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

    请参阅https://danluu.com/clwb-pcommit/,但请注意,不需要pcommit:Intel决定在发布任何需要它的芯片之前简化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++实现,该实现使用堆栈,并且在OS下运行程序...)。当然,这只会污染数据缓存,而不污染指令缓存,并且正如Basile指出的那样,某些级别的缓存是每个内核专用的,并且OS可以在CPU之间迁移进程。

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

    进行一些无法优化并占用大量内存的操作(例如使用long数组而不是位图的主筛)会更可靠,但当然仍然依赖于缓存污染来驱逐其他数据。仅仅读取大量数据也不可靠。一些CPU实现了自适应替换策略,以减少顺序访问带来的污染,因此,希望在大型阵列上循环不会驱逐大量有用数据。例如。 the L3 cache in Intel IvyBridge and later执行此操作。

    关于c++ - 有没有办法刷新与程序相关的整个CPU缓存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48527189/

    相关文章:

    c++ - <fstream> Release模式下奇怪的内存泄漏

    c++ - 在 Julia 中编写和调用 ArrayFire 自定义 C 函数的正确方法

    assembly - NASM 是否有默认目标处理器?

    r - 使用 mclapply、foreach 或 [r] 中的其他东西并行操作对象?

    c - 如何调试嵌入式应用程序中的内存问题

    c++ - 使用容器和字符串时如何强制使用显式分配器类型参数

    c++ - Mac OS X 上的 Emacs 24 和 GDB 6.3

    c - 如何在C中打印EIP地址?

    c++ - 使 GCC 的 ASM 标签可用于标记化

    performance - vim 是否将整个文件读入内存