x86 CPU 会重排指令吗?

14

我了解到一些CPU会重新排序指令,但对于单线程程序来说这并不是问题(指令仍将被重新排序,但看起来就像指令是按顺序执行的),只有对于多线程程序才是问题。

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

但x86 CPU是否会重新排序指令呢?如果它不会重新排序,则无需使用内存屏障,对吗?


1
现代的x86不仅会重新排序指令,还会将它们转换为微操作。即使没有指令重排,当写入内存不能保证按照原始顺序时,你需要内存屏障来进行MT。这不仅取决于指令的乱序执行,还取决于内存模型,内存模型可能足够弱以重新排列内存更改的顺序,从而影响到其他核心。 (如果我没记错,x86具有非常“强”的内存模型,为程序员解决了许多这些复杂性,但是x86仍然需要屏障)。 - Ped7g
5
内存重排序与乱序执行是独立的。按顺序执行的CPU按顺序开始指令,但它们仍然可以无序地完成,并且存储被缓冲。请参见http://preshing.com/20120515/memory-reordering-caught-in-the-act获取有关何时需要在x86上使用`mfence`的信息:仅用于防止StoreLoad重排序;据我所知,在按顺序执行的Atom或Pentium CPU上仍需要mfence。(但所有现代的x86 CPU都具有完全的乱序执行能力。) - Peter Cordes
2
是的,完全同意。事实上,我刚意识到我的答案原始版本是错误的,因为它写成“是的,x86会重新排序指令,所以你需要内存屏障。”这是错误的(_so_部分),我想这就是你上面提到的。我改了一下,现在更加独立 :)。我实际上同意它们在ISA/文档级别上大多是独立的,但在CPU设计uarch级别上密切相关(但乱序重排不是内存重排的唯一原因,正如你指出的那样)。@peter - BeeOnRope
1
现在我想在我的回答中使用“独立”这个词。必须有一个更好的词,它的意思是“不是暗示(或反之亦然),但可能与...相关”。 - BeeOnRope
1
@scottxiao:是的(如一个具有两个imul依赖链的示例所示)。反之亦然(按顺序执行指令,存储缓冲区创建的内存重排序。在允许的弱序ISA上,也可以通过命中下失效负载排序来实现。) - Peter Cordes
显示剩余11条评论
1个回答

32

重新排序

是的,来自英特尔和AMD的所有现代x86芯片都会在一个窗口中积极地重新排序指令,该窗口在两个制造商最近的CPU上大约有200条指令深度(即新指令可能执行,而旧指令“过去”超过200条指令仍在等待)。这通常对单个线程不可见,因为CPU仍通过尊重依赖关系维护当前线程的串行执行幻象1,所以从当前执行线程的角度来看,这些指令的执行方式就好像它们是按顺序执行的。

内存屏障

那应该回答了标题问题,但您的第二个问题是关于内存屏障的。但是,它包含一个错误的假设,即指令重新排序必然导致(并且是唯一的)可见内存重新排序。实际上,指令重新排序既不充分也不必要进行跨线程内存重新排序。

现在确实可以肯定的是,乱序执行是乱序访问内存能力的主要驱动程序,或者说是追求MLP(内存级并行性)的能力,推动现代CPU不断增强其乱序能力。实际上,两者可能同时成立:不断增强的乱序能力在很大程度上从强大的内存重新排序功能中受益,而同时也没有好的乱序能力,就无法进行激进的内存重新排序和重叠,因此它们在一种自我加强的超过部分的循环中相互帮助。

因此,乱序执行和内存重新排序肯定存在关系;但是,您可以轻松地获得重新排序而没有乱序执行!例如,核心本地存储缓冲区通常会导致表面上的重新排序:在执行点时,存储不会直接写入缓存(因此在一致性点上不可见),这会延迟本地存储与需要在执行点读取其值的本地加载之间的时间。

正如Peter在评论线程中指出的那样,当允许按顺序设计中的负载重叠时,还可以获得一种负载-负载重新排序类型:负载1可能开始,但在缺少使用其结果的指令的情况下,流水线按顺序设计可能继续执行接下来的指令,其中可能包括另一个负载2。如果负载2是缓存命中,而负载1是缓存未命中,则负载2可能从负载1更早地得到满足,因此表面上的顺序可能会被交换重新排序。

