一系列x86调用/ ret指令是否形成依赖链?

时间:2018-03-09 23:41:14

标签: performance assembly x86

考虑以下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的值。它还理解为callret指令处理类似的转换,它们以类似于pushpop的方式修改堆栈以及包括直接,间接(分别)跳跃。 CPU 包括一种预测返回间接跳转的机制,它在“#stack; stack engine" - 但在这里我将其分成"返回地址预测器"。

1 个答案:

答案 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无法轻松隐藏。我没有玩这么多钱。