加载和存储是唯一重新排序的指令吗?

时间:2018-05-23 17:57:06

标签: x86 cpu-architecture memory-barriers

我已经阅读了许多关于内存排序的文章,并且所有这些文章都只说CPU重新加载和存储。

CPU(我对x86 CPU特别感兴趣)是否仅重新排序加载和存储,并且不重新排序其拥有的其他指令?

2 个答案:

答案 0 :(得分:10)

无序执行保留了以单个线程/核心的程序顺序运行的错觉。这就像C / C ++ as-if优化规则:只要可见效果相同,就可以在内部做任何事情。

单独的线程只能通过内存相互通信,因此内存操作的全局顺序(加载/存储)是执行 1 的唯一外部可见的副作用。

即使是有序的CPU,它们的内存操作也可能无序地全局可见。 (例如,即使是具有存储缓冲区的简单RISC管道也将具有StoreLoad重新排序,如x86)。一个按顺序启动加载/存储但允许它们无序完成(以隐藏缓存未命中延迟)的CPU也可以重新排序负载,如果它没有特别避免它(或者像现代x86那样,积极执行 - 但是假装它没有仔细跟踪内存排序。

一个简单的例子:两个ALU依赖链可以重叠

(相关:http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/了解更多有关窗口查找指令级并行度的详细信息,例如,如果将此值增加到times 200,则只能看到有限的重叠。还相关: this beginner to intermediate-level answer I wrote关于像Haswell或Skylake这样的OoO CPU如何发现并利用ILP。)

要更深入地分析lfence在此处的影响,请参阅Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths

global _start
_start:
    mov  ecx, 10000000
.loop:
    times 25 imul eax,eax   ; expands to imul eax,eax  / imul eax,eax / ...
 ;   lfence
    times 25 imul edx,edx
 ;   lfence
    dec  ecx
    jnz  .loop

    xor  edi,edi
    mov  eax,231
    syscall          ; sys_exit_group(0)

构建(使用nasm + ld)到x86-64 Linux上的静态可执行文件中,在25 * 10M imul的每个链的预期750M时钟周期内运行(在Skylake上)指令乘以3周期延迟。

评论其中一个imul链并没有改变运行时间:仍然是750M周期。

这是无序执行交错两个依赖链的明确证据,否则。 (imul吞吐量是每个时钟1个,延迟3个时钟。http://agner.org/optimize/。所以第三个依赖链可以混合进来而没有太大的减速。)

taskset -c 3 ocperf.py stat --no-big-num -etask-clock,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,uops_retired.retire_slots:u -r3 ./imul的实际数字:

  • 同时使用两个imul链:750566384 +- 0.1%
  • 只有EAX链:750704275 +- 0.0%
  • 有一个times 50 imul eax,eax链:1501010762 +- 0.0%(差不多是预期的两倍)。
  • lfence阻止25 imul 1688869394 +- 0.0%的每个块之间重叠,比慢慢两倍。 uops_issued_anyuops_retired_retire_slots都是63M,高于51M,而uops_executed_thread仍然是51M(lfence没有使用任何执行端口,但显然有两个{{1}每个指令花费6个融合域uops.Agner Fog仅测量2。)

lfence序列化指令执行,但不是内存存储)。如果你没有使用来自WC内存的NT加载(这不会偶然发生),除了停止执行后续指令直到之前的指令已经在本地完成之后,它才是无操作的" #34 ;.即,直到他们从无序核心退休。这可能是为什么它比总时间加倍的原因:它必须等待一个块中的最后一个lfence来经历更多的流水线阶段。)

英特尔上的

imul总是那样,但on AMD it's only partially-serializing with Spectre mitigation enabled

脚注1 :当两个逻辑线程共享一个物理线程(超线程或其他SMT)时,还存在时序旁路通道。例如执行一系列独立lfence指令将在最近的Intel CPU上以每时钟1个运行,如果另一个超线程不需要端口1用于任何事情。因此,您可以通过在一次逻辑核心上对ALU绑定循环进行计时来测量端口0的压力。

其他微架构侧通道(如高速缓存访​​问)更可靠。例如,Spectre / Meltdown最容易利用缓存读取侧通道而不是ALU。

但是,与架构支持的对共享内存的读/写相比,所有这些侧通道都是挑剔且不可靠的,因此它们只与安全性相关。它们并不是故意在同一程序中用于线程之间的通信。

Skylake上的MFENCE是像LFENCE

这样的OoO执行障碍 Skylake上的

imul意外阻止mfence的无序执行,例如imul ,即使它没有记录在案那种效果。 (有关更多信息,请参阅移动聊天讨论)。

lfence(隐式xchg [rdi], ebx前缀)根本不会阻止ALU指令的无序执行。在上述测试中用locklfence ed指令替换xchg时,总时间仍为750M周期。

但使用lock时,费用最高可达1500M周期+ 2 mfence条指令的时间。为了进行对照实验,我保持指令计数相同,但将mfence指令移到彼此旁边,这样mfence链可以相互重新排序,时间减少到750M + 2 imul条指令的时间。

这种Skylake行为很可能是微码更新修复erratum SKL079的结果, MOVNTDQA来自WC内存可能会通过早期的MFENCE指令。错误的存在表明以前可以在mfence完成之前执行后续指令,因此他们可能会对mfence的微代码添加lfence uop进行强制修复。

这是另一个支持将mfence用于seq-cst存储,甚至xchg作为独立屏障的堆栈内存的另一个因素。 Linux已经做到了这两件事情,但编译器仍然使用lock add作为障碍。见Why does a std::atomic store with sequential consistency use XCHG?

(另请参阅this Google Groups thread上有关Linux的障碍选择的讨论,以及指向使用mfence而不是lock addl $0, -4(%esp/rsp)作为独立屏障的3条单独建议的链接。< / p>

答案 1 :(得分:5)

无序处理器通常可以对所有指令进行重新排序,这样做是可行的,可行的,有利于性能的。由于寄存器重命名,这对机器代码是透明的,除了加载和存储的情况这就是为什么人们通常只谈论加载和存储重新排序的原因。只有可观察到的重新排序。

通常,FPU异常也是您可以观察重新排序的地方。由于这个原因,大多数乱序处理器都有不精确的异常,但不是x86。在x86上,处理器确保报告异常,就像没有重新排序浮点运算一样。