两个可熔断对可以在同一时钟周期内解码吗?

moe*_*ep0 5 cpu x86 assembly intel cpu-architecture

我正在尝试使用我的 Intel i7-10700 和 ubuntu 20.04 来验证两个可熔断对可以在同一时钟周期内解码的结论。

测试代码排列如下,复制了8000次左右,以避免LSD和DSB的影响(主要使用MITE)。

ALIGN 32
.loop_1:
    dec ecx
    jge .loop_2
.loop_2:
    dec ecx
    jge .loop_3
.loop_3:
    dec ecx
    jge .loop_4
.loop_4:
.loop_5:
    dec ecx
    jge .loop_6
Run Code Online (Sandbox Code Playgroud)

测试结果表明,单个循环中仅融合一对。( r479 div r1002479 )

 Performance counter stats for process id '22597':

   120,459,876,711      cycles                                                      
    35,514,146,968      instructions     #    0.29  insn per cycle         
    17,792,584,278      r479             # r479: Number of uops delivered                     
                                         # to Instruction Decode Queue (IDQ) from MITE path                                  
        50,968,497      r4002479        
                                         
                                                  
    17,756,894,879      r1002479         # r1002479: Cycles MITE is delivering any Uop                                              

      26.444208448 seconds time elapsed
Run Code Online (Sandbox Code Playgroud)

我不认为阿格纳的结论是错误的。因此,我的性能使用是否有问题,或者我未能在代码中找到见解?

Pet*_*des 6

在 Haswell 及更高版本上,是的。在常春藤桥和更早的地方,没有。

Agner Fog 表示,在 Ice Lake 及之后的版本中,宏融合是在解码立即完成的,而不是在解码器中完成,后者需要预解码器将正确的 x86 机器代码块相应地发送到解码器。(Ice Lake 的限制略有不同:与以前的 CPU 型号不同,带有内存操作数的指令无法融合。带有立即数操作数的指令可以融合。)因此在 Ice Lake 上,宏融合不允许解码器处理超过 5 条指令每个时钟。

Wikichip声称在 Ice Lake 上每个时钟只能进行 1 次宏融合,但这可能是不正确的。Harold在 Rocket Lake 上使用我的微基准测试进行测试,发现与 Skylake 相同的结果。(Rocket Lake使用 Cypress Cove 核心,这是 Sunny Cove 的变体,向后移植到 14nm 工艺,因此在这方面它很可能与 Ice Lake 相同。)


您的结果表明uops_issued.any大约是一半instructions,因此您看到大多数对的宏观融合。(您还可以查看uops_retired.macro_fusedperf 事件。顺便说一句,现代perf对大多数 uarch 特定事件都有符号名称:使用perf list来查看它们。)

不过,解码器在 Skylake 衍生的微架构上每个时钟仍会产生多达 4 个甚至 5 个 uops,即使它们只进行两次宏融合。您没有查看 MITE 有多少个周期处于活动状态,因此在大多数情况下您看不到执行停止,直到 ROB / RS 中有空间容纳 4 个 uops 的问题组。这会在 IDQ 中为 MITE 的解码组开辟空间。


您的循环中还存在其他三个瓶颈:

  • 循环传递依赖dec ecx:只有 1 个/时钟,因为每个时钟dec都必须等待前一个时钟的结果准备好。

  • 每个周期只能执行一个采取的dec分支(在端口 6 上),几乎每次都会采取/ jge,除了 2^32 中的 1(当 dec 之前 ECX 为 0 时)。
    端口 0 上的另一个分支执行单元仅处理预测未采用的分支。 https://www.realworldtech.com/haswell-cpu/4/显示了布局,但没有提及该限制;Agner Fog的微架构指南就是如此。

  • 分支预测:即使跳转到下一条指令(在架构上是 NOP),CPU 也不是特殊情况。慢速 jmp 指令(因为真正的代码没有理由这样做,除了call +0/ pop,至少对于返回地址预测器堆栈来说是特殊情况。)

    这就是为什么每个时钟执行的指令远少于一条指令,更不用说每个时钟执行一个微指令了。


每个时钟 2 次融合的工作演示

令我惊讶的是,MITE并没有继续解码单独的信号test,并且jcc在进行两次融合的同一周期中。我猜解码器已针对填充 uop 缓存进行了优化。(对 Sandybridge / IvyBridge 的类似影响是,如果解码组的最后一个 uop 可能是可熔断的,例如dec,则解码器将在该周期中仅产生 3 个 uop,预计可能会熔断下一个dec周期。至少在 SnB/ 上是这样。 IvB,其中解码器每个周期只能进行 1 次融合,并且如果同一解码组中存在另一对微指令,则将解码单独的 ALU + jcc uop。这里,SKL 选择在生成两个微指令后不解码单独的微指令(testjcc另一个test)融合。)

global _start
_start:
   mov ecx, 100000000
ALIGN 32
.loop:
%rep 399          ; the loop branch makes 400 total
   test ecx, ecx
   jz  .exit_loop        ; many of these will be 6-byte jcc rel32
%endrep
   dec  ecx
   jnz  .loop

.exit_loop:
   mov eax, 231
   syscall          ; exit_group(EDI)
Run Code Online (Sandbox Code Playgroud)

在 i7-6700k Skylake 上,仅针对用户空间的性能计数器:

