assembly x86 uica报告ret指令的uop计数较高,与其他来源不一致

6ojccjat  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(126)

(one片段用于减少链接的跟踪数,实际吞吐量无关紧要,只有uop/指令重要)
这一系列的指令:

ret

    ret 4

    pop rcx
    jmp rcx

字符串
在haswell uarch上生成以下跟踪(因为这是我所拥有的,我将坚持它,但类似的跟踪也发生在其他uarch上):https://uica.uops.info/tmp/96c206fda86a42b6abcef52bd088ac13_trace.html
为什么retret imm16需要这么多uop?特别是与pop jmp相比?
this post声明pop jmpret是等效的,这确实是通过基准测试这段代码所观察到的:

section .text

global _start

_start:
    mov   rax, 0xFFFFFFF

.lp:
    call  .tst

    sub   rax, 1
    jge   .lp

    mov   eax, 0x3C
    xor   edi, edi
    syscall

.tst:
    pop   rcx
    jmp   rcx
;   ret


其中,ret版本大约需要1.3G周期,pop jmp版本也大约需要1.3G周期
xml file with instruction info for ret without immiediate for HSW有这个

<architecture name="HSW">
[...]
    ports="6*p06+6*p15+2*p23+2*p237+3*p4" uops="20" uops_MITE="4" uops_MS="6" uops_retire_slots="2"/>


这与UICA显示的顺序不完全相同,但在其他方面相同
Fog's instruction tables page 250表示ret使用p237和p6,这与pop jmp大致相同
此外,ret使用的端口也是意外的,顺序如下:
我不记得uica paper的任何内容,但我最近没有重读这篇论文,只是浏览了一下,寻找关键词
我没有检查source code,因为AFAIK所有指令数据都在上面提到的XML文件中
因此,根据UICA的判断,这显然是错误的,除非有一个我不知道的原因,所以问题是,有这样的原因吗?

mfpqipee

mfpqipee1#

