现代英特尔x86 CPU如何实现对存储指令的全序?

4

x86的TSO内存模型保证了所有存储操作的全序。我的问题是是否有人知道这是如何实现的。

我很清楚所有4个栅栏是如何实现的,所以我可以解释本地顺序是如何保持的。但是这4个栅栏只会给出程序顺序;它不会给你TSO(我知道TSO允许较旧的存储跳到较新的加载器前面,因此只需要3个栅栏)。

单个地址上所有内存操作的总序是一致性的责任。但我想知道英特尔(特别是Skylake)如何实现多个地址上的存储总序。


1
你看过这个视频吗?https://www.youtube.com/watch?v=WUfvvFD5tAA 或许它很相关...不过我也不确定,只是想帮忙而已^^ - SSpoke
这是一款不错的手表。但它并没有回答我的问题。我知道Intel X86提供的合同,但我想知道它是如何实际实现的。 - pveentjer
我想我在beeonrope的帮助下知道它是如何工作的。我会尽快写下解释并把我的想法记录下来。 - pveentjer
1个回答

13
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重新排序,因为我们知道需要特殊处理缓存未命中负载以获得可接受的性能。如何在顺序提交时进行负载->存储重排序?

2
请注意,与单位置情况不同,您可以同时将数据存储到不同的位置。 您不需要串行化,只需要确保没有线程会“不一致”。并不要求任何真实或假设的读者能够看到一个状态,在该状态中一个存储已经发生而另一个尚未发生。如果两个写入核在同一时钟周期内提交(假设跨核心同步时钟...),则对于这些行的任何共享请求顺序都将同时看到两个存储器的状态。如果它们不是真正同时的,则可能先看到其中一个。 - Peter Cordes
2
@pveentjer:一致性意味着读取永远不会观察到旧值。因此,写入传播到其他核心的“延迟”没有窗口可供重新排序。MESI一致性提供与零延迟互连或所有核心直接共享单个多端口缓存相同的正确性/排序行为。显然,这不能重新排序对共享内存的任何访问,因此任何重新排序都限于单个核心内部。我不知道你为什么说缓存仅保证单个地址的分离总顺序。 - Peter Cordes
2
@pveentjer:不引入任何重新排序意味着提交到缓存的真实顺序成为存储的总顺序。一旦存储提交到L1d的一行,它就是由MESI维护的一致状态的一部分。因此,存在一个总存储顺序。使用有序加载以已知顺序观察共享状态的读取器可以观察到该顺序。也许我在这里没有使用正确的正式术语(一致性与一致性),但请考虑缓存实际上是如何运作的,而不仅仅是像一致性这样的术语的正式定义。 - Peter Cordes
2
@pveentjer:你的意思是要分离行,我想。没有什么可以证明一个事件发生在另一个事件之前,因此没有观察者可以对顺序产生异议。这满足x86 TSO。假设的总顺序(理论上至少存在)如果你不喜欢事件在其中真正同时发生,可以选择任何一种顺序。也许我的回答应该说“核心不能对总顺序产生分歧”,而不是将其表述为核心能够确定总顺序实际上是什么。 - Peter Cordes
2
@BeeOnRope:IRIW 中的读取器必须执行获取负载,否则它与本地负载重排序无法区分。接收这些传入行的核心必须在第一行到达后检查第二个加载结果是否仍然有效,如果第二个加载的行先到达(即阻止 LoadLoad 重排序)。也许高效地执行此操作需要来自互连/缓存一致性机制的一些帮助。也许我已经表明 IRIW 被排除在支持 acq/rel 排序的任何系统之外,这确实需要互连避免重新排序,只留给 CPU 核心。 - Peter Cordes
显示剩余19条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接