x86 TSO内存模型上的"store buffer"石蕊测试名称的原因



我一直在研究内存模型,并看到了这一点(引用自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:这是许多文章中提到的存储缓冲区试金石。他们都说,由于存储缓冲区的存在,在TSO上r1和r2都可能为零。他们似乎假设所有的存储和加载都是按顺序执行的,但结果是r1和r2都为零。这后来得出结论;存储/加载重新排序可能发生";,作为";存储缓冲区存在的结果";。

  • 事实2:然而,我们知道OoO执行也可以对两个线程中的存储和加载进行重新排序。从这个意义上说,无论存储缓冲区是什么,只要所有四条指令都退出而不看到对方对x或y的无效,这种重新排序可能会导致r1和r2都为零;存储/加载重新排序可能发生";,只是因为";它们被无序地执行";。(我可能错了,因为这是我所知道的关于投机和OoO执行的最好的一次。)

我想知道这两个事实是如何收敛的(假设我碰巧对两者都是正确的):存储缓冲区或OoO执行是";存储/装载重新排序";,或者两者都是?

换句话说:假设我在x86机器上观察到了这个石蕊测试,是因为存储缓冲区,还是OoO执行?或者有可能知道哪一个?


EDIT:事实上,我的主要困惑是各种文献中以下几点之间的因果关系不清楚:

  1. OoO执行会导致内存重新排序
  2. 存储/加载重新排序是由存储缓冲区引起的,并通过石蕊测试(因此被命名为"存储缓冲区")来证明
  3. 一些具有与存储缓冲区石蕊测试完全相同的指令的程序被用作可观察的OoO执行示例,就像本文一样https://preshing.com/20120515/memory-reordering-caught-in-the-act确实如此

1+2似乎意味着存储缓冲区是原因,OoO执行是结果。3+1似乎意味着OoO执行是原因,而内存重新排序是结果。我再也说不出是什么原因造成的了。而正是这个石蕊测试坐落在这个谜的中间。

调用StoreLoad重新排序存储缓冲区的效果是有意义的,因为防止这种情况的方法是使用mfencelock指令,在允许以后的加载从缓存读取之前耗尽存储缓冲区。仅仅序列化执行(使用lfence)是不够的,因为存储缓冲区仍然存在。注意,即使是sfence ; lfence也是不够的。

此外,我假设P5Pentium(按顺序双问题)有一个存储缓冲区,因此基于它的SMP系统可能会产生这种影响,在这种情况下,这肯定是由于存储缓冲区。IDK在PPro存在之前的早期就已经对x86内存模型进行了详细的记录,但在此之前进行的任何石蕊测试的命名都可能很好地反映出有序的假设。(以命名可能包括仍然存在的订单系统。)


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

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

(退休是为了支持"精确异常"。否则,混乱随之而来,发现错误预测可能意味着回滚其他内核的状态,即一个不合理的设计。推测执行的CPU分支是否包含访问RAM的操作码?这解释了为什么存储缓冲区对于OoO执行器来说是必要的。)

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

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


一个正常的无序exec将所有指令视为推测性指令,只有当它们从ROB中退出时才成为非推测性指令。ROB是按照程序顺序执行的,以支持精确的异常。(请参阅无序执行与推测执行,以了解在英特尔崩溃漏洞的背景下对这一想法的更深入探索。)

使用OoO exec但没有存储缓冲区的假设设计是不可能的它会执行得很糟糕,每个存储都必须等待所有以前的指令明确无故障或预测错误/推测错误,然后才能允许执行存储。

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

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

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

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

P6中的无序exec没有引入任何新的内存重新排序,因为这可能会破坏现有的多线程二进制文件。(我想,当时主要是操作系统内核!)这就是为什么早期加载必须是推测性的。x86存在的主要原因是向后兼容;那时候不是表演之王。


Re:如果你是这个意思的话,为什么会有这个石蕊测试
显然是为了强调x86上可能发生的事情。

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

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


假设我在x86机器上观察到了这个石蕊测试

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

相关内容

  • 没有找到相关文章

最新更新