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

X86 加载和存储是唯一重新排序的指令吗?,x86,cpu-architecture,memory-barriers,X86,Cpu Architecture,Memory Barriers,我读过很多关于内存排序的文章,它们都只说CPU会重新排序加载和存储 CPU(我对x86 CPU特别感兴趣)是否只对加载和存储进行重新排序,而不对其拥有的其余指令进行重新排序?无序处理器通常可以在可能、可行、有利于性能的情况下对所有指令进行重新排序。由于注册重命名,这对机器代码是透明的,除了加载和存储†这就是为什么人们通常只谈论加载和存储重新排序,因为这是唯一可观察到的重新排序 † 通常,FPU异常也是可以观察重新排序的情况。由于这个原因,大多数无序处理器都有不精确的异常,但x86处理器除外。在

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


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

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



† 通常,FPU异常也是可以观察重新排序的情况。由于这个原因,大多数无序处理器都有不精确的异常,但x86处理器除外。在x86上,处理器确保报告异常,就像浮点操作没有重新排序一样。

无序执行保留了单线程/内核按程序顺序运行的假象。这类似于C/C++的“仿佛优化”规则:只要可见效果相同,就可以在内部执行任何操作

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

即使是有序的CPU,其内存操作也会变得无序而全局可见。(例如,即使是带有存储缓冲区的简单RISC管道也会进行存储负载重新排序,如x86)。如果CPU没有特别避免加载/存储,那么它可以按顺序启动加载/存储,但允许加载/存储按顺序完成(以隐藏缓存未命中延迟),也可以对加载进行重新排序(或者像现代x86一样,严格按顺序执行,但通过仔细跟踪内存顺序,假装没有这样做)


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

(相关:有关查找指令级并行的窗口有多大的更多信息,例如,如果将其增加到
乘以200
,则只会看到有限的重叠。还相关:关于Haswell或Skylake等OoO CPU如何查找和利用ILP。)

有关
lfence
影响的更深入分析,请参见

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

注释掉一个
imul
链不会改变运行所需的时间:仍然是750M周期

这是交错两个依赖链的无序执行的明确证明,否则。(
imul
吞吐量为每时钟1个,延迟为3个时钟..因此第三个依赖链可以在没有太多减速的情况下混合使用)

来自
任务集-c3 ocperf.py stat的实际数字-无大数字-任务时钟、上下文切换、cpu迁移、页面错误、周期:u、分支:u、指令:u、发出的uops\u。任意:u、uops\u已执行。线程:u、uops\u已失效。失效插槽:u-r3./imul

  • 对于两条imul链:
    750566384+-0.1%
  • 仅使用EAX链:
    750704275+-0.0%
  • 一个
    乘以50μl eax,eax链:
    1501010762+-0.0%
    (几乎是预期速度的两倍)
  • 使用
    lfence
    防止25个imul的每个块之间重叠:
    1688869394+-0.0%
    ,速度比两倍慢
    uops\U发出的任何
    uops\U失效的
    插槽都从51M增加到了63M,而
    uops\U执行的线程
    仍然是51M(
    lfence
    不使用任何执行端口,但显然两条
    lfence
    指令每条消耗6个融合域UOP。Agner Fog仅测量了2个。)
序列化指令执行,但不序列化内存存储)。如果您没有使用WC内存中的NT加载(这不会意外发生),那么除了停止执行后面的指令,直到前面的指令“本地完成”为止,这是不可能的。i、 直到他们从混乱的核心退休。这可能就是为什么它的总时间增加了一倍多:它必须等待一个块中的最后一个
imul
,才能通过更多的管道阶段。)

英特尔的lfence总是这样,但是


脚注1:当两个逻辑线程共享一个物理线程(超线程或其他SMT)时,也有定时端通道。e、 g.如果另一个超线程不需要端口1,则在最近的Intel CPU上执行一系列独立的
imul
指令将以每时钟1运行。所以,您可以通过对一次逻辑内核上的ALU绑定循环计时来测量端口0的压力

其他微体系结构侧通道(如缓存访问)更可靠。例如,Spectre/Meldown最容易利用缓存读取端通道,而不是ALU

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


天湖上的MFENCE和LFENCE一样是一个OoO执行屏障 Skylake上的
mfence
意外地阻止了
imul
的无序执行,如
lfence
,尽管没有相关文件证明会产生这种效果。(有关更多信息,请参阅“移动到聊天室”讨论)

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

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)