首页 > 解决方案 > 减少高速缓存行失效的总线流量

问题描述

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

标签: multithreadingcpu-architecturecpu-cachememory-barriersmemory-model

解决方案


真正的 CPU 不使用共享总线;流量通过 L3 缓存,其标签用作窥探过滤器(尤其是在单插槽 Intel 芯片中)。或在其他微架构上节省流量的类似方法。你是对的,当你扩展到许多核心时,实际上向每个其他核心广播一条消息对于功率和性能来说都是非常昂贵的。 共享总线只是 MESI 等协议的简单思维模型,而不是现代 CPU 中的真正实现。 请参阅现代 x86 CPU 使用什么缓存一致性解决方案?例如。

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

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

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


有趣的事实:Skylake-X 之前的英特尔 2 插槽 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 存储,并且显然在 ERMSB 还可以使用无 RFO 写入协议之前(rep stos至少在 P6 中,根据设计它的 Andy Glew 的说法),即使它们离开了他们的缓存层次结构中的数据。但是,这仍然需要使其他缓存失效,除非该内核已经拥有处于 E 或 M 状态的行。 用于 memcpy 的增强型 REP MOVSBrep movs

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


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

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

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

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


推荐阅读