x86 CPU 是否重新排序指令?

Does an x86 CPU reorder instructions?

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

为了解决指令重排序的问题,我们可以在代码中合适的地方插入内存屏障。

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

重新排序

是的,来自 Intel 和 AMD 的所有现代 x86 芯片都在 window 中积极地重新排序指令,这在两个制造商最近的 CPU 中大约有 200 条指令(即新指令可能会在执行时执行)超过 200 条指令的旧指令 "in the past" 仍在等待)。这通常对单个线程都是不可见的,因为 CPU 仍然通过尊重依赖关系保持当前线程串行执行1 的错觉,所以从当前执行线程是 as-if 指令是串行执行的。

内存障碍

这应该可以回答标题问题,但是您的第二个问题是关于记忆障碍的。但是,它包含一个错误的假设,即 instruction 重新排序必然导致(并且是唯一原因)可见 memory 重新排序。事实上,指令重新排序对于 cross-thread 内存 re-ordering.

既不充分也没有必要

现在 out-of-order 执行是 out-of-order 内存访问能力的主要 driver 绝对正确,或者它可能是寻求 MLP (Memory Level Parallelism) 来驱动现代 CPU 越来越强大的 out-of-order 能力。事实上,两者可能同时成立:增加 out-of-order 功能可以从强大的内存重新排序功能中获益良多,同时,如果没有良好的 out-of-order 功能,就不可能进行积极的内存重新排序和重叠,所以他们以一种 self-reinforcing sum-greater-than-parts 的循环方式互相帮助。

所以是的,out-of-order 执行和内存重新排序肯定有关系;然而,你可以很容易地得到 re-ordering 而无需 out-of-order 执行 !例如,core-local 存储缓冲区通常会导致明显的重新排序:在执行时,存储不会直接写入缓存(因此在一致性点不可见),这会延迟本地存储到需要在执行时读取其值的本地负载。

正如 Peter 在 comment thread 中指出的那样,当在 in-order 设计中允许负载重叠时,您还可以获得一种 load-load 重新排序:负载 1 可能 start 但在没有指令使用其结果的情况下,流水线 in-order 设计可能会继续执行以下指令,其中可能包含另一个加载 2。如果加载 2 是高速缓存命中并加载 1是缓存未命中,加载 2 可能比加载 1 更早得到满足,因此表观顺序可能会交换 re-ordered.

所以我们看到不是所有cross-thread内存re-ordering是由指令re-ordering引起的,而是某些指令re-ordering 意味着out-of-order 内存访问,对吗?没那么快!这里有两种不同的上下文:在硬件级别发生了什么(即内存访问指令是否可以作为实际问题执行 out-of-order),以及 ISA 和平台文档(通常称为 内存模型适用于硬件)。

x86 re-ordering

例如,在 x86 的情况下,现代芯片将或多或少地自由地 re-order 任何加载和存储流:如果加载或存储准备好执行,CPU 通常会尝试它,尽管之前存在未完成的加载和存储操作。

同时x86定义了相当严格的内存模型,bans大多数可能的重排序,大致总结如下:

  • 商店具有单一的全局可见性顺序,所有 CPUs 一致观察,受以下规则的影响。
  • 本地加载操作永远不会相对于其他本地加载操作重新排序。
  • 本地存储操作永远不会相对于其他本地存储操作重新排序(即,在指令流中较早出现的存储总是在全局顺序中较早出现)。
  • 本地加载操作可以相对于较早本地存储操作重新排序,这样加载似乎比本地存储更早执行全局存储顺序,但相反(较早的加载,较旧的商店)不正确。

所以实际上大多数内存 re-ordering 是 不允许的: 相对于每个外部加载,相对于彼此存储,并且相对于后面的存储加载.然而我在上面说过 x86 几乎可以自由执行 out-of-order 所有内存访问指令 - 你如何调和这两个事实?

好吧,x86 做了很多额外的工作来准确跟踪加载和存储的原始顺序,并确保没有内存 re-ordering 违反规则是可见的。例如,假设加载 2 在加载 1 之前执行(加载 1 在程序顺序中出现得更早),但是在加载 1 和加载 2 执行期间,两个涉及的缓存行都处于 "exclusively owned" 状态:重组ering,但是本地核心知道它无法被观察到,因为没有其他人能够窥视这个本地操作。

与上述优化一致,CPUs 还使用推测执行:乱序执行所有内容,即使在稍后的某个时刻某些核心可能会观察到差异,但不实际上 提交 指令,直到这样的观察是不可能的。如果确实发生此类观察,则将 CPU 回滚到较早的状态并重试。这是英特尔 "memory ordering machine clear" 的原因。

所以可以定义一个ISA,它根本不允许any re-ordering,但在幕后做re-ordering但仔细检查它没有被观察到。 PA-RISC 是这种顺序一致架构的一个例子。英特尔有一个强大的内存模型,允许一种类型的重新排序,但不允许许多其他类型,但每个芯片内部可能会做更多(或更少)re-ordering,只要它们能保证在可观察的意义上遵守规则(从这个意义上讲,它与编译器在优化时遵循的 "as-if" 规则有些相关。

所有这些的结果是 ,x86 需要内存屏障来专门防止 so-called StoreLoad re-ordering(对于需要此保证的算法).在 x86 的实践中,你不会发现很多独立的内存屏障,因为大多数并发算法也需要 atomic 操作,例如 atomic add,test-and-set 或 compare-and-exchange,在 x86 上,所有这些都带有完全免费的障碍。因此,像 mfence 这样的显式内存屏障指令的使用仅限于您没有同时执行原子 read-modify-write 操作的情况。

Jeff Preshing 的 当场抓获的内存重新排序 有一个示例确实显示了在真正的 x86 CPUs 上的内存重新排序,而 mfence 阻止了它。


1 当然,如果你足够努力,这样的重新排序是可见的! high-impact 最近的一个例子是 Spectre 和 Meltdown 攻击,它们利用推测性 out-of-order 执行和缓存侧通道来违反内存保护安全边界。