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

Can two fuseable pairs be decoded in the same clock cycle?

我正在尝试使用我的英特尔 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

测试结果表明,一个周期内只有一对融合。 ( 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

我不认为阿格纳的结论是错误的。因此,我的perf使用有问题,还是我没有在代码中找到见解?

在 Haswell 及之后,是的。在 Ivy Bridge 和更早的时候,没有。

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

Wikichip claims that only 1 macro-fusion per clock is possible on Ice Lake, but that's probably incorrect. with my microbenchmark on Rocket Lake and found the same results as Skylake. (Rocket Lake 使用 Cypress Cove 内核,这是 Sunny Cove 的一种变体,向后移植到 14nm 工艺,因此在这方面它很可能与 Ice Lake 相同。)


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

解码器在 Skylake 派生的微体系结构上仍将产生每个时钟最多四个甚至五个微指令,即使它们只进行两个宏融合。您没有查看有多少 cycles MITE 处于活动状态,因此大多数时候您看不到执行停滞,直到 ROB / RS 中有问题组的空间4微秒。这会在 IDQ 中为 MITE 的解码组打开 space。


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

  • 通过dec ecx的循环携带依赖:只有1/clock,因为每个dec必须等待前一个的结果就绪。

  • 每个周期只能执行一个taken分支(在端口6上),并且dec/jge几乎每个周期都被执行时间,除了 2^32 中的 1,当 ECX 在 12 月之前为 0 时。
    端口 0 上的另一个分支执行单元只处理预测未采用的分支。 https://www.realworldtech.com/haswell-cpu/4/ shows the layout but doesn't mention that limitation; Agner Fog 的微架构指南确实如此。

  • 分支预测:即使跳转到下一条指令(在架构上是 NOP)也不是 CPU 的特殊情况。 (Because there's no reason for real code to do this, except for call +0 / pop which is special cased at least for the return-address predictor stack.)

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


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

令我惊讶的是,MITE 没有 在同一周期内继续解码单独的 testjcc,因为它进行了两次融合。我想解码器已针对填充 uop 缓存进行了优化。 (对 Sandybridge / IvyBridge 的类似影响是,如果解码组的最终 uop 可能是可融合的,例如 dec,解码器将仅在该周期产生 3 uops,预计可能会融合 dec下一个周期。至少在 SnB/IvB 上是这样,解码器每个周期只能进行 1 次融合,如果同一解码组中有另一对,则将解码单独的 ALU + jcc uops。这里,SKL 选择不在进行两次融合后解码一个单独的 test uop(以及 jcc 和另一个 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)

在 i7-6700k Skylake 上,用户的性能计数器 - space 仅:

$ 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% )

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

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

使用交替的 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

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

NOP 仍然需要解码,但后端可以通过它们进行处理。这使得总 MITE 吞吐量成为唯一的瓶颈,而不是被限制在 2 微指令/时钟,无论有多少 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)
 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

所以 MITE 似乎跟不上 4-wide 的问题。 8 个分支的块使解码器产生的每个时钟明显少于 5 微指令;可能只有 2 个像我们看到的更长的 test/jcc.

24 个 nop 可以解码

减少到 3 个 test/jcc 和 29 个 nop 的组将它降低到 8.607 Gcycles MITE 活动 8.600 周期,32.100G MITE uops。 (3.099 G uops_retired.macro_fused,.1 来自循环分支。)仍然没有使前端以每个时钟 4.0 微指令饱和,就像我希望它可能在一次解码结束时进行宏融合一样群.
它达到了 4.09 IPC,所以至少解码器和问题瓶颈领先于没有宏融合的地方。
(宏融合的最佳情况是 6.0 IPC,每个周期有 2 个融合和 2 个来自非融合指令的其他 uops。这与通过微融合的未融合域后端 u​​op 吞吐量限制是分开的,参见 this test for ~7 uops_executed.thread per clock。 )

甚至%rep 2test/JCC都会影响吞吐量,这似乎表明它只是在进行 2 次融合后停止解码,甚至没有解码 2 或 3 次以上之后的NOP。 (对于一些较低的 NOP 计数,我们得到一些 uop-cache activity 因为外部 rep 计数不足以完全填满 uop 缓存。)

您可以使用 times NOPS nop.

for NOPS in {0..20}; do nasm ... -DNOPS=$NOPS ... 这样的 shell 循环中测试它

总周期与 %rep 2 的 NOPS 数量有一些 plateau/step 影响,所以可能这两个 test/JCC 微指令在一组的末尾解码,其中 1 、2 或 3 个 NOP 在它们之前。 (但它不是非常一致,特别是对于较低数量的 NOPS。但是 NOPS=16、17 和 18 都在 5.22 Gcycles 左右,14 和 15 都在 4.62 Gcycles。)

如果我们想真正了解正在发生的事情,例如,有很多可能相关的性能计数器。 idq_uops_not_delivered.cycles_fe_was_ok(问题阶段得到 4 微指令的周期,或者后端停滞的地方,所以这不是前端的错。)