我已经阅读了很多关于内存排序的文章,并且所有这些文章都只说CPU重新加载和存储。
CPU(我对x86 CPU特别感兴趣)是否仅重新排序加载和存储,并且不重新排序它具有的其余指令?
乱序执行保留了单个线程/核心的程序顺序运行的错觉。这就像C / C ++ as-if优化规则:只要可见效果相同,就可以在内部做任何事情。
单独的线程只能通过内存相互通信,因此内存操作(加载/存储)的全局顺序是execution1唯一外部可见的副作用。
即使是有序的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)
在x86-64 Linux上构建(使用nasm
+ ld
)静态可执行文件,在预期的750M时钟周期内运行(在Skylake上)每个25 * 10M
imul指令链的时间为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
的实际数字:
750566384 +- 0.1%
750704275 +- 0.0%
times 50 imul eax,eax
链:1501010762 +- 0.0%
(几乎是预期的两倍慢)。lfence
防止25个imul
:1688869394 +- 0.0%
的每个块之间的重叠,比慢两倍。 uops_issued_any
和uops_retired_retire_slots
都是63M,高于51M,而uops_executed_thread
仍然是51M(lfence
不使用任何执行端口,但显然两个lfence
指令每个花费6个融合域uops.Agner Fog仅测量2.)(lfence
序列化指令执行,但不是内存存储)。如果你没有使用来自WC内存的NT加载(这不会偶然发生),除了停止执行后续指令直到先前的指令“在本地完成”之外,它是无操作的。即直到他们从无序核心退役。这可能是为什么它比总时间加倍的原因:它必须等待一个块中的最后一个imul
才能经历更多的管道阶段。)
英特尔的lfence
总是这样,但是on AMD it's only partially-serializing with Spectre mitigation enabled。
脚注1:当两个逻辑线程共享一个物理线程(超线程或其他SMT)时,还存在时序旁路通道。例如执行一系列独立的imul
指令将在最近的Intel CPU上以每时钟1个运行,如果另一个超线程不需要端口1。因此,您可以通过在一次逻辑核心上对ALU绑定循环进行计时来测量端口0压力的大小。
其他微架构侧通道(如高速缓存访问)更可靠。例如,Spectre / Meltdown最容易利用缓存读取侧通道而不是ALU。
但是,与架构支持的对共享内存的读/写相比,所有这些侧通道都是挑剔且不可靠的,因此它们仅与安全性相关。它们不是故意在同一程序中用于线程之间的通信。
Skylake上的mfence
意外地阻止了imul
的无序执行,就像lfence
一样,尽管没有记录这种效果。 (有关更多信息,请参阅移动聊天讨论)。
xchg [rdi], ebx
(隐式lock
前缀)根本不阻止ALU指令的无序执行。在上述测试中用lfence
或xchg
ed指令替换lock
时,总时间仍为750M周期。
但是使用mfence
,成本上升到1500M周期+ 2个mfence
指令的时间。为了进行对照实验,我保持指令计数相同但将mfence
指令彼此相邻移动,因此imul
链可以相互重新排序,时间下降到750M + 2个mfence
指令的时间。
这种Skylake行为很可能是微码更新修复erratum SKL079的结果,MOVNTDQA来自WC Memory可能会通过早期的MFENCE指令。错误的存在表明它曾经有可能在mfence
完成之前执行后面的指令,所以可能他们做了一个强力修复,将lfence
uops添加到mfence
的微码。
这是支持将xchg
用于seq-cst存储,甚至lock add
作为独立屏障的一些堆栈内存的另一个因素。 Linux已经完成了这两件事,但编译器仍然使用mfence
作为障碍。见Why does a std::atomic store with sequential consistency use XCHG?
(另见关于Linux在this Google Groups thread上的障碍选择的讨论,链接到3个单独的建议,使用lock addl $0, -4(%esp/rsp)
而不是mfence
作为一个独立的障碍。
乱序处理器通常可以对所有指令进行重新排序,这样做是可行的,可行的,有利于性能的。由于寄存器重命名,这对机器代码是透明的,除了加载和存储的情况†这就是为什么人们通常只谈论加载和存储重新排序,因为这是唯一可观察的重新排序。
†通常,FPU异常也是您可以观察重新排序的地方。出于这个原因,大多数乱序处理器都有不精确的异常,但不是x86。在x86上,处理器确保报告异常,就像没有重新排序浮点运算一样。