代码之家  ›  专栏  ›  技术社区  ›  Vincent

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

  •  9
  • Vincent  · 技术社区  · 8 年前

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

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

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

    void flush_cache() 
    {
        // Contents
    }
    
    2 回复  |  直到 8 年前
        1
  •  18
  •   Peter Cordes    4 年前

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


    不,使用纯ISO C++17无法可靠或高效地执行此操作 . 它不知道也不关心CPU缓存。你所能做的就是触摸大量的内存,这样所有其他的东西都会被逐出 1. ,但这不是你真正想要的。(当然,冲洗 全部的 根据定义,缓存效率低下……)

    CPU缓存管理函数/内部函数/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缓存被刷新。i、 e.与大多数定时效果不同,延迟中断那么长时间可以被视为一种架构效果。在多核系统上也很复杂,因为它必须刷新缓存 全部的 核心。

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

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


    我能想到的唯一原因是,您可能希望进行某种实验,以了解特定CPU的内部结构是如何设计的。因此,具体操作细节至关重要。我甚至不想用一种便携/通用的方式来实现这一点。

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


    通常,当出于正确性原因需要刷新缓存时 知道 哪个地址范围需要刷新 . e、 g.在使用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 ,请注意 __内置\uu清除\u缓存 将x86的指令编译为零,但会对周围的代码产生影响:没有它,gcc可以在转换到函数指针并调用之前优化缓冲区中的存储。(它没有意识到数据被用作代码,所以它认为它们是死存储并将其消除。)

    尽管有名字, __内置\uu清除\u缓存 与…完全无关 wbinvd公司 . 它需要一个地址范围作为args,这样就不会刷新整个缓存并使其无效。它也不使用 clflush , clflushopt clwb 从缓存中实际写回(并有选择地逐出)数据。

    当需要刷新某些缓存以确保正确性时,只需刷新一系列地址, 通过刷新所有缓存来降低系统速度。


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

    没有一条轻量级指令被设计为性能提示,就像 _mm_prefetch .


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

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

    有一个Linux系统调用包装器可移植地逐出一系列地址: cacheflush(char *addr, int nbytes, int flags) . 假设x86上的实现使用 clflush公司 clflushopt公司 在循环中,如果x86完全支持它。手册页上说它最早出现在MIPS Linux中,“但是 现在,Linux在其他一些系统上提供了cacheflush()系统调用 架构,但参数不同。"

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


    最近的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 不需要:Intel决定在发布任何需要ISA的芯片之前简化ISA,因此 clwb公司 clflushopt公司 + sfence 足够了。看见 https://software.intel.com/en-us/blogs/2016/09/12/deprecate-pcommit-instruction .

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


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

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

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

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

        2
  •  1
  •   Peter Cordes    8 年前

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

    正如Johan所评论的,x86-64有一个特权指令来执行您想要的操作,但是 __内置\uu清除\u缓存 不使用它(并且在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” .