我在在线资源中发现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
。
我还尝试将第二个inc
从inc rbx
更改为inc rax
,这使其依赖于第一个inc
。结果的确接近20,000,000 cycles
,这表明依赖关系将延迟一条指令,使它们不能同时运行。那么jnz
为什么很特别?
我在这里缺少什么?
答案 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
更好。