加载和存储是否只有重新排序的指令?

Jam*_*mes 6 x86 cpu-architecture memory-barriers

我已经阅读了很多关于内存排序的文章,并且所有这些文章都只说CPU重新加载和存储.

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

Pet*_*des 11

乱序执行保留了单个线程/核心的程序顺序运行的错觉.这就像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.)

有关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)
Run Code Online (Sandbox Code Playgroud)

在x86-64 Linux上构建(带nasm+ ld)静态可执行文件,在预期的750M时钟周期内运行(在Skylake上)每个25 * 10Mimul指令链的时间为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链:( 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在英特尔总是这样,但在AMD上它只是部分序列化启用了Spectre缓解.


脚注1:当两个逻辑线程共享一个物理线程(超线程或其他SMT)时,还存在时序旁路通道.例如imul,如果另一个超线程不需要端口1,则执行一系列独立指令将在最近的Intel CPU上以每时钟1个运行.因此,您可以通过在一次逻辑核心上对ALU绑定循环进行计时来测量端口0压力的大小.

其他微架构侧通道(如高速缓存访​​问)更可靠.例如,Spectre/Meltdown最容易利用缓存读取侧通道而不是ALU.

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


Skylake上的MFENCE是像LFENCE一样的OoO执行障碍

mfence在SKYLAKE微架构意外块乱序执行imul,比如lfence,即使它没有记录有这种效果.(有关更多信息,请参阅移动聊天讨论).

xchg [rdi], ebx(隐式lock前缀)根本不阻止ALU指令的无序执行.的总时间仍然是750M周期更换时lfencexchglock在上述试验编指令.

但是mfence,成本上升到1500M周期+ 2个mfence指令的时间.为了进行受控实验,我保持指令计数相同,但是将mfence指令移到彼此旁边,因此imul链可以相互重新排序,时间下降到750M + 2个mfence指令的时间.

这种Skylake行为很可能是微码更新修复错误的结果SKL079,MOVNTDQA来自WC Memory可能会通过早期的MFENCE指令.错误的存在表明它曾经有可能在mfence完成之前执行后面的指令,所以可能他们做了一个强力修复,lfence为微代码添加uop mfence.

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

(另请参阅有关此Google网上论坛帖子的 Linux障碍选择的讨论,其中包含指向3个单独建议的链接,lock addl $0, -4(%esp/rsp)而不是mfence作为独立障碍使用.

  • @SamuelLiew:hrm,这些评论中有一些有用/有趣的微基准测试结果,现在很难找到.我想我现在就把它们编辑成这个答案.我真的不认为有必要对这个相对模糊的问题的答案进行清理.我知道asm/x86标签中的一些常规,包括我自己,"滥用"评论进行讨论,但IMO似乎一直运行正常,如果我能记住一些,通常可以找到谷歌的聊天评论我想稍后链接的关键字和/或参与者名称. (5认同)

fuz*_*fuz 5

乱序处理器通常可以对所有指令进行重新排序,这样做是可行的,可行的,有利于性能的.由于寄存器重命名,这对机器代码是透明的,除了加载和存储的情况 这就是人们通常只讨论加载和存储重新排序的原因,因为这是唯一可观察的重新排序.


 通常,FPU异常也是您可以观察重新排序的地方.出于这个原因,大多数乱序处理器都有不精确的异常,但不是x86.在x86上,处理器确保报告异常,就像没有重新排序浮点运算一样.

  • IMO 你的课程是错误的,大多数常用的 OoO CPU 都有精确的例外,除了一些相当模糊的情况。当然,在 OoO 中很难实现精确的异常,所以特别是有一个想法,也许没有它们你可以逃脱,但它在很大程度上并没有成功。 (2认同)
  • 该策略似乎在 2000 年代大部分(据我所知)半途而废,因为几乎每个人都是精确的 - 并且不精确 FP 异常的一些性能可以通过其他效果(例如粘性状态位)来实现。我很好奇是否有人知道任何常用的拱门仍然存在不精确的例外情况! (2认同)