zan*_*ato 4 x86 cpu-architecture memory-model memory-barriers
我一直在研究内存模型并看到了这个(引用自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!
Run Code Online (Sandbox Code Playgroud)
事实1:这是很多文章中提到的存储缓冲区试金石。他们都说,由于存储缓冲区的存在,TSO 上可能会发生 r1 和 r2 为零的情况。他们似乎假设所有存储和加载都按顺序执行,但结果是 r1 和 r2 都为零。后来得出的结论是“存储/加载重新排序可能会发生”,作为“存储缓冲区存在的后果”。
事实 2:但是我们知道 OoO 执行也可以重新排序两个线程中的存储和加载。从这个意义上说,无论存储缓冲区如何,只要所有四个指令都在没有看到彼此对 x 或 y 无效的情况下退出,这种重新排序就可能导致 r1 和 r2 都为零。在我看来,“存储/加载重新排序可能会发生”,只是因为“它们是无序执行的”。(我对此可能是非常错误的,因为这是我所知道的最好的猜测和 OoO 执行。)
我想知道这两个事实是如何融合的(假设我碰巧对这两个事实都是正确的):存储缓冲区或 OoO 执行是“存储/加载重新排序”的原因,还是两者都是?
换句话说:假设我以某种方式在 x86 机器上观察到了这个试金石,是因为存储缓冲区还是 OoO 执行?或者甚至有可能知道是哪一个吗?
编辑:实际上,我主要的困惑是各种文献中以下几点之间的因果关系不明确:
1 + 2 似乎暗示存储缓冲区是原因,OoO 执行是结果。3 + 1 似乎暗示 OoO 执行是原因,内存重新排序是结果。我再也无法分辨哪个原因导致了哪个。这就是这个谜团中间的试金石。
调用 StoreLoad 重新排序存储缓冲区的效果是有意义的,因为阻止它的方法是使用mfence
或lock
ed 指令,该指令会在允许从缓存读取后续加载之前耗尽存储缓冲区。仅仅序列化执行(使用lfence
)是不够的,因为存储缓冲区仍然存在。请注意,even 还sfence ; lfence
不够。
另外我假设P5 Pentium(有序双发)有存储缓冲区,因此基于它的SMP系统可能会产生这种效果,在这种情况下肯定是由于存储缓冲区所致。我不知道在 PPro 存在之前的早期,x86 内存模型的记录有多彻底,但在此之前完成的石蕊测试的任何命名都可能很好地反映了有序的假设。(命名可能包括仍然存在的有序系统。)
您无法判断哪个效果导致了 StoreLoad 重新排序。 在真正的 x86 CPU(带有存储缓冲区)上,可以在存储将其地址和数据写入存储缓冲区之前执行稍后的加载。
是的,执行存储仅意味着写入存储缓冲区;在存储从 ROB 中退出之前,它无法从 SB 提交到 L1d 缓存并对其他核心可见(因此已知是非推测性的)。
(退役的发生是为了支持“精确异常”。否则,混乱就会随之而来,发现错误预测可能意味着回滚其他核心的状态,即不理智的设计。推测执行的 CPU 分支是否可以包含访问的操作码RAM?解释了为什么 OoO exec 通常需要存储缓冲区。)
我想不出在存储数据和/或存储地址微指令之前或在存储退役之前执行的加载微指令有任何可检测到的副作用,而不是在存储退役之后但在提交到 L1d 缓存之前。
lfence
您可以通过在存储和加载之间放置一个来强制后一种情况,因此重新排序肯定是由存储缓冲区引起的。 (像 mfence、锁定指令或序列化指令这样的更强屏障cpuid
,都将通过在稍后的加载执行之前耗尽存储缓冲区来完全阻止重新排序。作为实现细节,甚至在它可以发出之前。)
正常的乱序执行程序将所有指令视为推测性的,只有当它们从 ROB 退出时才变为非推测性的,这是按程序顺序完成的以支持精确的异常。(请参阅乱序执行与推测执行,以在英特尔的 Meltdown 漏洞的背景下更深入地探讨该想法。)
具有 OoO exec 但没有存储缓冲区的假设设计是可能的。 它的执行效果会很糟糕,每个存储都必须等待所有先前的指令被明确地知道没有错误或以其他方式被错误预测/错误推测,然后才能允许存储执行。
不过,这与说它们需要已经执行并不完全相同(例如,仅执行较早存储的存储地址 uop 就足以知道它没有故障,或者对于执行 TLB/页面的加载-表检查会告诉您它没有故障,即使数据尚未到达)。然而,每个分支指令都需要已经执行(并且已知正确),就像每个类似的 ALU 指令一样div
。
这样的 CPU 也不需要在存储之前停止运行后面的加载。推测性负载没有架构效果/可见性,因此如果其他核心看到对缓存行的共享请求(这是错误推测的结果),那也没关系。(在语义允许的内存区域上,例如正常的 WB 回写可缓存内存)。这就是硬件预取和推测执行在普通 CPU 中工作的原因。
内存模型甚至允许 StoreLoad 排序,因此我们不会推测内存排序,仅推测存储(和其他干预指令)不会出错。这又没问题;推测负载总是好的,我们不能让其他核心看到推测存储。(所以如果我们没有存储缓冲区或其他机制,我们根本无法做到这一点。)
(有趣的事实:真正的 x86 CPU确实会通过彼此乱序加载来推测内存顺序,具体取决于地址是否准备好以及缓存命中/未命中。这可能会导致内存顺序错误推测“机器清除”machine_clears.memory_ordering
如果另一个核心在实际读取缓存行和内存模型所说的最早可以读取的缓存行之间写入数据,则又称为管道核武器(性能事件)。或者即使我们猜测加载是否会重新加载最近存储的内容是错误的;地址尚未准备好时的内存消歧涉及动态预测,因此您可以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 的其他关于内存排序的文章也非常出色。非常适合通过无锁操作获得对线程间通信的概念性理解。)
归档时间: |
|
查看次数: |
524 次 |
最近记录: |