为什么jnz需要2个周期才能在内循环中完成

时间:2019-01-12 03:17:06

标签: x86 micro-optimization microbenchmark micro-architecture

我在IvyBridge上。我发现jnz的性能行为在内部循环和外部循环中不一致。

以下简单程序具有一个固定大小为16的内部循环:

global _start
_start:
    mov rcx, 100000000
.loop_outer:
    mov rax,    16

.loop_inner:
    dec rax
    jnz .loop_inner

    dec rcx
    jnz .loop_outer

    xor edi, edi
    mov eax, 60
    syscall

perf工具显示外部循环运行32c / iter。这表明jnz需要2个周期才能完成。

然后我在Agner的指令表中进行搜索,条件跳转具有1-2个“倒数”,并带有注释“如果没有跳转则为快速”。

在这一点上,我开始相信上述行为是可以预料的。但是,为什么jnz在外部循环中只需要1个周期即可完成?

如果我完全删除.loop_inner部分,则外循环运行1c / iter。行为看起来不一致。

我在这里想念什么?

编辑以获取更多信息:

使用以下命令,上述程序的perf结果:

perf stat -ecycles,branches,branch-misses,lsd.uops,uops_issued.any -r4 ./a.out

是:

 3,215,921,579      cycles                                                        ( +-  0.11% )  (79.83%)
 1,701,361,270      branches                                                      ( +-  0.02% )  (80.05%)
        19,212      branch-misses             #    0.00% of all branches          ( +- 17.72% )  (80.09%)
        31,052      lsd.uops                                                      ( +- 76.58% )  (80.09%)
 1,803,009,428      uops_issued.any                                               ( +-  0.08% )  (79.93%)

参考案例的perf结果:

global _start
_start:
    mov rcx, 100000000
.loop_outer:
    mov rax,    16
    dec rcx
    jnz .loop_outer

    xor edi, edi
    mov eax, 60
    syscall

是:

   100,978,250      cycles                                                        ( +-  0.66% )  (75.75%)
   100,606,742      branches                                                      ( +-  0.59% )  (75.74%)
         1,825      branch-misses             #    0.00% of all branches          ( +- 13.15% )  (81.22%)
   199,698,873      lsd.uops                                                      ( +-  0.07% )  (87.87%)
   200,300,606      uops_issued.any                                               ( +-  0.12% )  (79.42%)

因此,原因很明显:在嵌套情况下,LSD由于某种原因停止工作。减小内部循环的大小会稍微缓解缓慢性,但不能完全缓解。

在搜索英特尔的《优化手册》时,我发现如果循环包含“超过八个分支”,则LSD将无法工作。这以某种方式解释了行为。

1 个答案:

答案 0 :(得分:3)

(部分答案/我在Hadi发表详细分析之前还没有写完文章的猜测;其中一些是从评论中继续的)

  

Agner的陈述“在uop缓存不是瓶颈的情况下,循环缓冲区没有可测量的作用……”是错误的吗?因为这肯定是可以测量的效果,并且uop缓存不是瓶颈,因为该缓存具有约1.5K的容量。

是的,Agner将其称为回送缓冲区。 他的声明是,将LSD添加到设计中不会加快任何代码的速度。但是,是的,对于非常紧密的循环,至少对于嵌套循环,这似乎是错误的。显然,SnB / IvB确实需要循环缓冲区来发出或执行1c / iter循环。除非微体系结构瓶颈是分支后要从uop缓存中获取uop,否则他的警告将解决此问题。

除了uop缓存未命中以外,在某些情况下读取uop缓存可能会成为瓶颈。例如如果由于对齐效果而导致uops打包得不太好,或者它们使用大的立即数和/或位移,而这需要花费额外的周期才能从uop缓存中读取。有关这些效果的更多详细信息,请参见Agner Fog's uarch guide的Sandybridge部分。您认为容量(如果包装得很好的话最多可达1.5k uops)是它可能变慢的唯一原因,这是非常错误的。

BTW,Skylake的微代码更新使LSD完全无法修复LSD,从而修复了部分寄存器合并错误erratum SKL150 1 ,实际上除了效果不大当一个微小的循环跨越32B边界并需要2条缓存行时。

但是Agner列出了JMP rel8/32,并在HSW / SKL上将JCC吞吐量视为1-2个周期,而在IvB上仅为2个周期。因此,自IvB以来,除了LSD本身以外,关于已采取分支的某些事情可能已经加速了。

除了LSD之外,可能还有CPU的某些部分,这对于长时间运行的微小循环也有特殊的情况,这使它们在Haswell及以后的时钟上每时钟运行1次跳跃。我尚未测试过什么条件会导致HSW / SKL上的分支吞吐量达到1个周期与2个周期。还要注意,Agner在更新勘误码SKL150之前进行了测量。


脚注1 :请参见How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent,并请注意,SKX和Kaby Lake附带了已经包含此代码的微码。它终于在Coffee Lake中重新启用,它修复了有问题的硬连线逻辑,因此可以安全地再次启用LSD。 https://en.wikichip.org/wiki/intel/microarchitectures/coffee_lake#Key_changes_from_Kaby_Lake