是什么将 x86 缓存行明确标记为脏 - 任何写入,还是需要显式更改?

What specifically marks an x86 cache line as dirty - any write, or is an explicit change required?

这个问题专门针对现代 x86-64 缓存一致性架构 - 我很感激答案在其他 CPU 上可能会有所不同。

如果我写入内存,MESI 协议要求先将缓存行读入缓存,然后在缓存中修改(值写入缓存行,然后标记为脏)。在较旧的 write-though 微架构中,这将触发缓存行被刷新,在回写下,被刷新的缓存行可能会延迟一段时间,并且在这两种机制下可能会发生一些写合并(更可能是写回) .而且我知道这如何与访问相同数据缓存行的其他内核交互 - 缓存侦听等

我的问题是,如果存储与缓存中已有的值精确匹配,如果没有翻转任何一位,英特尔微架构是否注意到这一点并且 NOT 标记该行是脏的,从而可能避免该行被标记为独占,并且在某个时候会出现写回内存开销?

随着我对更多循环进行矢量化,我的矢量化操作组合基元不会明确检查值的变化,在 CPU/ALU 中这样做似乎很浪费,但我想知道底层缓存是否电路可以在没有显式编码的情况下完成它(例如存储微操作或缓存逻辑本身)。随着跨多个内核的共享内存带宽越来越成为资源瓶颈,这似乎是一种越来越有用的优化(例如,对同一内存缓冲区重复清零——如果它们已经存在,我们不会从 RAM 中重新读取值在缓存中,但强制写回相同的值似乎很浪费)。回写缓存本身就是对此类问题的承认。

我能否礼貌地请求保留“理论上”或“这真的无关紧要”的答案 - 我知道内存模型是如何工作的,我正在寻找关于如何写入相同值的确凿事实(与避免存储相反)将影响内存总线的争用,您可以安全地假设这是一台机器 运行 几乎总是受内存带宽限制的多个工作负载。另一方面,对芯片不这样做的确切原因的解释(我悲观地假设它们不这样做)会很有启发性...

更新: 这里有一些符合预期的答案 https://softwareengineering.stackexchange.com/questions/302705/are-there-cpus-that-perform-this-possible-l1-cache-write-optimization 但仍然有很多猜测“这一定很难,因为它不是” t done”,并说在主 CPU 核心中这样做会很昂贵(但我仍然想知道为什么它不能成为实际缓存逻辑本身的一部分)。

更新(2020 年): Travis Downs 已找到硬件商店消除的证据,但似乎仅针对零且仅在数据遗漏 L1 和 L2 的情况下,即便如此,并非在所有情况下。 强烈推荐他的文章,因为它更详细...... https://travisdowns.github.io/blog/2020/05/13/intel-zero-opt.html

更新(2021): Travis Downs 现在发现了证据表明这种零存储优化最近在微代码中被禁用了......来自源头的更多细节 https://travisdowns.github.io/blog/2021/06/17/rip-zero-opt.html

目前 没有 x86(或任何其他 ISA,据我所知)的实现支持优化静默存储。

对此已有学术研究,甚至 "eliminating silent store invalidation propagation in shared memory cache coherency protocols" 也有专利。 (如果您对更多感兴趣,请谷歌搜索 '"silent store" cache'。)

对于 x86,这会干扰 MONITOR/MWAIT;一些用户可能希望监视线程在静默存储上唤醒(可以避免失效并添加 "touched" 一致性消息)。 (目前 MONITOR/MWAIT 享有特权,但将来可能会改变。)

同样,这可能会干扰事务内存的一些巧妙使用。如果内存位置用作保护以避免显式加载其他内存位置,或者在支持此类的体系结构中(例如在 AMD 的高级同步工具中),则从读取集中删除受保护的内存位置。

(硬件锁消除是静默 ABA 存储消除的一种非常受限制的实现。它具有显式请求检查值一致性的实现优势。)

在性能 impact/design 复杂性方面也存在实施问题。这样会禁止避免读取所有权(除非静默存储消除仅在缓存行已经存在于共享状态时才处于活动状态),尽管目前还没有实现读取所有权避免。

静默存储的特殊处理也会使内存一致性模型(可能尤其是 x86 相对强大的模型)的实现复杂化。这也可能会增加因一致性失败的推测而回滚的频率。如果静默存储仅支持 L1-present 行,时间 window 会非常小并且回滚 非常 很少见;存储到 L3 或内存中的缓存行可能会使频率增加到非常罕见,这可能会成为一个值得注意的问题。

缓存行粒度的静默也比访问级别的静默少见,因此避免的无效次数会更少。

