32 字节对齐例程不适合 uops 缓存

St.*_*rio 7 performance x86 assembly intel cpu-architecture

KbL i7-8550U

我正在研究 uops-cache 的行为并遇到了关于它的误解。

如英特尔优化手册2.5.2.2(我的)中所述:

解码的 ICache 由 32 组组成。每组包含八种方式。 每路最多可容纳六个微操作。

——

Way 中的所有微操作表示在代码中静态连续的指令,并且它们的 EIP 位于相同的对齐 32 字节区域内。

——

最多三种方式可以专用于相同的 32 字节对齐块,从而允许在原始 IA 程序的每个 32 字节区域中缓存总共 18 个微操作。

——

无条件分支是 Way 中的最后一个微操作。


情况1:

考虑以下例程:

uop.h

void inhibit_uops_cache(size_t);
Run Code Online (Sandbox Code Playgroud)

uop.S

align 32
inhibit_uops_cache:
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    jmp decrement_jmp_tgt
decrement_jmp_tgt:
    dec rdi
    ja inhibit_uops_cache ;ja is intentional to avoid Macro-fusion
    ret
Run Code Online (Sandbox Code Playgroud)

为了确保例程的代码实际上是 32 字节对齐的,这里是 asm

0x555555554820 <inhibit_uops_cache>     mov    edx,esi
0x555555554822 <inhibit_uops_cache+2>   mov    edx,esi
0x555555554824 <inhibit_uops_cache+4>   mov    edx,esi
0x555555554826 <inhibit_uops_cache+6>   mov    edx,esi
0x555555554828 <inhibit_uops_cache+8>   mov    edx,esi
0x55555555482a <inhibit_uops_cache+10>  mov    edx,esi
0x55555555482c <inhibit_uops_cache+12>  jmp    0x55555555482e <decrement_jmp_tgt>
0x55555555482e <decrement_jmp_tgt>      dec    rdi
0x555555554831 <decrement_jmp_tgt+3>    ja     0x555555554820 <inhibit_uops_cache>
0x555555554833 <decrement_jmp_tgt+5>    ret
0x555555554834 <decrement_jmp_tgt+6>    nop
0x555555554835 <decrement_jmp_tgt+7>    nop
0x555555554836 <decrement_jmp_tgt+8>    nop
0x555555554837 <decrement_jmp_tgt+9>    nop
0x555555554838 <decrement_jmp_tgt+10>   nop
0x555555554839 <decrement_jmp_tgt+11>   nop
0x55555555483a <decrement_jmp_tgt+12>   nop
0x55555555483b <decrement_jmp_tgt+13>   nop
0x55555555483c <decrement_jmp_tgt+14>   nop
0x55555555483d <decrement_jmp_tgt+15>   nop
0x55555555483e <decrement_jmp_tgt+16>   nop
0x55555555483f <decrement_jmp_tgt+17>   nop             
Run Code Online (Sandbox Code Playgroud)

运行为

int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}
Run Code Online (Sandbox Code Playgroud)

我拿到了柜台

 Performance counter stats for './bin':

     6?431?201?748      idq.dsb_cycles                                                (56,91%)
    19?175?741?518      idq.dsb_uops                                                  (57,13%)
         7?866?687      idq.mite_uops                                                 (57,36%)
         3?954?421      idq.ms_uops                                                   (57,46%)
           560?459      dsb2mite_switches.penalty_cycles                                     (57,28%)
           884?486      frontend_retired.dsb_miss                                     (57,05%)
     6?782?598?787      cycles                                                        (56,82%)

       1,749000366 seconds time elapsed

       1,748985000 seconds user
       0,000000000 seconds sys
Run Code Online (Sandbox Code Playgroud)

这正是我期望得到的。

绝大多数 uops 来自 uops 缓存。uops 数字也完全符合我的期望

mov edx, esi - 1 uop;
jmp imm      - 1 uop; near 
dec rdi      - 1 uop;
ja           - 1 uop; near
Run Code Online (Sandbox Code Playgroud)

4096 * 4096 * 128 * 9 = 19?327?352?832 大约等于计数器 19?326?755?442 + 3?836?395 + 1?642?975


案例2:

考虑inhibit_uops_cache通过注释掉的一条指令不同的实现:

align 32
inhibit_uops_cache:
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    ; mov edx, esi
    jmp decrement_jmp_tgt
decrement_jmp_tgt:
    dec rdi
    ja inhibit_uops_cache ;ja is intentional to avoid Macro-fusion
    ret
