【问题标题】:Reason for the name of the "store buffer" litmus test on x86 TSO memory modelx86 TSO 内存模型上的“存储缓冲区”石蕊测试名称的原因
【发布时间】:2021-11-05 18:08:30
【问题描述】:

我一直在研究内存模型,看到了这个(引自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!

  • 事实 1:这是许多文章中提到的存储缓冲区试金石。他们都说 r1 和 r2 都为零可能发生在 TSO 上,因为存在存储缓冲区。他们似乎假设所有的存储和加载都是按顺序执行的,但结果是 r1 和 r2 都为零。这后来得出结论,“可能发生存储/加载重新排序”,作为“存储缓冲区存在的后果”。

  • 事实 2:但是我们知道 OoO 执行也可以重新排序两个线程中的存储和加载。从这个意义上说,无论存储缓冲区如何,只要所有四个指令都退出而没有看到彼此对 x 或 y 的无效,这种重新排序可能导致 r1 和 r2 都为零。在我看来,“可能会发生存储/加载重新排序”,只是因为“它们是无序执行的”。 (我可能对此非常错误,因为这是我所知道的关于推测和 OoO 执行的最好的。)

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

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


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

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

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

【问题讨论】:

  • 标题问题的目的是为了说明为什么这样称呼它,对吧?不是为什么使用该代码的 StoreLoad 重新排序试金石测试很有用。 (它很少相关;通常 acq/rel 同步对于线程间通信就足够了,但作为指定允许哪些重新排序效果的一种方式,它是必不可少的。preshing.com/20120515/memory-reordering-caught-in-the-act 是这个 x86 试金石测试的实际实现)。我编辑了您的标题以澄清这一点,因为这是我正在写的答案的方向。
  • 重新整理思路后,我在问题正文中进行了一些编辑。但是,是的,您对图块的编辑仍然是我正在寻找的方向,加上您在下面的广泛回答解决了我所有的困惑。所以编辑肯定是 LGTM。
  • 你写的 Preshing 的测试是用来检测“OoO 执行”的。他说的不完全是这样。 “因此,它可能会像指令按以下顺序执行一样结束:”。他确实使用了“执行”这个词,但我认为他的真正意思是,就好像程序顺序相反。或者就像在串行机器上以其他顺序执行一样。他不是试图暗示“OoO exec”是创建重新排序的特定机制。
  • 请参阅preshing.com/20120710/…,他解释了“管道”如何延迟存储,以及(在底部)真正的 CPU 具有存储缓冲区,因此类比仅限于此。
  • cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf 这是和你一样的一群人写的,它在介绍中列出。 www0.cs.ucl.ac.uk/staff/j.alglave/papers/tacas11.pdf 这个也是由不同的人设计的。

标签: x86 cpu-architecture memory-barriers memory-model


【解决方案1】:

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

我还假设 P5 Pentium(按顺序双发)有一个存储缓冲区,因此基于它的 SMP 系统可能会产生这种效果,在这种情况下,肯定是由于存储缓冲区造成的。 IDK 在 PPro 甚至还没有出现之前的早期,x86 内存模型被记录得多么彻底,但在此之前完成的任何石蕊测试的命名都可能很好地反映了有序假设。 (并且命名可能包括仍然存在的有序系统。)


您无法判断是哪种影响导致 StoreLoad 重新排序。 在真正的 x86 CPU(带有存储缓冲区)上,可能会在存储写入其地址和数据之前执行稍后的加载到存储缓冲区。

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

(退役是为了支持“精确的例外”。否则,会出现混乱,发现错误预测可能意味着回滚其他内核的状态,即不合理的设计。 Can a speculatively executed CPU branch contain opcodes that access RAM? 解释了为什么 OoO exec 通常需要存储缓冲区。)

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

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


正常的乱序 exec 将 所有 指令视为推测性的,只有当它们从 ROB 中退出时才变为非推测性的,这是为了支持精确的异常。 (请参阅Out-of-order execution vs. speculative execution,在英特尔的 Meltdown 漏洞背景下更深入地探索该想法。)

一个带有 OoO exec 但没有存储缓冲区的假设设计是可能的。 它会执行得很糟糕,每个存储都必须等待所有先前的指令被确定为没有错误或被错误预测/ 在允许 store 执行之前错误推测。

与说它们需要已经执行完全一样(例如,仅执行早期存储的存储地址 uop 就足以知道它是无故障的,或者对于负载进行 TLB/页表检查,即使数据尚未到达,它也会告诉您它是非故障的)。但是,每条分支指令都需要已经执行(并且已知正确),就像 div 这样的每条 ALU 指令一样。

这样的 CPU 也不需要在存储之前阻止以后的加载运行。推测性负载没有架构效果/可见性,因此如果其他内核看到由于错误推测而导致的缓存行共享请求,则可以。 (在语义允许的内存区域上,例如普通的 WB 回写可缓存内存)。这就是硬件预取和推测执行在普通 CPU 中工作的原因。

内存模型甚至允许 StoreLoad 排序,因此我们不推测内存排序,仅推测存储(和其他中间指令)不会出错。这又是好的;推测性负载总是好的,我们不能让其他内核看到推测性存储。 (因此,如果我们没有存储缓冲区或其他机制,我们根本无法做到这一点。)

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

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


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

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

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


假设我在 x86 机器上以某种方式观察到这个试金石,

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

【讨论】:

  • 另一个很好的答案!这也很好地解释了为什么 x86 具有它所做的内存模型(所有加载获取,所有存储释放,但仍然允许 StoreLoad 重新排序):因为这是具有存储缓冲区的有序机器的自然模型,即P5实际上是什么。直到现在我才明白这一点。
  • 这个出色的答案解决了我所有的困惑。这很有帮助,我真的很感激。
  • 顺便说一句,我将程序放在preshing.com/20120515/memory-reordering-caught-in-the-act 中,并在存储和负载之间放置了lfence。我仍然观察到重新排序,但比不使用栅栏更罕见。我相信这将成为真正的存储缓冲区试金石。非常感谢你,彼得!
  • @zanmato:很酷,是的。 lfence 足够慢,除了阻塞 OoO exec 之外,它还为 store 提供了一些时间来退出和提交,但是在缓存未命中时,store 仍然需要等待足够长的时间。
  • @NateEldredge:请注意,有序流水线仍然可以加载记分牌,只有在您实际尝试读取尚未准备好的加载结果寄存器时才会停止。 (因此,hit-under-miss 会产生 LoadLoad 重新排序。)此外,允许存储缓冲区无序提交可能会产生 StoreStore 重新排序(例如,如果缓冲区的头部不是独占拥有的,请检查旧条目以查看它们是否可以犯罪)。如果您不禁止它,LoadStore 可以通过缓存未命中加载和快速提交的存储来实现。 Cortex-A53 等典型的有序 ARM CPU 可以完成所有这些事情。
猜你喜欢
  • 2022-01-09
  • 2021-12-23
  • 2011-10-11
  • 2012-12-09
  • 1970-01-01
  • 1970-01-01
  • 2019-07-19
  • 1970-01-01
  • 2023-04-01
相关资源
最近更新 更多