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

rwa*_*ace 7 multithreading cpu-architecture memory-model memory-barriers cpu-cache

共享内存多处理系统通常需要为缓存一致性生成大量流量。核心 A 写入缓存。Core 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 最常见的当前/未来服务器配置。

Pet*_*des 5

真正的 CPU 不使用共享总线;流量通过一个 L3 缓存,它的标签用作监听过滤器(特别是在单插槽 Intel 芯片中)。或在其他微架构上节省流量的类似方法。您是对的,当您扩展到多个内核时,实际上向每个其他内核广播消息的功耗和性能成本会高得令人望而却步。 共享总线只是像 MESI 这样的协议的简单思维模型,而不是现代 CPU 中的真正实现。 请参阅现代 x86 CPU 使用什么缓存一致性解决方案?例如。

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

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

我认为,多插座系统必须监听另一个插座或咨询监听过滤器来确定这一点,但对功率/能量最敏感的系统是移动的(总是单插座)。


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

有没有办法反过来做,向 CPU 指示给定的缓存行永远不会对任何其他线程感兴趣?

如果您有将存储数据与失效相结合的缓存写入协议,则可以跳过 RFO。 例如,x86 具有绕过缓存的 NT 存储,并且显然快速字符串存储 ( rep stos/ rep movs) 甚至在 ERMSB 也可以使用无 RFO 写入协议之前(至少在 P6 中,根据设计它的 Andy Glew 的说法),即使它们离开了它们缓存层次结构中的数据。但是,这仍然需要使其他缓存失效,除非该内核已经拥有处于 E 或 M 状态的线路。 用于 memcpy 的增强型 REP MOVSB

一些 CPU 确实有一些暂存内存,这是每个内核真正私有的。 它根本不共享,因此不需要或不可能进行显式刷新。请参阅带宽博士的回答,您可以使用程序集直接访问缓存吗?- 这在 DSP 上显然很常见。


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

通常内存屏障的工作原理是简单地让当前核心等待直到存储缓冲区排入 L1d 缓存(即先前的存储已成为全局可见),因此如果您允许非一致性 L1d,则需要一些其他机制来刷新它。(例如 x86clflushclwb强制写回外部缓存。)

为大多数软件创建利用这一点的方法是很困难的。例如,假设您可以获取本地 var 的地址并将其传递给其他线程。即使在单线程程序中,任何指针也可能来自mmap(MAP_SHARED). 因此,您不能默认将堆栈空间映射为非连贯或类似的东西,并且编译程序以使用额外的刷新指令,以防它们获得指向确实需要可见的非连贯内存的指针,毕竟这将完全失败整个事情的目的。

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

  • @SomeName:我的来源是 Andy Glew 的 Stack Overflow 评论,他在 Intel 从事 PPro 工作时设计了 P6 快速字符串实现。我在[这个答案](/sf/answers/2373412121/)中引用了他的评论。ERMSB 是一项单独的功能,允许这些存储无序提交。我认为即使在后来的 P6 系列和 ERMSB 之前的 Sandybridge 中,快速字符串也不仅仅是使用宽负载/存储,包括多核芯片,但我不确定无 RFO 存储功能是否始终存在。 (2认同)