因此,我们看到,并非所有的跨线程内存重新排序都是由指令重新排序引起的,但某些指令重新排序意味着乱序访问内存,对吧?不要那么快!这里有两种不同的上下文:发生在硬件级别例如,在x86中,现代芯片会自由地重排序几乎任何负载和存储流:如果一个加载或存储准备好执行,CPU通常会尝试执行它,即使存在尚未完成的较早的加载和存储操作。

同时,x86定义了相当严格的内存模型,禁止大部分可能的重新排序,大致总结如下:
- 存储具有单个全局可见性顺序,由所有CPU一致观察,其中有一个下面的规则要求放宽; - 本地加载操作永远不会与其他本地加载操作重新排序; - 本地存储操作永远不会与其他本地存储操作重新排序(即出现在指令流中较早的存储始终出现在全局顺序中较早的位置); - 本地加载操作可以与更早的本地存储操作重新排序,使得加载在全局存储顺序方面似乎比本地存储更早执行,但反之(更早的加载,更老的存储)不成立。
因此,实际上大多数内存重排序是不允许的:负载相对于外部进行重排序,存储相对于彼此进行重排序,以及负载相对于后来的存储进行重排序。然而我以上说过x86几乎自由地执行所有内存访问指令的乱序执行——如何调和这两个事实呢?
好吧,x86会做大量额外的工作来跟踪负载和存储的原始顺序,并确保不会出现违反规则的内存重排序。例如,假设加载2在加载1之前执行(加载1在程序顺序中先出现),但是在加载1和加载2执行期间,两个涉及的高速缓存行都处于“独占所有”状态:已经发生了重排序,但本地核心知道它不能被观察到,因为没有其他核心能够窥视此本地操作。
与上述优化相结合,CPU还使用推测执行:即使可能在稍后某个时刻某个核心可以观察到差异,也要执行所有的乱序操作,但直到不可能进行此类观察才实际提交指令。如果确实发生了这样的观察,则将CPU回滚到较早的状态并重新尝试。这是Intel“清空内存排序机器”的原因。
因此,可以定义一种ISA,它根本不允许任何重新排序,但在幕后进行重新排序,但仔细检查它是否被观察到。 PA-RISC就是这种顺序一致的架构的例子。Intel具有强大的内存模型,允许一种类型的重新排序,但禁止许多其他重新排序,但每个芯片在内部可以进行更多或更少的重新排序,只要它们保证以可观察的方式遵守规则(从这个意义上说,它与编译器在优化方面遵循的“as-if”规则有些相关)。所有这些的结果是,是的,x86需要内存屏障才能防止特定的所谓的StoreLoad重排序(对于需要此保证的算法)。在实际中,您不会发现许多单独的内存屏障在x86上使用,因为大多数并发算法也需要原子操作,例如原子添加,测试和设置或比较和交换,在x86上,所有这些都免费提供完整的屏障。因此,显式内存屏障指令(如mfence)的使用仅限于您没有执行原子读取修改写入操作的情况。
Jeff Preshing的Memory Reordering Caught in the Act 中有一个示例,显示了真实的x86 CPU上的内存重排序,mfence可以防止它。
1 当然,如果您足够努力,这种重排序是可见的!最近一个高影响的例子是Spectre和Meltdown漏洞,它们利用了预测的乱序执行和缓存侧信道来违反内存保护安全边界。

“并非所有的内存重排序都是由指令重排序引起的。”除了指令重排序,还有什么会导致内存重排序? - Steve
2
@Steve - 请看一下我与 Peter 在问题下面的评论串。主要示例是存储缓冲区,在根本不重新排序指令的芯片上可能存在。还给出了允许 MLP 的顺序芯片的例子-如果响应以与指令顺序不同的顺序返回(例如,因为旧负载未命中并且新负载命中),则可能会导致负载-负载重新排序。我正在更新我的答案以使其更清晰。 - BeeOnRope
@BeeOnRope,英特尔对内存重排序的限制是否会导致指令无法重排序? - choxsword
不,它们仍然会重新排序内存指令,但然后在退休之前仅检查重新排序是否可见。 - BeeOnRope
我很好奇,如果在两个storeLoad指令之间出现中断,CPU会怎么做?等待存储完成还是不考虑这种重排序情况? - Chinaxing
显示剩余3条评论

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