首页 > 解决方案 > 加载和存储是唯一被重新排序的指令吗?

问题描述

我读过很多关于内存排序的文章,所有文章都只说 CPU 重新排序加载和存储。

CPU(我对 x86 CPU 特别感兴趣)是否只重新排序加载和存储,而不重新排序它拥有的其余指令?

标签: x86cpu-architecturememory-barriers

解决方案


乱序执行保留了单个线程/内核按程序顺序运行的错觉。这就像 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重叠 25 imul: 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 执行障碍

mfenceimulon 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.

这是另一个有利于使用xchgseq-cst 存储,甚至lock add是一些堆栈内存作为独立屏障的因素。 Linux 已经完成了这两件事,但编译器仍然使用mfence屏障。请参阅为什么具有顺序一致性的 std::atomic 存储使用 XCHG?

(另请参阅此 Google Groups 线程上有关 Linux 屏障选择的讨论,其中包含 3 个单独建议的链接,用于lock addl $0, -4(%esp/rsp)代替mfence作为独立屏障使用。


推荐阅读