Run Code Online (Sandbox Code Playgroud)

疾病:

0x555555554820 <inhibit_uops_cache>     mov    edx,esi
0x555555554822 <inhibit_uops_cache+2>   mov    edx,esi
0x555555554824 <inhibit_uops_cache+4>   mov    edx,esi
0x555555554826 <inhibit_uops_cache+6>   mov    edx,esi
0x555555554828 <inhibit_uops_cache+8>   mov    edx,esi
0x55555555482a <inhibit_uops_cache+10>  jmp    0x55555555482c <decrement_jmp_tgt>
0x55555555482c <decrement_jmp_tgt>      dec    rdi
0x55555555482f <decrement_jmp_tgt+3>    ja     0x555555554820 <inhibit_uops_cache>
0x555555554831 <decrement_jmp_tgt+5>    ret
0x555555554832 <decrement_jmp_tgt+6>    nop
0x555555554833 <decrement_jmp_tgt+7>    nop
0x555555554834 <decrement_jmp_tgt+8>    nop
0x555555554835 <decrement_jmp_tgt+9>    nop
0x555555554836 <decrement_jmp_tgt+10>   nop
0x555555554837 <decrement_jmp_tgt+11>   nop
0x555555554838 <decrement_jmp_tgt+12>   nop
0x555555554839 <decrement_jmp_tgt+13>   nop
0x55555555483a <decrement_jmp_tgt+14>   nop
0x55555555483b <decrement_jmp_tgt+15>   nop
0x55555555483c <decrement_jmp_tgt+16>   nop
0x55555555483d <decrement_jmp_tgt+17>   nop
0x55555555483e <decrement_jmp_tgt+18>   nop
0x55555555483f <decrement_jmp_tgt+19>   nop                      
Run Code Online (Sandbox Code Playgroud)

运行为

int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}
Run Code Online (Sandbox Code Playgroud)

我拿到了柜台

 Performance counter stats for './bin':

     2?464?970?970      idq.dsb_cycles                                                (56,93%)
     6?197?024?207      idq.dsb_uops                                                  (57,01%)
    10?845?763?859      idq.mite_uops                                                 (57,19%)
         3?022?089      idq.ms_uops                                                   (57,38%)
           321?614      dsb2mite_switches.penalty_cycles                                     (57,35%)
     1?733?465?236      frontend_retired.dsb_miss                                     (57,16%)
     8?405?643?642      cycles                                                        (56,97%)

       2,117538141 seconds time elapsed

       2,117511000 seconds user
       0,000000000 seconds sys
Run Code Online (Sandbox Code Playgroud)

计数器完全出乎意料。

我希望所有的 uops 都像以前一样来自 dsb,因为例程符合 uops 缓存的要求。

相比之下,几乎 70% 的 uops 来自 Legacy Decode Pipeline。

问题:案例 2 有什么问题?看什么计数器来了解发生了什么?


UPD:按照@PeterCordes 的想法,我检查了无条件分支目标的 32 字节对齐方式decrement_jmp_tgt。结果如下:

案例3:

将 onconditionaljump目标对齐到 32 字节如下

align 32
inhibit_uops_cache:
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    mov edx, esi
    ; mov edx, esi
    jmp decrement_jmp_tgt
align 32 ; align 16 does not change anything
decrement_jmp_tgt:
    dec rdi
    ja inhibit_uops_cache
    ret
Run Code Online (Sandbox Code Playgroud)

疾病:

0x555555554820 <inhibit_uops_cache>     mov    edx,esi
0x555555554822 <inhibit_uops_cache+2>   mov    edx,esi
0x555555554824 <inhibit_uops_cache+4>   mov    edx,esi
0x555555554826 <inhibit_uops_cache+6>   mov    edx,esi
0x555555554828 <inhibit_uops_cache+8>   mov    edx,esi
0x55555555482a <inhibit_uops_cache+10>  jmp    0x555555554840 <decrement_jmp_tgt>
#nops to meet the alignment
0x555555554840 <decrement_jmp_tgt>      dec    rdi
0x555555554843 <decrement_jmp_tgt+3>    ja     0x555555554820 <inhibit_uops_cache>
0x555555554845 <decrement_jmp_tgt+5>    ret                                              
Run Code Online (Sandbox Code Playgroud)

并运行为

int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}
Run Code Online (Sandbox Code Playgroud)

