减少缓存行失效的总线流量

Reducing bus traffic for cache line invalidation

共享内存多处理系统通常需要为缓存一致性生成大量流量。 Core A 写入缓存。核心 B 稍后可能会读取相同的内存位置。因此,核心 A,即使它本来可以避免写入主内存,也需要向核心 B 发送通知,告诉 B 如果该地址保存在缓存中,则使其无效。

究竟什么时候需要这样做,这是一个复杂的问题。不同的 CPU 体系结构具有不同的内存模型,在此上下文中的内存模型是一组关于将观察到的事物发生顺序的保证。内存模型越弱,A 就越容易准确地了解何时发生它向B发送通知,A和B越容易并行做更多的事情。不同 CPU 架构的内存模型的一个很好的总结:https://en.wikipedia.org/wiki/Memory_ordering#Runtime_memory_ordering

所有的讨论似乎都是关于何时失效发生,顺序事情发生在什么地方。

但在我看来,在许多工作负载中,A 写入的大部分数据 永远不会 被 B 使用,因此如果总线流量为这些缓存失效可以完全消除。专用于执行缓存一致性的硬件仍然需要存在,因为 A 和 B 有时需要共享数据,但写入共享总线是 CPU 可以做的更耗能的事情之一,电池寿命和如今,散热通常会限制资源,因此减少总线流量将是一种有用的优化。有办法吗?

从效率的角度来看,理想的情况是,如果省略总线流量是默认设置(因为大多数写入数据不与其他线程共享),并且您必须在需要缓存一致性的地方显式发出内存屏障。另一方面,这可能是不可能的,因为现有代码的数量假定它在 x86 或 ARM 上是 运行;有没有办法反过来做,向 CPU 表明任何其他线程永远不会对给定的缓存行感兴趣?

我对任何系统的答案都感兴趣,但最感兴趣的是 x64、ARM 或 RISC-V 上 Linux 的最常见 present/future 服务器配置。

真正的CPU不使用共享总线;流量通过 L3 缓存,其标签用作侦听过滤器(尤其是在单路 Intel 芯片中)。或在其他微体系结构上节省流量的类似方法。你是对的,当你扩展到许多内核时,实际上向每个其他内核广播消息对于功率和性能来说会非常昂贵。 共享总线只是像 MESI 这样的协议的简单心智模型,而不是现代 CPU 中的实际实现。 例如参见 [​​=17=]。

具有写分配的回写缓存需要在存储到缓存行之前读取缓存行,因此它们具有该行其他部分的原始数据。此读取在由写入触发时称为“所有权读取”(RFO),以使线路进入 MESI 独占状态(无需外部流量即可转换为脏修改)。 RFO 包括失效。

如果初始访问是只读的,则该行通常会像 RFO 一样进入独占状态,如果没有其他核心具有缓存副本(即它在 L3 中丢失(最后一级)缓存)。这意味着对于读取一些私有数据然后修改它的常见模式,流量保持在最低水平。

我认为,多插槽系统必须侦听另一个插槽或咨询侦听过滤器才能确定这一点,但大多数 power/energy-sensitive 系统是移动的(总是单插槽)。


有趣的事实:Skylake-X 之前的英特尔 2 插槽 Xeon 芯片(例如 E5 ...-v4)没有用于插槽之间流量的侦听过滤器,并且只是通过 QPI 在另一个插槽上进行垃圾邮件侦听link。 E7 CPUs(能够在 quad 和更大的系统中使用)有专用的监听过滤器缓存来跟踪热线的状态,以及足够的 QPI links 来交叉连接更多的套接字。来源:John McCalpin's post on an Intel forum,虽然我没能找到太多其他数据。也许 John 正在考虑早期的系统,如 Core2 / Nehalem Xeons,英特尔确实在其中谈论具有监听过滤器,例如 https://www.intel.ca/content/dam/doc/white-paper/quick-path-interconnect-introduction-paper.pdf 将 QPI 与其早期设置进行比较。并且有一些关于可以权衡延迟与吞吐量的侦听模式的更多详细信息。也许英特尔只是没有以同样的方式使用术语“侦听过滤器”。

Is there a way to do it the other way around, to indicate to the CPU that a given cache line will never be of interest to any other thread?

如果您有将存储数据与失效结合起来的缓存写入协议,则可以跳过 RFO。 例如x86 有绕过缓存的 NT 存储,显然 fast-strings 存储 (rep stos / rep movs) 甚至在 ERMSB 之前也可以使用无 RFO 写入协议 (), even though they leave their data in the cache hierarchy. That does still require invalidation of other caches, though, unless this core already owns the lines in E or M state.

有些 CPU 确实有一些 scratchpad memory which is truly private to each core. It's not shared at all, so no explicit flushing is needed or possible. See Dr. Bandwidth's answer on - 这显然在 DSP 上很常见。


但除此之外,通常不会,CPUs 不提供将部分内存地址 space 视为非一致的方法。一致性是 CPU 不想让软件禁用的保证。 (也许是因为它可能会产生安全问题,例如,如果一些旧的写入可能 最终 在 OS 对其进行校验和之后但在 DMA 到磁盘之前在文件数据页中可见,非特权用户-space 可能导致像 BTRFS 或 ZFS 这样的校验和 FS 在它 mmap(PROT_WRITE|PROT_READ, MAP_SHARED) 上看到的文件中看到坏块。)

通常情况下,内存屏障的工作原理是让当前核心等待,直到存储缓冲区已排入 L1d 缓存(即先前的存储已变得全局可见),因此如果您允许非-coherent L1d 那么需要一些其他机制来刷新它。 (例如 x86 clflushclwb 强制写回外部缓存。)

为大多数软件创造利用这一点的方法是很困难的;例如假定您可以获取本地变量的地址并将其传递给其他线程。甚至在单线程程序中,任何指针都可能来自 mmap(MAP_SHARED)。因此,您不能默认将堆栈 space 映射为非一致的或类似的东西,并且编译程序以使用额外的刷新指令,以防它们获得指向非一致内存的指针,毕竟这确实需要可见只会完全破坏整个事情的目的。

所以这不值得追求的部分原因是它是额外的复杂性,堆栈上的所有东西都必须关心才能使它有效。 Snoop 过滤器和基于目录的一致性足以解决这个问题,总体上比期望每个人都为这个低级功能优化他们的代码要好得多!