为什么jnz不计周期?
Why jnz counts no cycle?
我在网上查到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个周期。
但是我不明白的是为什么整个循环只有1个循环? jnz
取决于dec rcx
的结果,应该算1个循环,这样整个循环就是2个循环。我希望输出接近 20,000,000 cycles
.
我还尝试将第二个 inc
从 inc rbx
更改为 inc rax
,这使其依赖于第一个 inc
。结果确实变得接近20,000,000 cycles
,这表明依赖会延迟一条指令,以至于它们不能同时运行。那么为什么 jnz
很特别呢?
我在这里缺少什么?
首先,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 上,这仍然 运行 每个周期 1 个迭代,因为它们有 4 个整数执行端口来跟上每次迭代 4 微指令。 (你的宏融合循环在 Intel CPUs 上只有 3 个融合域微指令,所以 SnB/IvB 也可以 运行 每个时钟 1 个。)
见Agner Fog's optimization guide and especially his microarch guide. Also other links in https://whosebug.com/tags/x86/info。
控制依赖被分支预测+推测执行隐藏,不像数据依赖。
乱序执行和分支预测+推测执行隐藏了控制依赖的"latency"。即下一次迭代可以在 CPU 验证确实应该采用 jnz
之前开始 运行ning。
所以每个jnz
在验证预测之前都对前一个dec rcx
有输入依赖,但是后面的指令不必等到它被检查才可以执行。按顺序 retirement 确保在任何事情 "see" 发生之前捕获错误推测(除了导致 Spectre 攻击的微架构影响...)
1000 万次迭代并不多。对于 运行s 的东西,我通常至少使用 100M,每个 iter 仅 1c。有一个简单的 microbenchmark 运行 0.1 到 1 秒通常可以很好地获得非常高的精度并隐藏启动开销。
顺便说一句,如果您使用 sysctl 设置 kernel.perf_event_paranoid = 0
,则不需要 sudo perf
。这样做几乎肯定比一直使用 sudo
更好。
我在网上查到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个周期。
但是我不明白的是为什么整个循环只有1个循环? jnz
取决于dec rcx
的结果,应该算1个循环,这样整个循环就是2个循环。我希望输出接近 20,000,000 cycles
.
我还尝试将第二个 inc
从 inc rbx
更改为 inc rax
,这使其依赖于第一个 inc
。结果确实变得接近20,000,000 cycles
,这表明依赖会延迟一条指令,以至于它们不能同时运行。那么为什么 jnz
很特别呢?
我在这里缺少什么?
首先,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 上,这仍然 运行 每个周期 1 个迭代,因为它们有 4 个整数执行端口来跟上每次迭代 4 微指令。 (你的宏融合循环在 Intel CPUs 上只有 3 个融合域微指令,所以 SnB/IvB 也可以 运行 每个时钟 1 个。)
见Agner Fog's optimization guide and especially his microarch guide. Also other links in https://whosebug.com/tags/x86/info。
控制依赖被分支预测+推测执行隐藏,不像数据依赖。
乱序执行和分支预测+推测执行隐藏了控制依赖的"latency"。即下一次迭代可以在 CPU 验证确实应该采用 jnz
之前开始 运行ning。
所以每个jnz
在验证预测之前都对前一个dec rcx
有输入依赖,但是后面的指令不必等到它被检查才可以执行。按顺序 retirement 确保在任何事情 "see" 发生之前捕获错误推测(除了导致 Spectre 攻击的微架构影响...)
1000 万次迭代并不多。对于 运行s 的东西,我通常至少使用 100M,每个 iter 仅 1c。有一个简单的 microbenchmark 运行 0.1 到 1 秒通常可以很好地获得非常高的精度并隐藏启动开销。
顺便说一句,如果您使用 sysctl 设置 kernel.perf_event_paranoid = 0
,则不需要 sudo perf
。这样做几乎肯定比一直使用 sudo
更好。