x86 - 加载和存储是唯一被重新排序的指令吗?
问题描述
我读过很多关于内存排序的文章,所有文章都只说 CPU 重新排序加载和存储。
CPU(我对 x86 CPU 特别感兴趣)是否只重新排序加载和存储,而不重新排序它拥有的其余指令?
解决方案
乱序执行保留了单个线程/内核按程序顺序运行的错觉。这就像 C/C++ as-if 优化规则:只要可见效果相同,就可以在内部做任何你想做的事情。
单独的线程只能通过内存相互通信,因此内存操作(加载/存储)的全局顺序是执行1的唯一外部可见副作用。
即使是有序的 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)
在 x86-64 Linux 上构建(使用nasm
+ )到静态可执行文件中,对于每个imul 指令ld
链乘以 3 个周期延迟,它在预期的 750M 时钟周期内运行(在 Skylake 上) 。25 * 10M
注释掉其中一条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
重叠 25imul
:1688869394 +- 0.0%
,比慢两倍还差。uops_issued_any
并且uops_retired_retire_slots
都是 63M,从 51M 上升,而uops_executed_thread
仍然是 51M(lfence
不使用任何执行端口,但显然两条lfence
指令每个花费 6 个融合域微指令。Agner Fog 仅测量 2。)
(lfence
序列化指令执行,但不序列化内存存储)。如果您不使用 WC 内存中的 NT 加载(这不会偶然发生),那么除了停止后面的指令执行,直到前面的指令“在本地完成”之前,它都是无操作的。即,直到他们从无序的核心中退休。这可能就是为什么它使总时间增加了一倍多的原因:它必须等待imul
一个块中的最后一个通过更多的流水线阶段。)
lfence
在 Intel 上总是这样,但在 AMD 上它只是在启用 Spectre 缓解的情况下进行部分序列化。
脚注 1:当两个逻辑线程共享一个物理线程(超线程或其他 SMT)时,也存在时序侧通道。imul
例如,如果另一个超线程不需要端口 1 来执行任何操作,那么在最近的 Intel CPU 上,执行一系列独立指令将以每个时钟 1 次运行。因此,您可以通过在一个逻辑核心上计时 ALU 绑定循环来测量端口 0 的压力。
其他微架构侧通道,例如缓存访问,更可靠。例如,Spectre / Meltdown 最容易通过缓存读取侧通道而不是 ALU 来利用。
但与架构支持的共享内存读/写相比,所有这些侧通道都是挑剔且不可靠的,因此它们仅与安全性相关。它们不是故意在同一个程序中用于线程之间的通信。
Skylake 上的 MFENCE 是像 LFENCE 一样的 OoO 执行障碍
mfence
imul
on Skylake 意外阻止了, like的乱序执行lfence
,即使它没有记录到具有这种效果。(有关更多信息,请参阅移至聊天讨论)。
xchg [rdi], ebx
(隐式lock
前缀)根本不会阻止 ALU 指令的乱序执行。上述测试lfence
中用xchg
或ed指令替换时,总时间仍为750M周期。lock
但是使用mfence
,成本高达 1500M 周期 + 2mfence
条指令的时间。为了做一个对照实验,我保持指令计数不变,但将mfence
指令彼此相邻移动,这样imul
链可以相互重新排序,时间下降到 750M + 2mfence
条指令的时间。
此 Skylake 行为很可能是修复错误 SKL079的微码更新的结果,来自 WC 内存的 MOVNTDQA 可能会通过较早的 MFENCE 指令。勘误表的存在表明它曾经有可能在mfence
完成之前执行后面的指令,所以他们可能做了一个暴力修复,将lfence
微码添加到mfence
.
这是另一个有利于使用xchg
seq-cst 存储,甚至lock add
是一些堆栈内存作为独立屏障的因素。 Linux 已经完成了这两件事,但编译器仍然使用mfence
屏障。请参阅为什么具有顺序一致性的 std::atomic 存储使用 XCHG?
(另请参阅此 Google Groups 线程上有关 Linux 屏障选择的讨论,其中包含 3 个单独建议的链接,用于lock addl $0, -4(%esp/rsp)
代替mfence
作为独立屏障使用。
推荐阅读
- python - 我想将一个表单的实例传递给另一个表单。我怎样才能做到这一点?
- python - 检查文本是否存在 2 个或多个用括号括起来的字符或数字,至少第一个字符为大写
- swift - 如果视图相同,则 TabView 生命周期问题
- javascript - Shortest Job First (Non preemptive) - 同时排序突发时间和到达时间
- javascript - 错误:无法获取 /
- javascript - Javascript html表格单元格编辑没有正确发生
- c - 将多个管道连接在一起时遇到问题
- algorithm - k-way归并排序基本情况
- javascript - 为什么 jQuery ui 可选插件禁用了我原来的绑定点击功能
- javascript - 这里有什么问题?为什么我的 Javascript 不起作用?