额外的缓存带宽也是一个问题。目前,英特尔仅在 L1 缓存上使用奇偶校验,以避免在小写入时需要读-修改-写。要求 每个 写入都有一个读取以检测静默存储,这将对性能和功耗产生明显的影响。 (这样读 可以仅限于共享缓存行并随机执行,在没有完全缓存访问利用的情况下利用周期,但这仍然会产生电力成本。)这也意味着如果已经存在读取-修改-写入支持,则此成本将会下降L1 ECC 支持(哪些功能会让一些用户满意)。

我不太了解静默存储消除,因此可能还有其他问题(和解决方法)。

随着性能改进的许多唾手可得的成果已经取得,难度更大、收益更小、通用性更差的优化变得更具吸引力。由于静默存储优化随着内核间通信的增加而变得更加重要,并且随着更多内核用于处理单个任务,内核间通信将增加,因此其价值似乎可能会增加。

可以在硬件中实现,但我认为没有人这样做。为每个商店都这样做会消耗缓存读取带宽或需要额外的读取端口并使流水线更难。

您将构建一个执行 read/compare/write 循环而不只是写入的缓存,并且可以有条件地将行保留在独占状态而不是修改(MESI)。这样做(而不是在它仍然是共享的时候检查)仍然会使该行的其他副本无效,但这意味着没有与内存排序的交互。当核心拥有高速缓存行的独占所有权时,(静默)存储变得全局可见,就好像它已经翻转到已修改,然后通过写回 DRAM 回到独占。

read/compare/write 必须以原子方式完成(您不能丢失读取和写入之间的缓存行;如果发生这种情况,比较结果将过时)。这使得通过管道将数据从存储队列提交到 L1D 变得更加困难。


在多线程程序中,将此作为优化在软件中仅适用于共享变量是值得的。

避免使其他人的缓存无效可以使其值得转换

shared = x;

进入

if(shared != x)
    shared = x;

我不确定这里是否存在内存排序问题。显然,如果 shared = x 从未发生,则没有发布序列,因此您只有获取语义而不是发布。但是,如果您存储的值通常是已经存在的值,那么任何使用它来订购其他东西都会有 ABA 问题。

IIRC,Herb Sutter 在他的 atomic Weapons: The C++ Memory Model and Modern Hardware 演讲的第 1 部分或第 2 部分中提到了这种潜在的优化。 (几个小时的视频)

除了共享变量之外,在软件中这当然太昂贵了,因为编写共享变量的成本是其他线程中的许多延迟周期(缓存未命中和内存顺序错误推测机器清除:)


相关:请参阅 以了解有关 x86 内存带宽的更多信息,尤其是 NT 与非 NT 存储内容,以及 "latency bound platforms"为什么多核 Xeons 上的单线程内存带宽低于四核,尽管来自多核的总带宽更高。

我发现证据表明来自英特尔的一些现代 x86 CPUs,包括 Skylake 和 Ice Lake 客户端芯片,可以在至少一种特定情况下优化冗余(静默)存储:

  • 全零缓存行被更多的零完全或部分覆盖。

即“零对零”的情况。

例如,此图表显示了一个场景的性能(圆圈,在左轴上测量)和相关性能计数器,在该场景中,不同大小的区域以 0 或 1 的 32 位值归档,在冰湖:

一旦该区域不再适合 L2 缓存,写入零有一个明显的优势:填充吞吐量几乎高出 1.5 倍。在零的情况下,我们还看到 L2 的驱逐几乎都是“无声”的,表明不需要写出脏数据,而在另一种情况下,所有驱逐都是非无声的。

有关此优化的一些杂项细节:

  • 它优化了脏缓存行的回写,而不是仍然需要发生的RFO(实际上,可能需要读取来决定优化可以是已应用)。
  • 它似乎发生在 L2 或 L2 <-> L3 接口周围。也就是说,我没有找到适合 L1 或 L2 的负载的这种优化的证据。
  • 因为优化在缓存层次结构最内层之外的某个点生效,不需要写零来利用:行就足够了只有在写回 L3 时才包含全零。因此,从全零行开始,您可以进行任意数量的非零写入,然后是整行的最终零写入 1,只要该行不同时逃到L3
  • 优化具有不同的性能影响:有时优化是基于对相关性能计数的观察而发生的,但几乎没有增加吞吐量。其他时候影响可能非常大。
  • 我没有在 Skylake 服务器或更早的 Intel 芯片中找到效果的证据。

我写得更详细了here, and there is an addendum for Ice Lake, which exhibits this effect more strongly here

更新,2021 年 6 月:此优化已在英特尔提供的最新 CPU 微码版本中 禁用 ,因为安全原因 (details).


1 或者,至少用零覆盖该行的非零部分。