$ nasm -felf64 fusion.asm && ld fusion.o -o fusion       # static executable
$ taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,idq.all_mite_cycles_any_uops,idq.mite_uops -r2 ./fusion

 Performance counter stats for './fusion' (2 runs):

          5,165.34 msec task-clock                #    1.000 CPUs utilized            ( +-  0.01% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
                 1      page-faults               #    0.194 /sec                   
    20,130,230,894      cycles                    #    3.897 GHz                      ( +-  0.04% )
    80,000,001,586      instructions              #    3.97  insn per cycle           ( +-  0.00% )
    40,000,677,865      uops_issued.any           #    7.744 G/sec                    ( +-  0.00% )
    40,000,602,728      uops_executed.thread      #    7.744 G/sec                    ( +-  0.00% )
    20,100,486,534      idq.all_mite_cycles_any_uops #    3.891 G/sec                    ( +-  0.00% )
    40,000,261,852      idq.mite_uops             #    7.744 G/sec                    ( +-  0.00% )

          5.165605 +- 0.000716 seconds time elapsed  ( +-  0.01% )
Run Code Online (Sandbox Code Playgroud)

未采用的分支不是瓶颈,也许是因为我的循环足够大,足以击败 DSB(uop 缓存),但又不会太大而无法击败分支预测。(实际上, Skylake 上的JCC 勘误缓解肯定会击败 DSB:如果一切都是宏融合分支,那么就会有一个触及每个 32 字节区域的末尾。只有当我们开始在分支之间引入 NOP 或其他指令时, uop 缓存能够运行。)

我们可以看到所有内容都已融合(40G 微指令中的 80G 指令),并以每个时钟 2 个测试和分支微指令(20G 周期)执行。此外,MITE 每个周期都提供 uops,即 20G MITE 周期。它所提供的显然是每个周期 2 uops,至少平均而言是这样。

使用交替的 NOP 组和未采用的分支进行测试可能会很好地了解当 IDQ 有空间从 MITE 接受更多微指令时会发生什么,看看它是否会将非融合测试和 JCC 微指令发送到 IDQ。


进一步测试:

所有分支jcc rel8的向后没有区别,相同的性能结果:

%assign i 0 
%rep 399          ; the loop branch makes 400 total
   .dummy%+i:
   test ecx, ecx
   jz  .dummy %+ i
   %assign i i+1
%endrep
Run Code Online (Sandbox Code Playgroud)

MITE 吞吐量:交替的 NOP 组和宏融合分支

NOP 仍然需要解码,但后端可以破解它们。这使得总 MITE 吞吐量成为唯一的瓶颈,而不是限制在 2 uops/时钟,无论可以产生多少 MITE。

global _start
_start:
   mov ecx, 100000000
ALIGN 32
.loop:
%assign i 0 
%rep 10
 %rep 8
   .dummy%+i:
   test ecx, ecx
   jz  .dummy %+ i
   %assign i i+1
 %endrep
 times 24 nop
%endrep

   dec  ecx
   jnz  .loop

.exit_loop:
   mov eax, 231
   syscall          ; exit_group(EDI)
Run Code Online (Sandbox Code Playgroud)
 Performance counter stats for './fusion':

          2,594.14 msec task-clock                #    1.000 CPUs utilized          
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
                 1      page-faults               #    0.385 /sec                   
    10,112,077,793      cycles                    #    3.898 GHz                    
    40,200,000,813      instructions              #    3.98  insn per cycle         
    32,100,317,400      uops_issued.any           #   12.374 G/sec                  
     8,100,250,120      uops_executed.thread      #    3.123 G/sec                  
    10,100,772,325      idq.all_mite_cycles_any_uops #    3.894 G/sec                  
    32,100,146,351      idq.mite_uops             #   12.374 G/sec                  

       2.594423202 seconds time elapsed

       2.593606000 seconds user
       0.000000000 seconds sys
Run Code Online (Sandbox Code Playgroud)

所以看来 MITE 无法跟上 4-wide 的问题。 8 个分支的块使解码器每个时钟产生的微指令明显少于 5 个;可能只有 2 个像我们在较长时间运行中看到的那样test/jcc

24 nops 可以解码

减少到 3 个测试/jcc 和 29 个测试组,nop可将 MITE 活动 8.600 个周期降至 8.607 Gcycle,并具有 32.100G MITE uops。(3.099 G uops_retired.macro_fused,其中 .1 来自循环分支。)仍然没有使前端达到每个时钟 4.0 uops 的饱和度,就像我希望在一个解码组末尾进行宏融合时那样。
它达到了 4.09 IPC,因此至少解码器和问题瓶颈领先于没有宏融合的情况。
(宏融合的最佳情况是 6.0 IPC,每个周期有 2 个融合,以及来自非融合指令的 2 个其他微指令。这与通过微融合的未融合域后端微指令吞吐量限制分开,请参阅此测试,每个周期7 个微指令uops_executed.thread。)

即使%rep 2test/JCC 也会损害吞吐量,这似乎表明它在进行 2 次融合后只是停止解码,甚至在此之后不再解码 2 或 3 个 NOP。(对于一些较低的 NOP 计数,我们会得到一些 uop 缓存活动,因为外部代表计数不足以完全填满 uop 缓存。)

您可以在 shell 循环中测试它,就像for NOPS in {0..20}; do nasm ... -DNOPS=$NOPS ...使用源代码一样times NOPS nop

总周期与 NOPS 数量之间存在一些平台/阶跃效应%rep 2,因此可能两个测试/JCC uops 在组末尾解码,在它们之前有 1、2 或 3 个 NOP。(但这并不是非常一致,特别是对于较低数量的 NOPS。但是 NOPS=16、17 和 18 都在 5.22 Gcycle 左右,14 和 15 都是 4.62 Gcycle。)

如果我们想真正了解正在发生的情况,有很多可能相关的性能计数器,例如idq_uops_not_delivered.cycles_fe_was_ok(问题阶段获得 4 uops 的周期,或者后端停滞的情况,因此这不是前端的错.)