考虑以下x86-64程序集:
inner:
...
ret
outer:
.top:
call inner
dec rdi
jnz .top
ret
函数outer
只是重复对函数call
inner
(其身体未显示 - 可能为空)。
call
中的一系列outer
说明以及ret
内的相应inner
指令是否构成了实际的依赖链(为了估算效果)?
这条链的形成方式不止一种。例如,ret
是否取决于前面call
指令的延迟,然后后续call
指令取决于ret
,形成call -> ret -> call
链?或者ret
可能是独立的,但call
不是,形成call -> call
链?如果有链,是通过内存,寄存器,堆栈引擎,返回地址预测器 1 ,还是什么?
动机:这个问题起源于对另一个问题的一系列评论,主要是this comment和之前的问题。
1 这里的术语可能有点不清楚:堆栈引擎通常被理解为处理转换rsp
- 将指令修改为适当的单一访问偏移量,以便push rax; push rbx
可以转换为类似mov [t0], rax; mov [t0 - 8], rbx
的内容,其中t0
是一些临时寄存器,在某个时刻捕获rsp
的值。它还理解为call
和ret
指令处理类似的转换,它们以类似于push
和pop
的方式修改堆栈以及包括直接,间接(分别)跳跃。 CPU 还包括一种预测返回间接跳转的机制,它在“#stack; stack engine" - 但在这里我将其分成"返回地址预测器"。
答案 0 :(得分:7)
不,分支预测+推测执行会破坏存储/重新加载依赖性。
前端(推测性地)从返回地址预测器中知道RIP。因此,下一个call
指令可以在不等待ret
执行的情况下推送返回地址(并实际加载并验证预测返回地址对堆栈数据的正确性)。
推测商店可以进入商店缓冲区并进行商店转发。
当然存在一个依赖链,它不是循环传播的。乱序执行通过保持多次迭代来隐藏它。
证明: call
商店中断本来就是循环内存依赖关系链。
align 64
global _start
_start:
mov ebp, 250000000 ; I had been unrolling by 4, should have changed this to 5000... before measuring, but forgot.
align 32
.mainloop:
call delay_retaddr
call delay_retaddr
dec ebp
jg .mainloop
xor edi,edi
mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h
syscall ; sys_exit_group(0)
;; Placing this function *before* _start, or increasing the alignment,
;; makes it somewhat slower!
align 32
delay_retaddr:
add qword [rsp], 0
add qword [rsp], 0 ; create latency for the ret addr
ret
汇编并链接yasm -felf64 -Worphan-labels -gdwarf2 foo.asm && ld -o foo foo.o
,生成静态ELF二进制文件。
使用ocperf.py描绘(在i7-6700k上),每核心时钟周期 0.99指令:
$ taskset -c 3 ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,dsb2mite_switches.penalty_cycles -r2 ./foo
Performance counter stats for './foo' (2 runs):
645.770390 task-clock (msec) # 1.000 CPUs utilized ( +- 0.05% )
1 context-switches # 0.002 K/sec ( +-100.00% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.004 K/sec ( +- 20.00% )
2,517,412,984 cycles # 3.898 GHz ( +- 0.09% )
1,250,159,413 branches # 1935.919 M/sec ( +- 0.00% )
2,500,838,090 instructions # 0.99 insn per cycle ( +- 0.00% )
4,010,093,750 uops_issued_any # 6209.783 M/sec ( +- 0.03% )
7,010,150,784 uops_executed_thread # 10855.485 M/sec ( +- 0.02% )
62,838 dsb2mite_switches_penalty_cycles # 0.097 M/sec ( +- 30.92% )
0.645899414 seconds time elapsed ( +- 0.05% )
使用_start
之前的被调用函数和128
的对齐值,IPC可以从0.99下降到0.84,这是非常奇怪的。 dsb2mite交换机的计数仍然很低,因此它仍然主要从uop缓存运行,而不是传统的解码器。 (此Skylake CPU具有禁用循环缓冲区的微代码更新,以防与所有此类跳转相关。)
为了保持良好的吞吐量,CPU必须保持内循环的多次迭代,因为我们已经显着延长了需要重叠的独立dep链。
将add [rsp], 0
指令更改为[rsp+16]
会在不同的位置创建一个循环承载的依赖关系链,而call
不存储该指令}。因此,存储转发延迟的循环瓶颈会以〜半速运行。
# With add qword [rsp+16], 0
Performance counter stats for './foo' (2 runs):
1212.339007 task-clock (msec) # 1.000 CPUs utilized ( +- 0.04% )
2 context-switches # 0.002 K/sec ( +- 60.00% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.002 K/sec
4,727,361,809 cycles # 3.899 GHz ( +- 0.02% )
1,250,292,058 branches # 1031.306 M/sec ( +- 0.00% )
2,501,537,152 instructions # 0.53 insn per cycle ( +- 0.00% )
4,026,138,227 uops_issued_any # 3320.967 M/sec ( +- 0.02% )
7,026,457,222 uops_executed_thread # 5795.786 M/sec ( +- 0.01% )
230,287 dsb2mite_switches_penalty_cycles # 0.190 M/sec ( +- 68.23% )
1.212612110 seconds time elapsed ( +- 0.04% )
请注意,我仍在使用RSP相对地址,因此仍然存在堆栈同步uop。我可以通过使用相对于不同寄存器的地址(例如rbp
)来解决call
/ ret
存储/重新加载的位置,从而避免两种情况相同并避免使用它们。返回地址。
我不认为存储转发的可变延迟(在简单的背靠背重新加载立即情况下更糟)足以解释差异。 Adding a redundant assignment speeds up code when compiled without optimization。这是破坏依赖性的2倍加速因素。 ( 0.99 IPC vs. 0.53 IPC,使用相同的指令只是不同的寻址模式。)
在寻址模式下,disp8
的指令长1个字节,在更快的版本中有前端的奇怪对齐,但移动的东西似乎没有改变任何东西[rsp+16]
版本。
使用创建存储转发停顿的版本(使用add dword [rsp], 0
)会使dep链过长,以至于OoO exec无法轻松隐藏。我没有玩这么多钱。