乱序执行保留了按程序顺序运行的错觉对于单线程/核心。这就像C/C++ as-if 优化规则:只要可见效果相同,内部就可以做任何你想做的事。
Separate threads can only communicate with each other via memory, so the global order of memory operations (loads/stores) is the only externally visible side-effect of execution1.
即使有序的 CPU 也可能使其内存操作变得全局可见且无序。 (例如,即使是带有存储缓冲区的简单 RISC 管道也会进行 StoreLoad 重新排序,如 x86)。按顺序启动加载/存储但允许它们无序完成(以隐藏缓存未命中延迟)的 CPU 也可以重新排序加载,如果它没有专门避免它(或者像现代 x86 一样,积极地执行无序)顺序但假装它没有通过仔细跟踪内存顺序)。
一个简单的例子:两个 ALU 依赖链可以重叠
(有关的:http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/有关查找指令级并行性的窗口有多大的更多信息,例如如果你把这个增加到times 200
你只会看到有限的重叠。还相关:我写的这个初级到中级的答案关于像 Haswell 或 Skylake 这样的 OoO CPU 如何发现和利用 ILP。)
也可以看看现代微处理器
90 分钟指南!深入了解超标量和乱序执行 CPU。
为了更深入地分析影响lfence
在这里,参见了解 lfence 对具有两个长依赖链的循环的影响,以增加长度
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 上的静态可执行文件中,这在每个链的预期 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
:
- 与两个 imul 链:
750566384 +- 0.1%
- 仅使用 EAX 链:
750704275 +- 0.0%
- 与一个
times 50 imul eax,eax
chain: 1501010762 +- 0.0%
(几乎是预期的两倍慢)。
- with
lfence
防止每个 25 块之间重叠imul
: 1688869394 +- 0.0%
,比慢两倍还差。uops_issued_any
and uops_retired_retire_slots
都是 63M,高于 51M,而uops_executed_thread
仍然是51M(lfence
不使用任何执行端口,但显然有两个lfence
每条指令花费 6 个融合域微指令。 Agner Fog 只测得 2。)
(lfence串行化指令执行,但不是内存存储)。如果您没有使用 WC 内存中的 NT 加载(这不会意外发生),则除了停止执行后续指令直到前面的指令“本地完成”之外,它是一个无操作。即直到他们retired来自无序核心。这可能就是为什么它的总时间增加了一倍以上:它必须等待最后一个imul
在一个块中经历更多的管道阶段。)
lfence
在英特尔上总是这样,但是开启并且仅在启用 Spectre 缓解的情况下进行部分序列化.
脚注1:当两个逻辑线程共享一个物理线程(超线程或其他 SMT)时,还有计时侧通道。例如执行一系列独立的imul
如果其他超线程不需要端口 1 进行任何操作,则指令将在最新的 Intel CPU 上以每个时钟 1 的速度运行。因此,您可以通过对一个逻辑核心上的 ALU 绑定循环进行计时来测量端口 0 的压力有多大。
其他微架构侧通道(例如缓存访问)更加可靠。例如,Spectre / Meltdown 最容易利用缓存读取侧通道(而不是 ALU)来利用。
但与架构支持的共享内存读/写相比,所有这些侧通道都非常挑剔且不可靠,因此它们仅与安全相关。它们不是故意在同一程序中用于线程之间的通信。
Skylake 上的 MFENCE 是一个像 LFENCE 一样的 OoO 执行屏障
mfence
Skylake 意外阻止乱序执行imul
, like lfence
,尽管没有记录表明有这种效果。 (有关更多信息,请参阅移至聊天讨论)。
xchg [rdi], ebx
(隐式lock
前缀)根本不会阻止 ALU 指令的无序执行。替换时总时间仍为750M Cycleslfence
with xchg
or a lock
上述测试中的 ed 指令。
但与mfence
,成本高达 1500M 周期 + 2 的时间mfence
指示。为了进行对照实验,我保持指令计数相同,但移动了mfence
指令彼此相邻,因此imul
链之间可以重新排序,时间下降到750M + 2的时间mfence
指示。
Skylake 的这种行为很可能是微代码更新修复的结果勘误表 SKL079, 来自 WC 内存的 MOVNTDQA 可能会通过早期的 MFENCE 指令。勘误表的存在表明,以前可以执行后面的指令mfence
已完成,所以他们可能进行了暴力修复添加lfence
uop 到微码mfence
.
这是有利于使用的另一个因素xchg
对于 seq-cst 存储,甚至lock add
将某些堆栈内存作为独立的屏障。Linux 已经做了这两件事,但编译器仍然使用mfence
为障碍。看为什么具有顺序一致性的 std::atomic 存储使用 XCHG?
(另请参阅关于 Linux 屏障选择的讨论此 Google 网上论坛帖子,包含 3 个单独的使用建议的链接lock addl $0, -4(%esp/rsp)
代替mfence
作为一个独立的屏障。