为什么jnz不算周期?

时间:2019-01-04 03:38:16

标签: performance assembly x86 micro-optimization micro-architecture

我在在线资源中发现IvyBridge具有3个ALU。所以我写了一个小程序来测试:

global _start
_start:
    mov rcx,    10000000
.for_loop:              ; do {
    inc rax
    inc rbx
    dec rcx
    jnz .for_loop       ; } while (--rcx)

    xor rdi,    rdi
    mov rax,    60      ; _exit(0)
    syscall

我用perf编译并运行它:

$ nasm -felf64 cycle.asm && ld cycle.o && sudo perf stat ./a.out

输出显示:

10,491,664      cycles
乍一看似乎很有意义,因为有3条独立的指令(2条inc和1条dec)在循环中使用ALU,因此它们合计1个周期。

但是我不明白的是为什么整个循环只有一个循环? jnz取决于dec rcx的结果,它应计为1个周期,因此整个循环为2个周期。我希望输出结果接近20,000,000 cycles

我还尝试将第二个incinc rbx更改为inc rax,这使其依赖于第一个inc。结果的确接近20,000,000 cycles,这表明依赖关系将延迟一条指令,使它们不能同时运行。那么jnz为什么很特别?

我在这里缺少什么?

1 个答案:

答案 0 :(得分:3)

首先,dec/jnz将宏融合到Intel Sandybridge系列上的单个uop中。您可以通过在dec和jnz之间放置一个非标志设置指令来克服它。

.for_loop:              ; do {
    inc rax
    dec rcx
    lea rbx, [rbx+1]    ; doesn't touch flags, defeats macro-fusion
    jnz .for_loop       ; } while (--rcx)

在Haswell以及以后的Ryzen和Ryzen上,这仍然会以每个周期1个迭代的速度运行,因为它们有4个整数执行端口,每次迭代可以保持4 oups。 (您的带宏融合的循环在Intel CPU上只有3个融合域uops,因此SnB / IvB也可以以每个时钟1个的速度运行它。)

请参见Agner Fog's优化指南,尤其是他的Microarch指南。还有https://stackoverflow.com/tags/x86/info中的其他链接。


与数据依赖项不同,控制依赖项通过分支预测+推测性执行被隐藏。

乱序执行和分支预测+投机执行隐藏了控件依赖项的“等待时间”。也就是说,下一次迭代可以在CPU验证jnz确实被采用之前开始运行。

因此,每个jnz在可以验证预测之前都对前一个dec rcx具有输入依赖性,但是后面的指令不必等待其被检查就可以执行。有序的退休可确保在任何事情都可以“看到”之前就抓住了错误的猜测(导致幽灵攻击的微体系结构效应除外)


1000万次迭代并不多。通常,对于每次迭代仅运行1c的东西,我通常会至少使用100M。简单运行一次微基准测试通常需要0.1到1秒,这对于获得非常高的精度并隐藏启动开销通常是很好的。

顺便说一句,如果您将sysctl设置为sudo perf,则不需要kernel.perf_event_paranoid = 0。这样做肯定比一直使用sudo更好。