我得到以下计数器

 Performance counter stats for './bin':

     4?296?298?295      idq.dsb_cycles                                                (57,19%)
    17?145?751?147      idq.dsb_uops                                                  (57,32%)
        45?834?799      idq.mite_uops                                                 (57,32%)
         1?896?769      idq.ms_uops                                                   (57,32%)
           136?865      dsb2mite_switches.penalty_cycles                                     (57,04%)
           161?314      frontend_retired.dsb_miss                                     (56,90%)
     4?319?137?397      cycles                                                        (56,91%)

       1,096792233 seconds time elapsed

       1,096759000 seconds user
       0,000000000 seconds sys
Run Code Online (Sandbox Code Playgroud)

结果完全符合预期。超过 99% 的 uops 来自 dsb。

平均 dsb uops 交付率 = 17?145?751?147 / 4?296?298?295=3.99

接近峰值带宽。

Pet*_*des 5

这不是 OP 问题的答案,而是需要注意的问题

请参阅代码对齐显着影响编译器选项的性能,以解决英特尔引入 Skylake 派生 CPU 的性能坑,作为此解决方法的一部分。


其他观察结果:6mov条指令块应该填充一个 uop 缓存行,并单独一行jmp。在第 2 种情况下, 5 mov+jmp应该适合一个缓存行(或更恰当的“方式”)。

张贴这对谁可能具有相同的症状,但不同的事业未来的读者受益。 我意识到权利,我写完这是0x...30不是一个32字节的边界,只有0x...2040,所以此错误不应该成为问题问题中的代码。


最近(2019 年末)微代码更新引入了一个新的性能坑。 它围绕 Intel 在 Skylake 衍生的微架构上的 JCC 勘误表工作。(特别是您的 Kaby-Lake 上的 KBL142)。

微码更新 (MCU) 以缓解 JCC 错误

这种错误可以通过微码更新 (MCU) 来防止。当跳转指令跨越 32 字节边界或在 32 字节边界处结束时,MCU 会阻止跳转指令缓存在解码的 ICache 中。在这种情况下,跳转指令包括所有跳转类型:条件跳转 (Jcc)、宏融合 op-Jcc(其中 op 是 cmp、test、add、sub、and、inc 或 dec 之一)、直接无条件跳转、间接跳转、直接/间接调用,然后返回。

英特尔的白皮书还包括触发这种非 uop 可缓存效应的案例图。(PDF 截图从Phoronix 文章中借用了之前/之后的基准测试,之后使用 GCC/GAS 中的一些变通方法重建,试图避免这种新的性能陷阱)。

联谊会


代码中 ja 的最后一个字节是...30,所以它是罪魁祸首。

如果这是一个 32 字节的边界,而不仅仅是 16,那么我们就会遇到问题:

0x55555555482a <inhibit_uops_cache+10>  jmp         # fine
0x55555555482c <decrement_jmp_tgt>      dec    rdi
0x55555555482f <decrement_jmp_tgt+3>    ja          # spans 16B boundary (not 32)
0x555555554831 <decrement_jmp_tgt+5>    ret         # fine
Run Code Online (Sandbox Code Playgroud)

本节未完全更新,还在讨论跨越32B边界

JA 本身跨越了一个边界。

dec rdi应该工作之后插入 NOP ,将 2 字节ja完全放在边界之后,并带有一个新的 32 字节块。无论如何,dec/ja 的宏融合是不可能的,因为 JA 读取 CF(和 ZF)但 DEC 不写入 CF。

使用sub rdi, 1移动JA将工作; 它会进行宏融合,并且与该指令对应的 x86 代码的组合 6 字节仍将跨越边界。

如果在块的最后一个字节之前将所有内容全部放入,则可以使用单字节 nops 代替mov之前的jmp所有内容。


ASLR 可以更改从哪个虚拟页面代码执行(地址的第 12 位和更高位),但不能更改页面内的对齐方式或相对于缓存线的对齐方式。因此,我们在一种情况下在反汇编中看到的情况每次都会发生。

  • @Noah - uop 缓存打包规则适用于 32 字节块(每个 32 字节块映射到一个 uop 缓存集)。原始片段不适合 uop 缓存,因为它需要同一组中的 4 个 uop 缓存行,但最大值为 3 个,并且整个片段位于一个 32 字节块内。将代码段内的对齐方式更改为 32 会将其分散到两个 32 字节块中,因此限制不再被打破,并且可以成功缓存。 (2认同)