微融合和寻址模式

使用英特尔®架构代码分析器(IACA),我发现了一些意想不到的情况(对我来说)。

以下指令使用[base+index]寻址

addps xmm1, xmmword ptr [rsi+rax*1]

根据IACA没有微熔丝。 但是,如果我这样使用[base+offset]

addps xmm1, xmmword ptr [rsi]

IACA报告说它确实融合了。

“英特尔优化参考手册”第2-11部分提供了以下内容作为“可由所有解码器处理的微型熔合微操作”

FADD DOUBLE PTR [RDI + RSI*8]

和Agner Fog的优化组装手册也给出了使用[base+index]寻址的微操作融合的例子。 例如,请参见第12.2节“Core2上的相同示例”。 那么正确的答案是什么?


在解码器和uop-cache中,寻址模式不会影响微融合(除了具有立即操作数的指令不能使RIP相对寻址模式微熔化)。

但是,uop和寻址模式的某些组合无法在ROB(无序内核)中保持微熔合,所以在必要时Intel SnB系列CPU“解叠”,在问题/重命名阶段。 对于问题吞吐量和无序窗口大小(ROB大小),重叠后的融合域计数是重要的。

英特尔的优化手册在第2.3.2.4节:微操作队列和循环流检测器(LSD)中描述了Sandybridge的未分层,但没有描述任何稍后的微体系结构的更改。


