x86 - 指令级并行性 - 指令的最佳顺序

Posted

技术标签:

【中文标题】x86 - 指令级并行性 - 指令的最佳顺序【英文标题】:x86 - Instruction-level parallelism - optimal order of instructions 【发布时间】:2016-08-15 06:49:15 【问题描述】:

以下两个 x86_64 代码的 sn-ps 哪个应该是最快的?还是根本没有区别?

; #1
    bsf    rax, rdi
    mov    rdx, -1
    cmove  rax, rdx

对比

; #2
    mov    rdx, -1
    bsf    rax, rdi
    cmove  rax, rdx

(或 #1 的替代方案,使用寄存器更经济。

; #1a
    bsf    rax, rdi
    mov    rdi, -1
    cmove  rax, rdi

)

是的,我知道我应该只对它们进行基准测试,但我没有工具,并且由于目前长期致残的疾病,我现在无法进行设置。

【问题讨论】:

这样的问题通常最好通过简单地编写代码并在多次迭代中计时。 @DavidHoelzer:我不同意:微基准测试很难,并且很可能由于某些不相关的原因一个版本看起来比另一个版本快。当序列的延迟和吞吐量不同时,也很容易出错。 microbench 可能会测试吞吐量,而实际使用对延迟敏感。这不是一个好问题,但答案只是“阅读 Agner Fog 的东西”,而不是“尝试自己计时”。如果你不知道我给出的答案,除了运气之外,你将无法写出好的微基准测试。 【参考方案1】:

另请参阅x86 标签 wiki 中的性能链接,尤其是 Agner Fog's microarch pdf and his Optimizing Assembly guide.


除非解码/前端效果发挥作用,否则它们基本上都是相等的,因为执行是乱序的。 (否则它取决于周围的代码,并且对于不同的微架构是不同的。)

它们都具有相同数量的并行度(两条链:独立的mov(无输入)和bsf(一个输入),外加一个依赖的 cmov)。它足够小,以至于乱序执行很容易找到这种并行性。如果您关心有序 Atom,那么 bsf 和 mov 都可以配对。

任何差异都取决于周围的代码。

如果我必须选择,我可能会选择#1a,因为这样可以减少movbsf 窃取执行端口的机会。 mov r64, imm32-sign-extended 可以在大多数 CPU 的任何端口上运行,但 bsf 通常不能。将指令放在关键路径上的 insn 之前不会减少资源冲突,至少在循环之外,来自先前迭代的非关键指令可以延迟关键路径。 (mov 有点在关键路径上,但它没有输入依赖,因此乱序执行可以在它发出后的任何时候运行它,可能在产生bsf 输入的指令之前。 )

我可能会使用#1a 而不是#1 以使sn-p 使用更少的寄存器来应对未来。我会使用#1,如果我有一个特定用途来为某个寄存器启动一个新的依赖链,比如后面的指令有一个错误的依赖,并且寄存器的值取决于一个长的依赖链(或者一个可能缓存未命中的负载) )。例如如果我想使用 8 位或 16 位寄存器,或者output register for popcnt

说到这,bsf 可能也对 Intel CPU 有错误的依赖。如果输入值为 0,英特尔 CPU 将保持目标不变。 (ISA 说 dest 是未定义的,但这是 Core2 实际所做的,例如。这需要依赖于目标寄存器以及源)。我怀疑这就是为什么 lzcnt / tzcnt / popcnt 依赖于输出寄存器。

说到虚假依赖:有趣的事实是,您可以通过执行or rdx, -1 (or r64, imm8)、with a false dependency on the dst register. 将一个寄存器设置为具有较少字节机器代码的全1。通常是个坏主意,不要这样做。

【讨论】:

我熟悉 Agner Fog 的杰出作品。他是英雄,强烈推荐, @PeterCordes 如果代码大小 > 指令获取块大小(我认为 skylake 上的 16 个字节),指令顺序会起作用吗?即[16 byte aligned]; mov rdx, -1; [filler instructions to 16 bytes]; bsf rax, rdi; cmove rax, rdx 的性能会比[16 byte aligned]; mov rdx, -1; bsf rax, rdi; [filler instructions to 16 bytes]; cmove rax, rdx @Noah:可能,但通常还是不会。如果关键路径的延迟是瓶颈(不是前端吞吐量),则前端通常已经在关键路径执行的位置之前向 OoO 后端发出指令(例如,调度器大小为 97 微秒) SKL,ROB 为 224 微秒)。此外,大部分时间代码是从 uop 缓存中运行的,而不是从旧版解码中运行的。 (但是每次运行时只运行一次的块在 uop 缓存中可能总是很冷。如果你有很多这样的功能,调整它们以用于传统的 fetch/decode 可能是相关的。) @Noah:Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths 展示了 OoO exec 如何在没有源代码/机器代码交错的情况下重叠两条长 imul dep 链,达到相当大的限制。 (只要你不使用lfence 来阻止OoO exec。)

以上是关于x86 - 指令级并行性 - 指令的最佳顺序的主要内容,如果未能解决你的问题,请参考以下文章

Java内存模型——重排序

重排序

Java并发:重排序

Java进阶 - 并发(关键字)

jvm重排序

重排序