通常,在使用uiCA测量的块中包含ret是没有用的;它把你的代码当作一个循环体,不管它是否以jcc返回到顶部结束,也不管它是否包含无条件跳转。(如果没有跳转,就好像你要展开循环体,尽管对于JCC-erratum和uop-cache目的,它只使用一个副本的地址,就好像有一个免费执行的跳转一样。

uiCA使用来自https://uops.info/测量的数据。这些测量并不反映retcall配对的正常使用,只是在循环中单独使用ret作为一般的间接跳转。ret很难单独进行微基准测试,特别是在希望测试循环内连续代码片段的基准测试框架中,所以没有地方可以将call放到单独的ret中。(除了nanobench允许的“设置”代码的一部分;它可以包含在实际进入循环时跳转的ret。但uops.info目前没有这样做; @AndreasAbel可能对改进ret微基准测试感兴趣?)

认为Agner Fog在Haswell上对p237 p6端口的ret解码为1个微融合uop的测量是正确的;这与我在Skylake 1上的测量相匹配。(除了p7几乎肯定是不正确的,可能是他的电子表格中的错别字或复制/粘贴错误。端口7只是一个存储地址执行单元,它不能运行加载。)
Haswell上ret_nearhttps://uops.info/html-instr/RET_NEAR.html)的uops.info编号为

  • 28.91个周期的实际测量吞吐量,非常慢,可能是由于分支的错误预测。这将抛出除已退役的uop计数以外的任何uop计数,因为错误路径上的uop仍然会在错误预测的阴影下发出和执行。像uops_dispatched_port.port_6这样的事件的计数器不会区分继续退休的uop和被丢弃的uop。这对于微基准测试大多数指令,因为基准测试循环预测完美。
  • 2个前端uop/ 20个后端uop,用于端口6*p06+6*p15+2*p23+2*p237+3*p4。显然太高了;我们知道ret在正常使用时(与call配对)并没有那么慢。

uops.info的测试循环(例如,https://uops.info/html-tp/HSW/RET_NEAR-Measurements.html)使用RIP相关莱亚来获得“返回地址”,它用mov而不是push来存储该“返回地址”。

# uops.info microbenchmark loop body for ret_near, unroll_count=500
   0:   48 8d 05 05 00 00 00    lea    rax,[rip+0x5]        # 0xc
   7:   48 89 04 24             mov    QWORD PTR [rsp],rax
   b:   c3                      ret  
      lea_target:   # ret jumps here

字符串
使用mov而不是push意味着RSP堆栈不平衡。将显式[rsp]与隐式修改RSP的堆栈操作(如ret)混合使用意味着stack engine必须在每个mov之前插入堆栈同步uop。这可能是MS(微码序列器)uop。这是此循环的真正额外成本,但也会因错误预测而放大。

在Skylake上,当返回预测器堆栈为空时,ret福尔斯会返回到正常的间接分支预测。但我认为早期的CPU不会。(这对Spectre缓解很重要,如果在retpoline中间发生抢占或其他情况,IIRC使retpoline不是100%可靠。)uops.info在SKL上测量ret吞吐量约为10个周期,从Haswell上的~29个周期和Sandybridge上的~31个周期提高,可能是因为这个原因。Ice Lake恢复到~31个周期,桤木Lake P-核心下降到2.17个周期/ret,具有相同的循环。(也许冰湖放弃了正常的间接分支预测,但桤木湖把它带回来?或者发生了其他事情,比如在冰湖中的预测器中,一些分支可能彼此混淆。)

无论如何,ret尝试使用来自下溢预测器堆栈的不存在的预测可能会导致它在大部分时间内错误预测,并带来灾难性的后果。HSW和更早版本的每个端口uop故障在www.example.com上是无稽之谈uops.info,但对于SKL和更晚版本来说有些合理。仍然可能由于堆栈同步uop和可能不完美的分支预测而膨胀,因为在桤木湖之前似乎还有一家商店在清点。

Footnote 1:Skylake测量call/ret

我对Skylake的测量是,call rel32/ret对是3个融合域uop,5个需要执行单元的未融合域uop。uops.info和Agner Fog同意,call rel32单独是2个前端uop,作为3个未融合域uop运行。(跳转和存储地址+存储数据)。这使得ret成为预期的1个融合域uop:一个间接跳转微融合负载。我希望Agner Fog做了类似的事情来测量ret
我把一个或两个call test放在一个紧密循环(dec/jnz)中,在一个静态可执行文件中的perf stat下运行,其中test: ret是后来定义的。所有的ALU uop都是端口6。加载/存储是端口2,3,7的混合。(当然端口7 uop只来自call
https://uops.info/,其中在2个前端和7个后端单独测量SKL的ret_near。(可能端口故障包括来自其测试循环的额外错误推测的mov存储。)

; testloop.asm   - static executable for Linux
default rel
%use smartalign
alignmode p6, 64

global _start
_start:

    mov     ebp, 100000000
align 64
.loop:
    call test
    call test
    dec ebp
    jnz .loop
.end:

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)

align 32        ; in a separate 32-byte block to improve uop-cache hit rate, not that it matters much
test:
  ret
## on my i7-6700k Skylake, Linux 6.5 / perf 6.5
$ nasm -felf64 testloop.asm
$ ld -o testloop testloop.o
$ taskset -c 1 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,idq.mite_uops,branch-misses  ./testloop

 Performance counter stats for './testloop':

            282.37 msec task-clock                       #    0.998 CPUs utilized             
                 0      context-switches                 #    0.000 /sec                      
                 0      cpu-migrations                   #    0.000 /sec                      
                 1      page-faults                      #    3.541 /sec                      
     1,100,001,492      cycles                           #    3.896 GHz                       
       600,000,098      instructions                     #    0.55  insn per cycle            
       700,089,165      uops_issued.any                  #    2.479 G/sec                     
     1,100,088,709      uops_executed.thread             #    3.896 G/sec                     
         8,413,976      idq.mite_uops                    #   29.798 M/sec                     
                 8      branch-misses                                                         

       0.282836215 seconds time elapsed

       0.282460000 seconds user
       0.000000000 seconds sys

的数据

注解掉其中一条call指令会像预期的那样减少计数,每个iter有3个前端uop,5个后端uop。dec/jnz只是1个前端/ 1个后端uop,每次迭代总共有4个和6个uop。

; with only one call in the loop
            120.58 msec task-clock                       #    0.996 CPUs utilized             
       463,605,997      cycles                           #    3.845 GHz                       
       400,000,049      instructions                     #    0.86  insn per cycle            
       400,041,809      uops_issued.any                  #    3.318 G/sec                     
       600,041,674      uops_executed.thread             #    4.976 G/sec                     
       134,054,953      idq.mite_uops                    #    1.112 G/sec


在两次单独运行中使用uops_dispatched_port.port_0,uops_dispatched_port.port_1,uops_dispatched_port.port_5,uops_dispatched_port.port_6uops_dispatched_port.port_2,uops_dispatched_port.port_3,uops_dispatched_port.port_7的计数器,我们可以看到分布:

# 2 call/ret pairs + dec/jnz

             5,619      uops_dispatched_port.port_0      #   19.750 K/sec                     
             7,637      uops_dispatched_port.port_1      #   26.843 K/sec                     
             8,827      uops_dispatched_port.port_5      #   31.025 K/sec                     
       500,099,429      uops_dispatched_port.port_6      #    1.758 G/sec                     

       164,323,545      uops_dispatched_port.port_2      #  581.175 M/sec                     
       135,457,372      uops_dispatched_port.port_3      #  479.082 M/sec                     
       100,224,734      uops_dispatched_port.port_7      #  354.472 M/sec


我不知道端口0、1和5上的几千个uop是从哪里来的。这个程序在用户空间中运行的指令都不能在它们上面运行。除非在中断发生时有某种堆栈同步。

相关问题