首页 > 解决方案 > 全局不可见的加载指令

问题描述

由于存储加载转发,某些加载指令是否永远不会全局可见?换句话说,如果加载指令从存储缓冲区中获取其值,则它永远不必从缓存中读取。
正如通常所说的,从 L1D 缓存中读取的负载是全局可见的,不从 L1D 读取的负载应该使其全局不可见。

标签: x86cpu-architecturecpu-cachememory-barriers

解决方案


负载的全局可见性概念很棘手,因为负载不会修改内存的全局状态,其他线程也无法直接观察到它。

但是一旦在乱序/推测执行之后尘埃落定,我们就可以判断如果线程将负载存储在某个地方或基于它的分支,负载会得到什么值。线程的这种可观察到的行为很重要。(或者我们可以用调试器观察它,和/或只是推断负载可能会看到什么值,如果实验很困难。)


至少在像 x86 这样的强排序 CPU 上,所有 CPU 都可以就存储的总顺序达成一致,变得全局可见,更新单个一致+一致的缓存+内存状态。在不允许StoreStore 重新排序的 x86 上,此 TSO(总存储顺序)与每个线程的程序顺序一致。(即总顺序是每个线程的程序顺序的一些交错)。SPARC TSO 也是这种强排序。

(正确观察您自己的商店相对于其他商店的全局mfence顺序需要或类似:否则商店转发意味着您可以立即看到您自己的商店,在它们对其他核心可见之前。x86 TSO 基本上是程序顺序加商店-转发。)

(对于绕过缓存的存储,全局可见性是当它们从私有写入组合缓冲区刷新到 DRAM 时。英特尔行填充缓冲区或任何等效的私有写入组合机制,其中存储数据仍然对其他 CPU 不可见是有效的一部分用于我们重新排序目的的存储缓冲区。)

在弱排序的 ISA 上,线程 A 和 B 可能不会就线程 C 和 D 完成的存储 X 和 Y 的顺序达成一致,即使读取线程使用获取加载来确保它们自己的加载不会重新排序。即,可能根本没有全局的商店顺序,更不用说它与程序顺序不同了。

IBM POWER ISA 就是那么弱,C++11 内存模型也是如此(在不同线程中对不同位置的两次原子写入是否总是被其他线程以相同的顺序看到?)。但是 POWER 上的实践机制是(退休的,也就是毕业的)存储在通过提交 L1d 缓存成为全局可见之前对其他一些内核可见。即使在 POWER 系统中,缓存本身也确实是连贯的,就像所有普通 CPU 一样,并且允许通过屏障恢复顺序一致性。这些多阶效应仅由于SMT(一个物理 CPU 上的多个逻辑 CPU)提供了一种无需通过缓存即可查看来自其他逻辑内核的存储的方法而发生。

(一种可能的机制是让其他逻辑线程在提交到 L1d 之前就从存储缓冲区窥探非推测性存储,仅将尚未退休的存储保持为逻辑线程私有。这可以稍微减少线程间延迟。x86不能这样做,因为它会破坏强内存模型;当两个线程在一个内核上处于活动状态时,英特尔的 HT 静态分区存储缓冲区。但正如@BeeOnRope 评论的那样,允许重新排序的抽象模型可能是更好的方法关于正确性的推理。仅仅因为您无法想到导致重新排序的硬件机制并不意味着它不会发生。

但是,如果不使用屏障或释放存储,弱排序的 ISA 不如 POWER(在实践中和/或在纸面上)仍然在每个内核的本地存储缓冲区中重新排序。在许多 CPU 上,所有存储都有一个全局顺序,但这不是程序顺序的某种交错。OoO CPU 必须跟踪内存顺序,因此单个线程不需要屏障来按顺序查看其自己的存储,但是允许存储从存储缓冲区提交到 L1d 的程序顺序肯定可以提高吞吐量(特别是如果有多个存储等待同一行,但程序顺序会将该行从每个存储之间的集合关联缓存中逐出。例如,令人讨厌的直方图访问模式。)


让我们做一个关于负载数据来自哪里的思想实验

以上仍然只是关于商店可见性,而不是负载。 我们能否将每次加载所看到的值解释为在某个时刻从全局内存/缓存中读取(忽略任何加载排序规则)?

如果是这样,那么可以通过将所有线程的所有存储和加载按某种组合顺序来解释所有加载结果,读取和写入一致的全局内存状态。

事实证明,不,我们不能,存储缓冲区打破了这一点:部分存储到加载转发给了我们一个反例(例如在 x86 上)。在存储变得全局可见之前,窄存储后接宽负载可以将存储缓冲区中的数据与 L1d 缓存中的数据合并。 真正的 x86 CPU 确实做到了这一点,我们有真实的实验来证明这一点。

如果您只查看完整存储转发,其中加载仅从存储缓冲区中的一个存储获取其数据,您可能会争辩说存储缓冲区延迟了加载。即负载出现在全局总加载-存储顺序中,紧跟在使该值全局可见的存储之后。

(这个全局的总加载-存储顺序并不是试图创建一个替代的内存排序模型;它无法描述 x86 的实际加载排序规则。)


部分存储转发暴露了加载数据并不总是来自全局一致缓存域的事实。

如果来自另一个核心的存储更改了周围的字节,则原子范围的负载可以读取在全局一致状态下从未存在且永远不会存在的值。

请参阅我对x86 是否可以重新排序具有更广泛负载的狭窄商店的回答,以完全包含它?,以及亚历克斯对实验证明可能发生这种重新排序的回答,这使得该问题中提出的锁定方案无效。 存储然后从同一地址重新加载不是 StoreLoad 内存屏障

有些人(例如 Linus Torvalds)通过说存储缓冲区不连贯来描述这一点。(Linus 正在回复另一个独立发明了同样无效锁定想法的人。)

另一个涉及存储缓冲区和一致性的问答:如何有效地并行设置位向量的位?. 您可以执行一些非原子 OR 来设置位,然后返回并检查由于与其他线程冲突而错过的更新。但是您需要一个 StoreLoad 屏障(例如 x86 lock or)以确保您在重新加载时不会只看到自己的商店。


建议的定义:负载在读取其数据时变得全局可见。通常来自 L1d,但存储缓冲区或 MMIO 或不可缓存的内存是其他可能的来源。

此定义与 x86 手册一致,即负载不会与其他负载一起重新排序。即它们从本地内核的内存视图加载(按程序顺序)。

加载本身可以变得全局可见,而与任何其他线程是否可以从该地址加载该值无关。

尽管根本不谈论可缓存负载的“全局可见性”可能更有意义,因为它们是某个地方提取数据,而不是做任何具有可见效果的事情。只有不可缓存的负载(例如来自 MMIO 区域)才应该被视为可见的副作用。

(在 x86 上,不可缓存的存储和加载是非常有序的,因此我认为将存储转发到不可缓存的存储是不可能的。除非存储是通过与 UC 加载访问相同的物理页面的 WB 映射完成的。)


推荐阅读