短环路时延
Latency of short loop
我试图理解为什么一些简单的循环 运行 会以它们的速度
第一种情况:
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
越看越不明白为什么throughput是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
).
在任何给定的迭代中,只需要上一次迭代的结果来生成本次迭代的结果。迭代内没有循环携带的依赖,只有迭代之间。
就像我解释的那样 ,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 只能宏融合无符号分支条件。我使用了一个大的循环计数器,所以我不需要在这之外再循环一次。 (IDK 为什么你选择了一个像 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
所以它 运行s 在每个周期 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)。
但是,循环中的第 4 个 insn 也读取 ecx
会使它大大变慢。 Core2 每个时钟可以发出 4 微指令,即使(如 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。但是,pre-SnB CPU 存在更多瓶颈,例如 ROB 读取和寄存器读取瓶颈。 SnB 切换到物理寄存器文件消除了这些减速。
在你的第二个循环中,IDK 为什么它不在每 1.333c 的一次迭代中 运行。 insn 更新 rbx
不能 运行 直到该迭代的其他指令之后,但这就是乱序执行的目的。您确定它像每 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
我试图理解为什么一些简单的循环 运行 会以它们的速度
第一种情况:
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
越看越不明白为什么throughput是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
).
在任何给定的迭代中,只需要上一次迭代的结果来生成本次迭代的结果。迭代内没有循环携带的依赖,只有迭代之间。
就像我解释的那样 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 只能宏融合无符号分支条件。我使用了一个大的循环计数器,所以我不需要在这之外再循环一次。 (IDK 为什么你选择了一个像 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
所以它 运行s 在每个周期 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)。
但是,循环中的第 4 个 insn 也读取 ecx
会使它大大变慢。 Core2 每个时钟可以发出 4 微指令,即使(如 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。但是,pre-SnB CPU 存在更多瓶颈,例如 ROB 读取和寄存器读取瓶颈。 SnB 切换到物理寄存器文件消除了这些减速。
在你的第二个循环中,IDK 为什么它不在每 1.333c 的一次迭代中 运行。 insn 更新 rbx
不能 运行 直到该迭代的其他指令之后,但这就是乱序执行的目的。您确定它像每 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