x86 TSO内存模型基本上相当于程序顺序加上具有存储转发的存储缓冲区。大多数保证在理论上对硬件来说都相当容易实现,只需具有存储缓冲区和一致的共享内存即可。存储缓冲区隔离了乱序执行和按顺序提交要求(以及缓存未命中存储),
使得可能对存储进行推测性执行,并且(通过存储-加载转发)在这些存储仍然是推测性的时候重新加载它们。
所有核心都可以就所有存储发生的
总顺序达成一致。或者更准确地说,核心不能在它们实际观察到的总顺序的任何部分上产生不一致。对于两个不同行的存储,它们可以真正同时发生,因此任何观察都与假设的总顺序中的任一顺序兼容。
如果使一个存储对任何其他核心可见的唯一方法是使其对所有核心同时可见,则会自动发生这种情况。即通过提交到一致的L1d。这使得IRIW重排序变为不可能。(MESI确保存储不能提交到L1d,除非它由该核心独占:没有其他核心有有效副本。)(观察其自身存储的核心需要完整的屏障,否则它将通过存储转发观察其自身存储,而不是全局总顺序。典型的IRIW litmus测试考虑了4个总线程,因此没有本地重新加载。)
实际上,任何硬件都很少
不具有这种属性。一些
POWER CPU 可以在同一物理核上的SMT线程之间进行存储转发,从而使2个读取器对2个写入器的存储顺序产生不同意见(IRIW重排序)。即使x86 CPU也经常具有SMT(例如Intel的HyperThreading),内存模型要求它们不在逻辑核之间进行存储转发。这没问题;它们静态分区存储缓冲区。
在一个具有HT的核心上执行的线程之间用于数据交换的是什么?。以及
超级兄弟和非超级兄弟之间的生产者-消费者共享内存位置的延迟和吞吐量成本是多少?用于实验测试。
唯一发生的重新排序是在每个CPU核心内部进行的,仅限于对全局一致共享状态的访问。这就是为什么本地内存屏障只会使该核心等待某些事情发生(例如存储缓冲区耗尽),可以在x86 TSO之上恢复顺序一致性。同样适用于更弱的内存模型:基于MESI一致性的本地重新排序。
其余保证适用于每个(逻辑)CPU核心。关于如何在核心之间创建同步的
Q&A。
- 存储按程序顺序变得可见:从存储缓冲区到L1d缓存的有序提交。此意味着缓存未命中存储必须阻止存储缓冲区,不允许年轻的存储提交。请参阅
为什么退役后的RFO不会破坏内存排序?,了解此简单的心理模型以及Skylake可能实际执行的一些详细信息(将存储丢失的数据提交到LFB,同时等待缓存行到达)。
- 加载不会与后续存储器重新排序:很容易:要求加载在退役之前完全完成(已从L1d缓存中获取数据)。由于退役是有序的,并且存储器不能提交到L1d直到它退役(变为非规范性),因此我们免费获得LoadStore排序。
- 加载按程序顺序从一致缓存(内存)中获取数据。这是最难的部分:加载在执行时访问全局状态(缓存),不像存储器,存储器缓冲区可以吸收OoO exec和有序提交之间的不匹配。实际上,英特尔CPU会大胆地猜测,现在存在的缓存行将在允许进行加载的架构时间后仍然存在(在早期加载执行后)。如果情况不是这样,就会清除流水线(内存顺序误判)。这里有一个性能计数器事件。
实际上,为了追求更高的性能或者对于推测性的早期加载,所有事情都可能变得更加复杂。
(在C++术语中,这至少与
acq_rel
一样强,但也涵盖了在C++中可能是UB的行为。例如,加载部分重叠最近存储到另一个线程可能正在读写的位置,允许此核心加载一个对于其他线程来说从未出现或将出现在内存中的值。
全局不可见的加载指令)。
相关问答:
注1:
一些弱序执行的OoO CPU可以进行LoadStore重新排序,可能是通过让负载从ROB中退役,只要负载检查权限并请求缓存行(对于未命中),即使数据实际上还没有到达。需要一些单独跟踪寄存器尚未准备好的情况,而不是通常的指令调度器。
在顺序流水线上,实际上更容易理解LoadStore重新排序,因为我们知道需要特殊处理缓存未命中负载以获得可接受的性能。
如何在顺序提交时进行负载->存储重排序?