2

我在在线资源中发现 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 条独立指令(2inc和 1 dec)使用 ALU,所以它们一起计算 1 个周期。

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

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

我在这里缺少什么?

4

1 回答 1

3

首先,dec/jnz将宏融合到英特尔 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 CPU 上只有 3 个融合域微指令,因此 SnB/IvB 也可以以每个时钟 1 个运行它。)

请参阅Agner Fog 的优化指南,尤其是他的微架构指南。还有https://stackoverflow.com/tags/x86/info中的其他链接。


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

乱序执行和分支预测+推测执行隐藏了控制依赖的“延迟”。即,下一次迭代可以在 CPU 验证确实jnz应该执行之前开始运行。

因此,每个指令在验证预测之前都jnz对前一个有输入依赖性dec rcx,但后面的指令不必等待它被检查后才能执行。按顺序退休可确保在任何事情“看到”它发生之前发现错误推测(导致 Spectre 攻击的微架构影响除外......)


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

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

于 2019-01-04T09:00:27.790 回答