x86 CPU是否重新排序指令?

Ste*_*eve 6 x86 assembly multithreading cpu-architecture memory-barriers

我已经读过一些CPU重新排序指令,但这对于单线程程序来说不是问题(指令仍会在单线程程序中重新排序,但看起来好像指令是按顺序执行的),这只是一个问题用于多线程程序.

为了解决指令重新排序的问题,我们可以在代码中的适当位置插入内存屏障.

但x86 CPU是否重新排序指令?如果没有,那么就没有必要使用内存屏障了吧?

Bee*_*ope 16

重新排序

是的,来自英特尔和AMD的所有现代x86芯片都在一个窗口上积极地重新排序指令,该窗口在两个制造商的最新CPU上大约200条指令深度(即,一条新指令可以执行,而过去的旧指令超过200条指令仍然是"等候).这通常对于单个线程都是不可见的,因为CPU仍然通过遵循依赖性来维持当前线程的串行执行1的错觉,因此从当前执行线程的角度来看,如果指令是串行执行的那样.

记忆障碍

那应该回答那个名义上的问题,但接下来你的第二个问题就是记忆障碍.然而,它包含一个错误的假设,即指令重新排序必然导致(并且是唯一的原因)可见内存重新排序.事实上,指令重新排序对于跨线程内存重新排序既不充分也不必要.

现在,无序执行无疑是无序内存访问功能的主要驱动因素,或者可能是对MLP(内存级并行)的追求驱动日益强大的无序功能适用于现代CPU.事实上,两者都可能同时成为现实:增加无序功能可以从强大的内存重新排序功能中获益,同时如果没有良好的无序功能,则无法进行积极的内存重新排序和重叠,因此他们互相帮助的是一种自我强化的总和 - 超过部分的循环.

所以,是的,无序执行和内存重新排序肯定有关系; 但是,您可以轻松地重新订购而无需执行无序操作!例如,核心本地存储缓冲区通常会导致明显的重新排序:在执行时,存储不会直接写入缓存(因此在一致点处不可见),这会延迟本地存储与本地存储的关系需要在执行点读取其值的负载.

正如Peter在注释线程中指出的那样,当允许加载在有序设计中重叠时,你也可以得到一种加载 - 加载重新排序:加载1可以开始但是在没有指令消耗其结果的情况下是流水线的 - 阶设计可以继续执行以下指令,其中可能包括另一个负载2.如果负载2是高速缓存命中并且负载1是高速缓存未命中,则负载2可能在负载1的早期时间满足,因此可以交换明显的顺序重新排序.

所以我们看到并非所有的跨线程内存重新排序都是由指令重新排序引起的,但是某些指令重新排序意味着无序的内存访问,对吧?没那么快!这里有两种不同的上下文:在硬件级别发生的事情(即,内存访问指令是否可以,实际上是无序执行),以及ISA和平台文档(通常称为内存)所保证的内容适用于硬件的型号).

x86重新订购

例如,在x86的情况下,现代芯片可以相对于彼此自由地重新排序任何加载和存储流:如果加载或存储准备好执行,CPU通常会尝试它,尽管存在早期未完成的加载和存储操作.

与此同时,x86定义了一个非常严格的内存模型,它禁止了大多数可能的重新排序,大致总结如下:

  • 商店具有单一的全球可见性顺序,所有CPU都会一致地观察,但下面会放松一条规则.
  • 对于其他本地加载操作,本地加载操作永远不会重新排序.
  • 本地存储操作从不对其他本地存储操作进行重新排序(即,在指令流中较早出现的存储总是在全局顺序中较早出现).
  • 可以针对较早的本地存储操作重新排序本地加载操作,使得加载看起来比本地存储更早地执行全局存储顺序,但反向(较早加载,较旧存储)不是真实的.

因此,实际上大多数内存重新排序是不允许的:相对于每个外部的加载,相对于彼此的存储,以及相对于后来的存储的加载.然而我在上面说过,x86几乎可以自由地执行无序的所有内存访问指令 - 你如何协调这两个事实呢?

好吧,x86做了大量的额外工作来准确跟踪加载和存储的原始顺序,并确保不会出现违反规则的内存重新排序.例如,假设load 2在加载1之前执行(加载1在程序顺序中出现得更早),但是在加载1和加载2执行期间,两个涉及的缓存行都处于"独占"状态:已经重新排序,但是本地核心知道无法观察到它,因为没有其他人能够窥探这种本地操作.

与上述优化一致,CPU也使用推测执行:不按顺序执行所有操作,即使有可能在稍后某个核心可以观察到差异,但实际上不执行指令直到这样的观察不可能.如果确实发生了这样的观察,则将CPU回滚到较早的状态并再次尝试.这是英特尔"内存订购机清除"的原因.

因此,可以定义一个不允许任何重新排序的ISA ,但是在封面下进行重新排序,但要仔细检查是否未遵守.PA-RISC是这种顺序一致的架构的一个例子.英特尔有一个强大的内存模型,允许一种类型的重新排序,但不允许其他许多,但每个芯片内部可以做更多(或更少)重新排序,只要他们可以保证在可观察的意义上遵守规则(在此感觉,它与编译器在优化时所扮演的"as-if"规则有些相关.

所有这一切的结果,x86需要内存屏障来防止所谓的StoreLoad重新排序(对于需要此保证的算法).在x86中你没有发现很多独立的内存障碍,因为大多数并发算法也需要原子操作,例如原子添加,测试和设置或比较和交换,而在x86上,这些都有完全的障碍.自由.因此,使用显式内存屏障指令mfence仅限于您不进行原子读取 - 修改 - 写入操作的情况.

Jeff Preshing的内存重新排序法中 有一个例子确实显示了在真正的x86 CPU上进行内存重新排序,并且mfence阻止了它.


1当然,如果你足够努力,这样的重新排序是可见的!最近的一个影响很大的例子是Spectre和Meltdown漏洞,它利用推测性无序执行和缓存侧通道来破坏内存保护安全边界.

  • @Steve - 在与 Peter 的问题下方查看我的评论线程。主要示例是存储缓冲区,它可能存在于根本不重新排序指令的芯片上。该示例还给出了允许 MLP 的有序芯片 - 如果响应以与指令顺序不同的顺序返回(例如,因为旧加载未命中而新加载命中),则这可能导致加载加载重新排序。我正在更新我的答案以使其更清楚。 (2认同)