我试图理解为什么一些简单的循环以它们的速度运行
第一种情况:
L1:
add rax, rcx # (1)
add rcx, 1 # (2)
cmp rcx, 4096 # (3)
jl L1
根据IACA ,吞吐量为 1 个周期,瓶颈为端口 1、0、5。 我不明白为什么是1周期。毕竟我们有两个循环携带的依赖项:
(1) -> (1) ( Latancy is 1)
(2) -> (2), (2) -> (1), (2) -> (3) (Latency is 1 + 1 + 1).
这个延迟是循环携带的,所以它应该会减慢我们的迭代速度。
Throughput Analysis Report
--------------------------
Block Throughput: 1.00 Cycles Throughput Bottleneck: Port0, Port1, Port5
Port Binding In Cycles Per Iteration:
-------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 |
-------------------------------------------------------------------------
| Cycles | 1.0 0.0 | 1.0 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.0 |
-------------------------------------------------------------------------
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | |
---------------------------------------------------------------------
| 1 | 1.0 | | | | | | CP | add rax, rcx
| 1 | | 1.0 | | | | | CP | add rcx, 0x1
| 1 | | | | | | 1.0 | CP | cmp rcx, 0x1000
| 0F | | | | | | | | jl 0xfffffffffffffff2
Total Num Of Uops: 3
第二种情况:
L1:
add rax, rcx
add rcx, 1
add rbx, rcx
cmp rcx, 4096
jl L1
Block Throughput: 1.65 Cycles Throughput Bottleneck: InterIteration
Port Binding In Cycles Per Iteration:
-------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 |
-------------------------------------------------------------------------
| Cycles | 1.4 0.0 | 1.4 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.3 |
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | |
---------------------------------------------------------------------
| 1 | 0.6 | 0.3 | | | | | | add rax, rcx
| 1 | 0.3 | 0.6 | | | | | CP | add rcx, 0x1
| 1 | 0.3 | 0.3 | | | | 0.3 | CP | add rbx, rcx
| 1 | | | | | | 1.0 | CP | cmp rcx, 0x1000
| 0F | | | | | | | | jl 0xffffffffffffffef
我越不明白为什么吞吐量是1.65。
最佳答案
在第一个循环中,有两个 dep 链,一个用于 rax
,一个用于 rcx
。
add rax, rcx # depends on rax and rcx from the previous iteration, produces rax for the next iteration
add rcx, 1 # latency = 1
add rcx,1
-> add rax, rcx
的 2 周期延迟 dep 链跨越 2 次迭代(因此它已经有时间发生),但它不是无论如何,甚至循环进位(因为 rax
不会反馈到 add rcx,1
)。
在任何给定的迭代中,只需要前一次迭代的结果即可生成本次迭代的结果。迭代内不存在循环携带的依赖关系,仅在迭代之间存在。
就像我解释的那样in answer to your question a couple days ago ,cmp/jcc
不是循环携带的 dep 链的一部分。
cmov
或 setcc
读取其生成的标志输出,cmp
仅是 dep 链的一部分。控制依赖关系是可以预测的,而不是像数据依赖关系那样等待。
实际上,在我的 E6600(第一代 Core2,我目前没有可用的 SnB)上:
; Linux initializes most registers to zero on process startup, and I'm lazy so I depended on this for this one-off test. In real code, I'd xor-zero ecx
global _start
_start:
L1:
add eax, ecx ; (1)
add ecx, 1 ; (2)
cmp ecx, 0x80000000 ; (3)
jb L1 ; can fuse with cmp on Core2 (in 32bit mode)
mov eax, 1
int 0x80
我将其移植到32位,因为Core2只能在32位模式下进行宏融合,并使用jb
,因为Core2只能对无符号分支条件进行宏融合。我使用了一个大循环计数器,所以我不需要在此之外的另一个循环。 (不知道为什么你选择了像 4096 这样的小循环计数。你确定你没有测量来自短循环之外的其他东西的额外开销吗?)
$ yasm -Worphan-labels -gdwarf2 -felf tinyloop.asm && ld -m elf_i386 -o tinyloop tinyloop.o
$ perf stat -e task-clock,cycles,instructions,branches ./tinyloop
Performance counter stats for './tinyloop':
897.994122 task-clock (msec) # 0.993 CPUs utilized
2,152,571,449 cycles # 2.397 GHz
8,591,925,034 instructions # 3.99 insns per cycle
2,147,844,593 branches # 2391.825 M/sec
0.904020721 seconds time elapsed
因此它每个周期运行 3.99 个 insns,这意味着每个周期进行一次迭代。
如果您的 Ivybridge 运行该代码的速度只有一半左右,我会感到惊讶。更新:根据聊天中的讨论,是的,看来 IVB 确实只获得 2.14 IPC。 (每 1.87c 一次迭代)。 将 add rax, rcx
更改为 add rax, rbx
或其他内容以消除上一次迭代中对循环计数器的依赖,使吞吐量高达 3.8 IPC (每 1.05c) 一次迭代。我不明白为什么会发生这种情况。
使用不依赖于宏融合的类似循环,(add
/inc ecx
/jnz
)我也得到了一个每 1c 迭代一次。 (每个周期 2.99 个insns)。
但是,循环中的第四个 insn 也读取 ecx
会使其速度大大减慢。 Core2 每个时钟可以发出 4 个 uop,尽管(如 SnB/IvB)它只有三个 ALU 端口。 (很多代码都包含内存微指令,因此这确实有意义。)
add eax, ecx ; changing this to add eax,ebx helps when there are 4 non-fusing insns in the loop
; add edx, ecx ; slows us down to 1.34 IPC, or one iter per 3c
; add edx, ebx ; only slows us to 2.28 IPC, or one iter per 1.75c
; with neither: 3 IPC, or one iter per 1c
inc ecx
jnz L1 # loops 2^32 times, doesn't macro-fuse on Core2
我预计仍以 3 个 IPC 运行,即每 4/3 = 1.333c 运行 1 个 iter。然而,SnB 之前的 CPU 存在更多瓶颈,例如 ROB 读取和寄存器读取瓶颈。 SnB 改用物理寄存器文件消除了这些速度下降的情况。
在第二个循环中,我不知道为什么它不按 1.333c 运行一次迭代。更新 rbx 的 insn 直到该迭代中的其他指令之后才能运行,但这就是乱序执行的目的。您确定它慢到每 1.85 个周期迭代一次吗?您使用 perf
测量了足够高的计数以获得有意义的数据? (rdtsc
周期计数不准确,除非您禁用 Turbo 和频率缩放,但性能计数器仍会计算实际的核心周期)。
我没想到它会与
有太大不同L1:
add rax, rcx
add rbx, rcx # before/after inc rcx shouldn't matter because of out-of-order execution
add rcx, 1
cmp rcx, 4096
jl L1
关于performance - 短环路时延,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36840527/