这些规则 ,我从SnB,HSW和SKL的实验中可以看出:

  • SnB(我也假设为IvB):索引寻址模式总是未叠加的,其他的保持微熔合。 IACA(主要?)是正确的。
  • HSW,SKL:如果它具有2个操作数并且将dst寄存器视为读 - 修改 - 写,它们仅保持索引ALU指令微熔合。 这里的“操作数”包括标志,这意味着adccmov不会微熔丝。 大多数VEX编码指令也不熔断,因为它们通常有三个操作数(所以paddb xmm0, [rdi+rbx]熔丝但vpaddb xmm0, xmm0, [rdi+rbx]不)。 最后,第一个操作数是只写的偶尔2操作数指令,例如pabsb xmm0, [rax + rbx]也不会融合。 IACA是错误的,应用了SnB规则。
  • 相关:简单(非索引)寻址模式是port7(Haswell和更高版本)上专用存储地址单元可以处理的唯一寻址模式,所以它仍然可能有助于避免商店的索引寻址模式。 (一个好的方法是用单个寄存器来寻址你的dst,但用dst+(initial_src-initial_dst) src,那么你只需要在一个循环内增加dst寄存器。)

    请注意,有些指令根本不是微型熔丝(即使在解码器/ uop-cache中也是如此)。 例如shufps xmm, [mem], imm8vinsertf128 ymm, ymm, [mem], imm8 ,通过Skylake在SnB上总是2个uops,即使它们的寄存器源版本只有1个uop。 对于带有imm8控制操作数加上通常的dest / src1,src2寄存器/内存操作数的指令,这是典型情况,但还有其他一些情况。 例如PSRLW/D/Q xmm,[mem] (来自存储器操作数的向量移位计数)不会微熔丝,PMULLD也不会。

    在阅读大量寄存器时,另请参见Agner Fog博客上关于HSW / SKL上的问题吞吐量限制的讨论: 大量与索引寻址模式的微融合可导致相对于具有较少寄存器操作数的相同指令的减速:寄存器寻址模式和立即数。 我们还不知道原因,但我怀疑某种寄存器读取限制,可能与读取PRF中的大量冷寄存器有关。


    测试案例,来自实际测量的数字 :这些解码器中的所有微型保险丝AFAIK,即使它们之后未被层压。

    # store
    mov        [rax], edi  SnB/HSW/SKL: 1 fused-domain, 2 unfused.  The store-address uop can run on port7.
    mov    [rax+rsi], edi  SnB: unlaminated.  HSW/SKL: stays micro-fused.  (The store-address can't use port7, though).
    mov [buf +rax*4], edi  SnB: unlaminated.  HSW/SKL: stays micro-fused.
    
    # normal ALU stuff
    add    edx, [rsp+rsi]  SnB: unlaminated.  HSW/SKL: stays micro-fused.  
    # I assume the majority of traditional/normal ALU insns are like add
    

    HSW / SKL可能不得不重叠的三输入指令

    vfmadd213ps xmm0,xmm0,[rel buf] HSW/SKL: stays micro-fused: 1 fused, 2 unfused.
    vfmadd213ps xmm0,xmm0,[rdi]     HSW/SKL: stays micro-fused
    vfmadd213ps xmm0,xmm0,[0+rdi*4] HSW/SKL: un-laminated: 2 uops in fused & unfused-domains.
         (So indexed addressing mode is still the condition for HSW/SKL, same as documented by Intel for SnB)
    
    # no idea why this one-source BMI2 instruction is unlaminated
    # It's different from ADD in that its destination is write-only (and it uses a VEX encoding)
    blsi   edi, [rdi]       HSW/SKL: 1 fused-domain, 2 unfused.
    blsi   edi, [rdi+rsi]   HSW/SKL: 2 fused & unfused-domain.
    
    
    adc         eax, [rdi] same as cmov r, [rdi]
    cmove       ebx, [rdi]   Stays micro-fused.  (SnB?)/HSW: 2 fused-domain, 3 unfused domain.  
                             SKL: 1 fused-domain, 2 unfused.
    
    # I haven't confirmed that this micro-fuses in the decoders, but I'm assuming it does since a one-register addressing mode does.
    
    adc   eax, [rdi+rsi] same as cmov r, [rdi+rsi]
    cmove ebx, [rdi+rax]  SnB: untested, probably 3 fused&unfused-domain.
                          HSW: un-laminated to 3 fused&unfused-domain.  
                          SKL: un-laminated to 2 fused&unfused-domain.
    

    我认为Broadwell的行为就像adlac / cmov的Skylake。

    奇怪的是HSW取消了内存源ADC和CMOV的层叠。 也许英特尔在达到运输Haswell的最后期限之前并没有考虑从SnB那里改变它。

    Agner的insn表格说cmovcc r,madc r,m在HSW / SKL上完全没有微熔丝,但这与我的实验不符。 我测量的周期数与熔融域uop问题计数匹配,出现4个uops / clock问题瓶颈。 希望他会仔细检查并纠正表格。

    内存 - 目标整数ALU

    add        [rdi], eax  SnB: untested (Agner says 2 fused-domain, 4 unfused-domain (load + ALU  + store-address + store-data)
                           HSW/SKL: 2 fused-domain, 4 unfused.
    add    [rdi+rsi], eax  SnB: untested, probably 4 fused & unfused-domain
                           HSW/SKL: 3 fused-domain, 4 unfused.  (I don't know which uop stays fused).
                      HSW: About 0.95 cycles extra store-forwarding latency vs. [rdi] for the same address used repeatedly.  (6.98c per iter, up from 6.04c for [rdi])
                      SKL: 0.02c extra latency (5.45c per iter, up from 5.43c for [rdi]), again in a tiny loop with dec ecx/jnz
    
    
    adc     [rdi], eax      SnB: untested
                            HSW: 4 fused-domain, 6 unfused-domain.  (same-address throughput 7.23c with dec, 7.19c with sub ecx,1)
                            SKL: 4 fused-domain, 6 unfused-domain.  (same-address throughput ~5.25c with dec, 5.28c with sub)
    adc     [rdi+rsi], eax  SnB: untested
                            HSW: 5 fused-domain, 6 unfused-domain.  (same-address throughput = 7.03c)
                            SKL: 5 fused-domain, 6 unfused-domain.  (same-address throughput = ~5.4c with sub ecx,1 for the loop branch, or 5.23c with dec ecx for the loop branch.)
    

    是的,没错, adc [rdi],eax / dec ecx / jnz运行速度比在SKL上使用add而不是adc循环快。 我没有尝试使用不同的地址,因为很显然,SKL不喜欢重复地址重写(存储转发延迟高于预期)。另见这篇文章,关于重复存储/重新加载到相同地址的速度比预期慢。

    内存目标adc是如此之多的uops,因为Intel P6系列(显然是SnB系列)不能为多uop指令的所有uop保留相同的TLB条目,所以它需要一个额外的uop来解决这个问题 - 加载和添加完成,然后存储故障,但insn不能重新启动,因为CF已经更新。 Andy Glew有趣的一系列评论(@krazyglew)。

    据推测,解码器中的融合和未分层可以使我们免于需要微码ROM,从adc [base+idx], reg 。的单条指令中生成超过4个融合域uops。


    为什么SnB系列不层压

    Sandybridge简化了内部uop格式以节省功耗和晶体管(同时使用物理寄存器文件进行主要更改,而不是将输入/输出数据保存在ROB中)。 SnB系列CPU只允许有限数量的输入寄存器用于无序内核中的融合域uop。 对于SnB / IvB,该限制是2个输入(包括标志)。 对于HSW及其后续版本而言,限额为3个输入。 我不确定内存目标addadc是否充分利用了这一点,或者如果英特尔不得不通过一些说明让Haswell走出门外

    Nehalem和更早的版本对于一个未融合域的uop有2个输入的限制,但是ROB可以明显地跟踪具有3个输入寄存器(非存储器寄存器操作数,基址和索引)的微混合微操作。


    因此索引存储和ALU +加载指令仍然可以高效地进行解码(不必是组中的第一个uop),并且不会在uop缓存中占用额外的空间,否则微融合的优势基本上不适用于调优紧圈。 “未分层”发生在4-fused-domain-uops-per-cycle问题/退休宽度乱序核心之前 。 融合域性能计数器(uops_issued / uops_retired.retire_slots)在拆分后计算融合域uops。

    英特尔对重命名人员的描述(第2.3.3.1节:更名)意味着这是问题/重命名阶段,实际上是进行非分层的,所以用于未分层的高级层仍然可以在28/56/64融合中进行微融合-domain uop问题队列/循环缓冲区(又名IDQ)。

    TODO:测试这个。 创建一个只能适应循环缓冲区的循环。 改变一些东西,这样在发布之前,其中一个uop将被解压缩,然后看它是否仍然从循环缓冲区(LSD)运行,或者现在所有的uops是从uop缓存(DSB)重新获取的。 有perf计数器来跟踪uop从哪里来,所以这应该很容易。

    更难的TODO:如果在从uop缓存读取和添加到IDQ之间发生拆分,测试它是否能减少uop-cache带宽。 或者如果在问题阶段发生分层问题,是否会影响问题吞吐量? (即它在发布第一个4后如何处理剩余的uops)


    (请参阅此答案的以前版本,了解基于调整某些LUT代码的一些猜测,关于vpgatherdd一些说明比pinsrw循环多出约pinsrw 。)

    SnB实验测试

    HSW / SKL号码是在i5-4210U和i7-6700k上测量的。 两者都启用了HT(但是系统闲置,因此线程本身具有整个内核)。 我使用ocperf.py在两个系统上运行相同的静态二进制文件,即SKL上的Linux 4.10和HSW上的Linux 4.8。 (HSW笔记本电脑NFS安装在我的SKL桌面/家中。)

    如下所述,在不再工作的i5-2500k上测量SnB值。

    通过使用性能计数器测试uops和周期来确认。

    我找到了英特尔Sandybridge的PMU事件表,用于Linux的perf命令。 (不幸的是,标准的perf没有大多数特定于硬件的PMU事件的象征名,比如uops。)我利用它来得到最近的答案。

    ocperf.py为这些uarch特定的PMU事件提供了符号名称,所以您不必查找表格。 此外,同一个符号名称适用于多个用户。 当我第一次写这个答案时,我并没有意识到这一点。

    为了测试uop微融合,我构建了一个测试程序,这个测试程序在英特尔CPU的每循环4次高频融合域极限上存在瓶颈。 为了避免任何执行端口争用,这些uop中的许多都是nop ,它们仍然位于uop缓存中,并通过与其他uop相同的流水线,除非它们不会被分派到执行端口。 (一个xor x, same或者被淘汰的举动将是相同的。)

    测试程序: yasm -f elf64 uop-test.s && ld uop-test.o -o uop-test

    GLOBAL _start
    _start:
        xor eax, eax
        xor ebx, ebx
        xor edx, edx
        xor edi, edi
        lea rsi, [rel mydata]   ; load pointer
        mov ecx, 10000000
        cmp dword [rsp], 2      ; argc >= 2
        jge .loop_2reg
    
    ALIGN 32
    .loop_1reg:
        or eax, [rsi + 0]
        or ebx, [rsi + 4]
        dec ecx
        nop
        nop
        nop
        nop
        jg .loop_1reg
    ;   xchg r8, r9     ; no effect on flags; decided to use NOPs instead
    
        jmp .out
    
    ALIGN 32
    .loop_2reg:
        or eax, [rsi + 0 + rdi]
        or ebx, [rsi + 4 + rdi]
        dec ecx
        nop
        nop
        nop
        nop
        jg .loop_2reg
    
    .out:
        xor edi, edi
        mov eax, 231    ;  exit(0)
        syscall
    
    SECTION .rodata
    mydata:
    db 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
    

    我还发现,如果环路不是4个uops的倍数,则循环缓冲区内的uop带宽不是每个周期4个常量。 (即它是abcabc ,...;不是abcabcab ,...)。 不幸的是,Agner Fog的微型文档在循环缓冲区的限制上并不清楚。 请参阅在执行uop数量不是处理器宽度倍数的循环时性能是否降低? 对HSW / SKL进行更多调查。 在这种情况下,SnB可能比HSW差,但我不确定,并且仍然没有工作的SnB硬件。

    我想将宏观融合(比较和分支)保留在图片之外,所以我在dec和分支之间使用了nop 。 我使用了4个nop ,所以在微融合时,循环将是8个uops,并且每次迭代以2个周期填充管道。

    在其他版本的循环中,使用不进行微熔丝的2操作数寻址模式,该循环将是10个熔融域uops,并在3个循环中运行。

    来自3.3GHz Intel Sandybridge(i5 2500k)的结果。 在测试之前,我没有做任何事情来让cpufreq调速器提高时钟速度,因为循环是不与内存进行交互的循环。 我已经为必须以十六进制输入的性能计数器事件添加了注释。

    测试1-reg寻址模式:no cmdline arg

    $ perf stat -e task-clock,cycles,instructions,r1b1,r10e,r2c2,r1c2,stalled-cycles-frontend,stalled-cycles-backend ./uop-test
    
    Performance counter stats for './uop-test':
    
         11.489620      task-clock (msec)         #    0.961 CPUs utilized
        20,288,530      cycles                    #    1.766 GHz
        80,082,993      instructions              #    3.95  insns per cycle
                                                  #    0.00  stalled cycles per insn
        60,190,182      r1b1  ; UOPS_DISPATCHED: (unfused-domain.  1->umask 02 -> uops sent to execution ports from this thread)
        80,203,853      r10e  ; UOPS_ISSUED: fused-domain
        80,118,315      r2c2  ; UOPS_RETIRED: retirement slots used (fused-domain)
       100,136,097      r1c2  ; UOPS_RETIRED: ALL (unfused-domain)
           220,440      stalled-cycles-frontend   #    1.09% frontend cycles idle
           193,887      stalled-cycles-backend    #    0.96% backend  cycles idle
    
       0.011949917 seconds time elapsed
    

    测试2-reg寻址模式:使用cmdline arg

    $ perf stat -e task-clock,cycles,instructions,r1b1,r10e,r2c2,r1c2,stalled-cycles-frontend,stalled-cycles-backend ./uop-test x
    
     Performance counter stats for './uop-test x':
    
             18.756134      task-clock (msec)         #    0.981 CPUs utilized
            30,377,306      cycles                    #    1.620 GHz
            80,105,553      instructions              #    2.64  insns per cycle
                                                      #    0.01  stalled cycles per insn
            60,218,693      r1b1  ; UOPS_DISPATCHED: (unfused-domain.  1->umask 02 -> uops sent to execution ports from this thread)
           100,224,654      r10e  ; UOPS_ISSUED: fused-domain
           100,148,591      r2c2  ; UOPS_RETIRED: retirement slots used (fused-domain)
           100,172,151      r1c2  ; UOPS_RETIRED: ALL (unfused-domain)
               307,712      stalled-cycles-frontend   #    1.01% frontend cycles idle
             1,100,168      stalled-cycles-backend    #    3.62% backend  cycles idle
    
           0.019114911 seconds time elapsed
    

    因此,两个版本都运行了80M指令,并将60M微处理器分派给执行端口。 ( or使用内存源为or分派ALU,并为负载分派一个加载端口,而不管它是否在流水线的其余部分是微融合的nop根本不派遣到执行端口)同样,两个版本都退出了100M未融合域的uops,因为这里有40M个nops。

    区别在于融合域的计数器。

  • 1寄存器地址版本仅发布并退役80M融合域uop。 这与指令数量相同。 每个insn变成一个融合域uop。
  • 2寄存器地址版本发布100M融合域uops。 这与未融合域uops的数量相同,表明没有发生微融合。
  • 我怀疑你只会看到UOPS_ISSUED和UOPS_RETIRED(退役插槽使用)之间的差异,如果分支预测失误导致uops在发布后被取消,但在退休之前。

    最后,性能影响是真实的。 非融合版本花费了1.5倍的时钟周期。 与大多数真实案例相比,这夸大了性能差异。 循环必须运行多个循环,并且2个额外的uops将它从2推到3.通常,额外的2个融合域uops将会减少差异。 如果代码被除了4-fused-domain-uops-per-cycle之外的其他东西所掩盖,那么可能没有区别。

    尽管如此,如果通过适度的展开和递增多指针来实现循环中的大量内存引用,那么使用简单的[base + immediate offset]寻址代替使用[base + index]寻址模式。

    进一步的东西

    RIP相对立即不能微型保险丝 。 Agner Fog的测试表明,甚至在解码器/ uop-cache中也是如此,所以它们从不首先融合(而不是未叠加)。

    IACA得到这个错误,并声称这两个微型保险丝:

    cmp dword  [abs mydata], 0x1b   ; fused counters != unfused counters (micro-fusion happened, and wasn't un-laminated).  Uses 2 entries in the uop-cache, according to Agner Fog's testing
    cmp dword  [rel mydata], 0x1b   ; fused counters ~= unfused counters (micro-fusion didn't happen)
    

    当没有直接的时候,RIP-rel会执行微型保险丝(并保持融合),例如:

    or  eax, dword  [rel mydata]    ; fused counters != unfused counters, i.e. micro-fusion happens
    

    微融合不会增加指令的延迟 。 在其他输入准备好之前,负载可能会发出。

    ALIGN 32
    .dep_fuse:
        or eax, [rsi + 0]
        or eax, [rsi + 0]
        or eax, [rsi + 0]
        or eax, [rsi + 0]
        or eax, [rsi + 0]
        dec ecx
        jg .dep_fuse
    

    由于eax dep链,此循环每次迭代运行5个周期。 不要比序列or eax, [rsi + 0 + rdi]mov ebx, [rsi + 0 + rdi] / or eax, ebx 。 (未融合版本和mov版本都运行相同数量的uops。)调度/ dep检查发生在未融合域中。 新发布的微软进入调度程序(又名Reservation Station(RS))以及ROB。 他们在派遣之后离开调度员(又被送到执行部门),但留在ROB直到退休。 因此隐藏负载等待时间的无序窗口至少是调度程序的大小(Sandybridge 54个未融合域的uops,Haswell的60个,Skylake的97个)。

    微融合没有一个快捷方式的基地和偏移是相同的寄存器。 带有or eax, [mydata + rdi+4*rdi]的循环or eax, [mydata + rdi+4*rdi] (其中rdi被清零)与循环or eax, [rsi+rdi]一样运行尽可能多的uops和循环。 这种寻址模式可用于遍历从固定地址开始的奇数大小结构数组。 这在大多数程序中可能从未使用,因此英特尔没有花费晶体管来允许这种双寄存器模式的特殊情况进行微型熔丝就不足为奇了。 (无论如何,英特尔将其称为“索引寻址模式”,其中需要寄存器和比例因子。)


    cmp / jccdec / jcc 宏融合创建了一个uop,即使在未融合域中也能保持单个uop。 dec / nop / jge仍然可以在单个循环中运行,但是是三个uops而不是一个。


    注意:自从我写了这个答案以后,Peter也测试了Haswell和Skylake,并将结果集成到了上面所接受的答案中(特别是,下面大部分针对Skylake的改进似乎实际上都出现在Haswell中)。 你应该可以看到CPU之间行为流逝的答案,这个答案(尽管没有错)主要是有历史意义的。

    我的测试表明,至少在Skylake上,处理器完全融合了复杂的寻址模式,这与Sandybridge不同。

    也就是说,Peter上面发布的代码的1-arg2-arg版本运行的循环次数相同,并且有相同数量的uop被派遣和退役。

    我的结果:

    ./uop-test性能计数器统计信息:

         23.718772      task-clock (msec)         #    0.973 CPUs utilized          
        20,642,233      cycles                    #    0.870 GHz                    
        80,111,957      instructions              #    3.88  insns per cycle        
        60,253,831      uops_executed_thread      # 2540.344 M/sec                  
        80,295,685      uops_issued_any           # 3385.322 M/sec                  
        80,176,940      uops_retired_retire_slots # 3380.316 M/sec                  
    
       0.024376698 seconds time elapsed
    

    性能计数器统计信息./uop-test x

         13.532440      task-clock (msec)         #    0.967 CPUs utilized          
        21,592,044      cycles                    #    1.596 GHz                    
        80,073,676      instructions              #    3.71  insns per cycle        
        60,144,749      uops_executed_thread      # 4444.487 M/sec                  
        80,162,360      uops_issued_any           # 5923.718 M/sec                  
        80,104,978      uops_retired_retire_slots # 5919.478 M/sec                  
    
       0.013997088 seconds time elapsed
    

    性能计数器统计信息./uop-test xx

         16.672198      task-clock (msec)         #    0.981 CPUs utilized          
        27,056,453      cycles                    #    1.623 GHz                    
        80,083,140      instructions              #    2.96  insns per cycle        
        60,164,049      uops_executed_thread      # 3608.645 M/sec                  
       100,187,390      uops_issued_any           # 6009.249 M/sec                  
       100,118,409      uops_retired_retire_slots # 6005.112 M/sec                  
    
       0.016997874 seconds time elapsed
    

    我没有在Skylake上找到任何UOPS_RETIRED_ANY指令,只有那些显然是融合域的“退役老虎机”家伙。

    最后的测试( uop-test xx )是Peter建议的一种变体,它使用与RIP相关的cmp与immediate,这种变体已知的不是microfuse:

    .loop_riprel
        cmp dword [rel mydata], 1
        cmp dword [rel mydata], 2
        dec ecx
        nop
        nop
        nop
        nop
        jg .loop_riprel
    

    结果显示,每个周期额外的2个微处理器被uops发出和退出的计数器拾取(因此测试可以区分融合发生与否)。

    欢迎在其他体系结构上进行更多测试! 你可以在github中找到代码(从Peter上面复制)。


    [1] ...也许在Skylake和Sandybridge之间还有其他架构,因为彼得只测试了SB,而我只测试了SKL。


    我现在已经回顾了英特尔Sandy Bridge,Ivy Bridge,Haswell和Broadwell的测试结果。 我还没有通过Skylake测试。 结果是:

  • 具有双寄存器寻址和三个输入相关性的指令完好无损。 只要它们包含不超过32位数据(或2 * 16位),它们在微操作缓存中只占用一个条目。
  • 可以使用Haswell和Broadwell上的融合乘加指令来制作具有四个输入依赖性的指令。 这些指令仍然融入到单个微操作中,并且只在微操作高速缓存中占用一个条目。
  • 具有多于32位数据的指令(例如32位地址和8位立即数据)仍然可以熔合,但是在微操作高速缓存中使用两个条目(除非32位可以被压缩成16位有符号整数)
  • 即使偏移量和立即数常量都很小,但使用翻转相对寻址和立即常数的指令也不会熔断。
  • 所有测试结果都相同。
  • 这些测试是在我自己的测试程序中使用循环中的性能监视计数器执行的,这些循环足够小以适合微操作缓存。
  • 您的结果可能是由于其他因素。 我没有尝试过使用IACA。

    链接地址: http://www.djcxy.com/p/79179.html

    上一篇: Micro fusion and addressing modes

    下一篇: When is a C++ destructor called?