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

Posted

技术标签:

【中文标题】什么特别地将 x86 缓存行标记为脏 - 任何写入,还是需要显式更改?【英文标题】:What specifically marks an x86 cache line as dirty - any write, or is an explicit change required? 【发布时间】:2018-05-05 04:12:41 【问题描述】:

这个问题专门针对现代 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,但仍然有很多猜测“它一定很难,因为它没有完成”,并说如何做到这一点主 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

【问题讨论】:

softwareengineering.stackexchange.com/questions/302705/… 上的答案大多很糟糕,尤其是目前接受的答案表明对缓存/CPU 寄存器缺乏了解。 【参考方案1】:

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

对此已有学术研究,甚至还有一项关于“消除共享内存缓存一致性协议中的静默存储失效传播”的专利。 (如果你有兴趣,谷歌搜索'"silent store" cache'。)

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

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

(Hardware Lock Elision 是静默 ABA 存储消除的一个非常受限的实现。它具有显式请求检查值一致性的实现优势。)

在性能影响/设计复杂性方面也存在实施问题。这将禁止避免为所有权而读取(除非静默存储消除仅在缓存行已处于共享状态时才有效),尽管目前也未实现为所有权而避免读取。

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

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

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

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

随着性能改进的许多唾手可得的成果已经被采纳,更困难、更少有益和不太通用的优化变得更具吸引力。由于静默存储优化随着更高的内核间通信而变得更加重要,并且随着更多内核被用于处理单个任务,内核间通信将增加,因此此类的价值似乎可能会增加。

【讨论】:

感谢您的回答,这给了我很多进一步调查的机会,但我注意到您暗示“英特尔 [不] 要求每次写入都必须读取”,这不是我的理解。除了不可缓存的内存和非临时写入(两者都将排除此类内容)之外,每次写入都要求该值在缓存中,因此如果缓存行不存在则强制读取。 @Tim Read-for-ownership Avoidance 是一个类似的学术建议。除其他外,它需要以更精细的粒度跟踪有效性/脏度。鉴于标签 ECC 不如数据 ECC 常见(“哦,天哪,我们将不得不在标签上花费更多位!”),支持更细粒度的有效性(这也增加了一致性复杂性)并不是一个快速采用的优化。跨度> @Tim - 我对保罗所说的理解特别是英特尔不需要从 L1 缓存读取到核心/存储缓冲区实现写入:字节可以简单地存储到没有读取的 L1(当该行存在时)。之所以提到 ECC,是因为如果 L1 受 ECC 保护,通常需要读取,因为您需要与存储相邻的值来重新计算纠错码。 Paul 建议 Intel 使用更简单的错误检查机制(奇偶校验),无需相邻字节即可更新。 你所说的关于“写意味着读”的一切都是正确的——但你说的是从 L1 到 L2 的路径以及更高级别的缓存层次结构和内存,这与 Paul 所说的不同. @PaulA.Clayton,如果在全行粒度上完成 RFO 避免,则不需要部分行标记。对于 AVX512,这很可能是一个用例(但也可以合并连续的较小商店而不会破坏排序)。值得注意的是,这也不允许您避免与一致性相关的流(窥探等),只能避免数据获取。这是否真的发生是一个不同的问题,但并不难检查。【参考方案2】:

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

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

读取/比较/写入必须以原子方式完成(您不能丢失读取和写入之间的缓存线;如果发生这种情况,比较结果将是陈旧的)。这使得管道数据从存储队列提交到 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 部分中提到了这种潜在的优化。 (几个小时的视频)

对于除共享变量之外的任何东西,在软件中这样做当然太昂贵了,因为写入它们的成本是其他线程中的许多延迟周期(缓存未命中和内存顺序错误推测机器清除:What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?)


相关:请参阅 this answer 了解有关一般 x86 内存带宽的更多信息,尤其是 NT 与非 NT 存储的东西,以及“延迟绑定平台”以了解为什么单线程内存带宽尽管多核的总带宽更高,但多核 Xeon 的性能低于四核。

