"store buffer" litmus test on x86 TSO 内存模型名称的原因

Reason for the name of the "store buffer" litmus test on x86 TSO memory model

我一直在研究内存模型并看到了这个(引自https://research.swtch.com/hwmm):

Litmus Test: Write Queue (also called Store Buffer)
Can this program see r1 = 0, r2 = 0?
// Thread 1           // Thread 2
x = 1                 y = 1
r1 = y                r2 = x
On sequentially consistent hardware: no.
On x86 (or other TSO): yes!

我想知道这两个事实是如何融合的(假设我恰好对两者都是正确的):存储缓冲区或 OoO 执行是“store/load 重新排序”的原因,还是两者都是?

换句话说:假设我以某种方式在 x86 机器上观察到这个试金石,是因为存储缓冲区还是 OoO 执行?或者甚至有可能知道是哪一个?


编辑:实际上我的主要困惑是各种文献中以下几点之间的因果关系不明确:

  1. OoO 执行会导致内存重新排序;
  2. Store/load 重新排序是由存储缓冲区引起的,并由石蕊测试证明(因此命名为“存储缓冲区”);
  3. 一些与存储缓冲区石蕊测试具有完全相同指令的程序被用作可观察到的 OoO 执行示例,正如本文 https://preshing.com/20120515/memory-reordering-caught-in-the-act 所做的那样。

1 + 2 似乎暗示存储缓冲区是原因,OoO 执行是结果。 3 + 1 似乎暗示 OoO 执行是原因,内存重新排序是结果。我再也分不清是哪个原因造成的了。正是这个谜团中间的试金石。

调用 StoreLoad 重新排序存储缓冲区的效果是有意义的,因为防止它的方法是使用 mfencelocked 指令,在以后加载之前耗尽存储缓冲区允许从缓存中读取。仅序列化 execution(使用 lfence)是不够的,因为存储缓冲区仍然存在。请注意 even sfence ; lfence isn't sufficient.

另外我假设P5 Pentium (in-order dual-issue)有一个store buffer,所以基于它的SMP系统可能会有这种效果,在这种情况下肯定是由于store buffer。我知道 x86 内存模型在 PPro 出现之前的早期就被记录得多么彻底,但是在此之前完成的石蕊测试的任何命名都可能很好地反映了顺序假设。 (并且命名可能包括仍然存在的有序系统。)


您无法判断是什么影响导致了 StoreLoad 重新排序。 在真正的 x86 CPU(带有存储缓冲区)上,稍后的加载可以在之前执行商店甚至已将其地址和数据写入商店缓冲区。

是的,执行 存储只是意味着写入存储缓冲区;它不能从 SB 提交到 L1d 缓存,并且在存储从 ROB 退出之前对其他内核可见(因此被认为是非推测性的)。

(退役是为了支持“精确异常”。否则,混乱随之而来,发现错误预测可能意味着回滚 other 核心的状态,即设计不合理。 解释了为什么一般情况下 OoO exec 需要存储缓冲区。)

我想不出加载 uop 在存储数据 and/or 存储地址 uops 之前或存储退休之前执行的任何可检测的副作用,而不是在商店退休之后但之前它提交给 L1d 缓存。

你可以通过在存储和加载之间放置一个lfence来强制后一种情况,所以重新排序肯定是由存储缓冲区引起的。(更强像 mfence 这样的屏障,一个锁定指令,或者像 cpuid 这样的序列化指令,都将通过在后面的加载可以执行之前耗尽存储缓冲区来完全阻止重新排序。作为一个实现细节,甚至可以在它发出之前。)


正常的乱序执行将 所有 指令视为推测性的,只有当它们从 ROB 退出时才变为非推测性的,这是按程序顺序完成的,以支持精确的异常。 (在英特尔的 Meltdown 漏洞的背景下,请参阅 以更深入地探讨该想法。)

具有 OoO exec 但没有存储缓冲区的假设设计是可能的。 它会执行得非常糟糕,每个存储都必须等待所有先前的指令被明确知道不在允许存储执行之前发生故障或以其他方式被错误预测/错误推测。

不是 与说它们需要已经执行的完全相同(例如,仅执行较早存储的存储地址 uop 就足以知道它没有故障,或者对于执行 TLB/page-table 检查的负载会告诉你它没有故障,即使数据还没有到达)。但是,每个分支指令都需要已经执行(并且已知正确),就像每个 ALU 指令一样 div 可以。

这样的 CPU 也不需要在存储之前停止来自 运行 的稍后加载。推测负载没有架构效果/可见性,因此如果其他核心看到缓存行的共享请求是错误推测的结果,那也没关系。 (在语义允许的内存区域上,例如普通的 WB 回写可缓存内存)。这就是硬件预取和推测执行在正常 CPUs 中工作的原因。

内存模型甚至允许 StoreLoad 排序,所以我们不是在猜测内存排序,只是在存储(和其他中间指令)上没有错误。这又很好;推测性负载总是好的,我们不能让其他核心看到的是推测性存储。 (因此,如果我们没有存储缓冲区或其他机制,我们根本无法执行这些操作。)

(有趣的事实:真正的 x86 CPUs do 通过相互乱序加载来推测内存排序,这取决于地址是否准备好,和缓存 hit/miss。这可能导致内存顺序错误推测“机器清除”又名管道核弹(machine_clears.memory_ordering perf 事件)如果另一个核心在实际读取和缓存行之间写入缓存行最早的内存模型说我们可以。或者即使我们猜测加载是否会重新加载最近存储的东西是错误的;地址尚未准备好时的内存消歧涉及动态预测,因此您可以使用 machine_clears.memory_ordering单线程代码。)

P6 中的乱序执行没有引入任何新的内存重新排序类型,因为这可能会破坏现有的多线程二进制文件。 (我猜当时大部分只有 OS 内核!)这就是为什么早期加载必须是推测性的(如果完成的话)。 x86 存在的主要原因是向后兼容;那时候还不是性能王


回复:为什么这个试金石存在,如果那是你的意思?
显然是为了强调 x86 上可能发生的事情。

StoreLoad 重新排序重要吗?通常这不是问题;获取/释放同步对于大多数关于缓冲区准备好读取的线程间通信来说已经足够了,或者更普遍的是一个无锁队列。或者实现互斥体。 ISO C++只保证mutexes lock/unlock是acquire和release操作,不是seq_cst.

很少有算法依赖于在稍后加载之前耗尽存储缓冲区。


Say I somehow observed this litmus test on an x86 machine,

可验证此重新排序在真实 x86 CPUs 上的现实生活中是否可行的完整工作程序:https://preshing.com/20120515/memory-reordering-caught-in-the-act/。 (Preshing 关于内存排序的其余文章也非常出色。非常适合通过无锁操作从概念上理解线程间通信。)