【讨论】:

@Tim:是的,我想这就是你要问的。直到您记住缓存是流水线并支持每个时钟 1 次写入之前,这似乎很容易而且很好。在现代 Intel CPU 中,未对齐的写入(包括 32B AVX 向量)没有性能损失,只要它们不跨越缓存线边界,因此任何多周期操作都会因后续存储的重叠而变得混乱。 (一些算法,比如***.com/questions/36932240/…,依赖于高效的重叠存储。) 即使在没有多线程的情况下,软件级别的条件写入优化仍然很有意义:想象一个 memcpy,其中目标很可能已经与源相同(例如大多数缓存行)。如果您实施此操作以首先检查相等性,您将完全删除相等行的商店流量。对于较大的矢量化副本,内存流量往往是主要因素,因此与普通副本相比,这将有所帮助(但它与 NT 存储不兼容)。 @Leeor:您可以这样做,但如果比较结果不相等,则您必须在拥有该行后重新安排提交时间。如果您已经拥有处于E 状态的行,则可以根据比较结果将其切换为M 状态或不切换,但可以通过任何一种方式提交存储。因此,这是一个不那么具有侵入性的设计更改(但没有那么强大的优化)。 @Tim 好吧,通常它只会减少 33% 的内存带宽。您将从 2 次读取(1 个 src,1 个 dest 用于 RFO)和 1 次写入(dest)变为 2 次读取(1 个 src,1 个 dest 用于 RFO)。请记住,如果您的阵列很大,您应该查看 NT 存储,它们以不同的方式获得相同的减少(1 次读取 src,1 次写入 dest)并且可能更快(因为在某些芯片上似乎总带宽是较高的一些 NT 商店在混合)。 @Tim:有一个很长的 SO 答案,其中包含更多关于 NT 与非 NT 存储和相关内存带宽问题的详细信息:***.com/questions/43343231/…【参考方案3】:

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

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

即“零对零”的场景。

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

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

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

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

我把这个写得更详细了here,还有一个冰湖的附录,它更强烈地展示了这个效果here。

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


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

【讨论】:

这是手写 asm,以避免当 GCC 将 0-fill 识别为 memset 但 dword 1 仅作为正常自动矢量化填充时出现 Why is std::fill(0) slower than std::fill(1)? asm 差异? 哦,对了,你之前写过这个。 IIRC 我查看了那个可能的问题的时间,我想你避免了它,但我不记得是怎么做的。 @PeterCordes - 我用几种不同的方式实现了它,但是对于这里显示的图表和大多数其他结果,我只是确保无论填充值如何都使用完全相同的函数:即,填充值作为参数传递给非内联函数,因此我可以确定两个测试都在执行相同的代码(字面意思是在 .text 部分中的相同字节中),只有寄存器内容不同。参见例如here。 这个特殊情况确实依赖于HEDLEY_NEVER_INLINE(但我检查了程序集),所以更安全的方法是单独编译,从另一个 TU 传入的参数,加上通过其中一个清洗参数通常的技巧,使其失去其恒定性(作为最终防御,例如,面对 LTO)。 @PeterCordes - 是的,我已经做了那个测试,一些discussion here。我认为它支持优化发生在 L1L2 边界或 L2 的想法。也就是说,如果您建议的测试中的非零值永远不会逃脱 L1,那么优化就会发生。当它从 L1 逃逸到 L2 时,它就停止了。

以上是关于什么特别地将 x86 缓存行标记为脏 - 任何写入,还是需要显式更改?的主要内容,如果未能解决你的问题,请参考以下文章

formbuilder 的 patchvalue 或 setvalue 不会将字段标记为脏或已触及

如何在 Drupal 7 Search API 模块中将内容实体标记为脏

程序的载入和运行——《x86汇编语言:从实模式到保护模式》读书笔记25

缓存 和 数据库 数据一致性

在 x64 上再次读取之前在未缓存的地址写入完整的缓存行

从 Windows CLI 刷新磁盘写入缓存