关于性能:32字节对齐的例程不适合uops缓存

32-byte aligned routine does not fit the uops cache

KbL i7-8550U

我正在研究 uops-cache 的行为,但遇到了一个误解。

如英特尔优化手册 2.5.2.2 中所述(emp. mine):

The Decoded ICache consists of 32 sets. Each set contains eight Ways.
Each Way can hold up to six micro-ops.

-

All micro-ops in a Way represent instructions which are statically
contiguous in the code and have their EIPs within the same aligned
32-byte region.

-

Up to three Ways may be dedicated to the same 32-byte aligned chunk,
allowing a total of 18 micro-ops to be cached per 32-byte region of
the original IA program.

-

A non-conditional branch is the last micro-op in a Way.

案例 1:

考虑以下例程:

uop.h

1
void inhibit_uops_cache(size_t);

uop.S

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

运行方式

1
2
3
int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}

我拿到了计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 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

这正是我期望得到的。

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

1
2
3
4
mov edx, esi - 1 uop;
jmp imm      - 1 uop; near
dec rdi      - 1 uop;
ja           - 1 uop; near

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

案例 2:

考虑 inhibit_uops_cache 的实现,这与注释掉的一条指令不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

disas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

运行方式

1
2
3
int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}

我拿到了计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 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

计数器完全出乎意料。

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

相比之下,几乎 70% 的微指令来自传统解码管道。

问题:CASE 2 有什么问题?需要查看哪些计数器以了解发生了什么?

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

案例 3:

将有条件的jump目标对齐到32字节如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

disas:

1
2
3
4
5
6
7
8
9
10
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

并运行为

1
2
3
int main(void){
    inhibit_uops_cache(4096 * 4096 * 128L);
}

我得到了以下计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 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

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

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

接近峰值带宽。


这不是 OP 问题的答案,但要提防

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

其他观察:6 个 mov 指令的块应该填充一个 uop 缓存行,而 jmp 自己在一行中。在情况 2 中,5 个 mov jmp 应该适合一个缓存行(或更正确的"方式")。

(为了将来可能有相同症状但原因不同的读者的利益而发布此内容。我在写完它时意识到 0x...30 不是 32 字节边界,只有 0x...20,所以这个勘误不应该是问题中代码的问题。)

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

Microcode Update (MCU) to Mitigate JCC Erratum

This erratum can be prevented by a microcode update (MCU). The MCU prevents
jump instructions from being cached in the Decoded ICache when the jump
instructions cross a 32-byte boundary or when they end on a 32-byte boundary. In
this context, Jump Instructions include all jump types: conditional jump (Jcc), macrofused op-Jcc (where op is one of cmp, test, add, sub, and, inc, or dec), direct
unconditional jump, indirect jump, direct/indirect call, and return.

英特尔的白皮书还包括一个触发这种不可缓存效应的案例图表。 (PDF 屏幕截图取自 Phoronix 文章,其中包含之前/之后的基准测试,以及在 GCC/GAS 中尝试避免这种新的性能陷阱的一些变通方法进行重建之后)。

JCC

1
2
3
4
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

此部分未完全更新,仍在讨论跨越 32B 边界

JA 本身跨越了一个边界。

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

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

您可以在 jmp 之前使用单字节 nops 而不是 mov 来提前移动所有内容,如果这样可以在块的最后一个字节之前将所有内容全部放入。

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


OBSERVATION 1:目标在同一 32 字节区域内的分支,从 uops 缓存的angular来看,其行为与无条件分支非常相似(即它应该是行中的最后一个 uop)。

考虑 inhibit_uops_cache 的以下实现:

1
2
3
4
5
6
7
8
9
10
11
12
align 32
inhibit_uops_cache:
    xor eax, eax
    jmp t1 ;jz, jp, jbe, jge, jle, jnb, jnc, jng, jnl, jno, jns, jae
t1:
    jmp t2 ;jz, jp, jbe, jge, jle, jnb, jnc, jng, jnl, jno, jns, jae
t2:
    jmp t3 ;jz, jp, jbe, jge, jle, jnb, jnc, jng, jnl, jno, jns, jae
t3:
    dec rdi
    ja inhibit_uops_cache
    ret

代码针对评论中提到的所有分支进行了测试。结果证明差异非常微不足道,因此我只提供其中 2 个:

jmp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     4?748?772?552      idq.dsb_cycles                                                (57,13%)
     7?499?524?594      idq.dsb_uops                                                  (57,18%)
     5?397?128?360      idq.mite_uops                                                 (57,18%)
         8?696?719      idq.ms_uops                                                   (57,18%)
     6?247?749?210      dsb2mite_switches.penalty_cycles                                     (57,14%)
     3?841?902?993      frontend_retired.dsb_miss                                     (57,10%)
    21?508?686?982      cycles                                                        (57,10%)

       5,464493212 seconds time elapsed

       5,464369000 seconds user
       0,000000000 seconds sys

jge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     4?745?825?810      idq.dsb_cycles                                                (57,13%)
     7?494?052?019      idq.dsb_uops                                                  (57,13%)
     5?399?327?121      idq.mite_uops                                                 (57,13%)
         9?308?081      idq.ms_uops                                                   (57,13%)
     6?243?915?955      dsb2mite_switches.penalty_cycles                                     (57,16%)
     3?842?842?590      frontend_retired.dsb_miss                                     (57,16%)
    21?507?525?469      cycles                                                        (57,16%)

       5,486589670 seconds time elapsed

       5,486481000 seconds user
       0,000000000 seconds sys

IDK 为什么 dsb uop 的数量是 7?494?052?019,明显小于 4096 * 4096 * 128 * 4 = 8?589?934?592

用预测不会被采用的分支替换任何 jmp 会产生明显不同的结果。例如:

1
2
3
4
5
6
7
8
9
10
11
12
align 32
inhibit_uops_cache:
    xor eax, eax
    jnz t1 ; perfectly predicted to not be taken
t1:
    jae t2
t2:
    jae t3
t3:
    dec rdi
    ja inhibit_uops_cache
    ret

产生以下计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     5?420?107?670      idq.dsb_cycles                                                (56,96%)
    10?551?728?155      idq.dsb_uops                                                  (57,02%)
     2?326?542?570      idq.mite_uops                                                 (57,16%)
         6?209?728      idq.ms_uops                                                   (57,29%)
       787?866?654      dsb2mite_switches.penalty_cycles                                     (57,33%)
     1?031?630?646      frontend_retired.dsb_miss                                     (57,19%)
    11?381?874?966      cycles                                                        (57,05%)

       2,927769205 seconds time elapsed

       2,927683000 seconds user
       0,000000000 seconds sys

考虑另一个类似于CASE 1的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
align 32
inhibit_uops_cache:
    nop
    nop
    nop
    nop
    nop
    xor eax, eax
    jmp t1
t1:
    dec rdi
    ja inhibit_uops_cache
    ret

导致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     6?331?388?209      idq.dsb_cycles                                                (57,05%)
    19?052?030?183      idq.dsb_uops                                                  (57,05%)
       343?629?667      idq.mite_uops                                                 (57,05%)
         2?804?560      idq.ms_uops                                                   (57,13%)
           367?020      dsb2mite_switches.penalty_cycles                                     (57,27%)
        55?220?850      frontend_retired.dsb_miss                                     (57,27%)
     7?063?498?379      cycles                                                        (57,19%)

       1,788124756 seconds time elapsed

       1,788101000 seconds user
       0,000000000 seconds sys

jz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     6?347?433?290      idq.dsb_cycles                                                (57,07%)
    18?959?366?600      idq.dsb_uops                                                  (57,07%)
       389?514?665      idq.mite_uops                                                 (57,07%)
         3?202?379      idq.ms_uops                                                   (57,12%)
           423?720      dsb2mite_switches.penalty_cycles                                     (57,24%)
        69?486?934      frontend_retired.dsb_miss                                     (57,24%)
     7?063?060?791      cycles                                                        (57,19%)

       1,789012978 seconds time elapsed

       1,788985000 seconds user
       0,000000000 seconds sys

jno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Performance counter stats for './bin':

     6?417?056?199      idq.dsb_cycles                                                (57,02%)
    19?113?550?928      idq.dsb_uops                                                  (57,02%)
       329?353?039      idq.mite_uops                                                 (57,02%)
         4?383?952      idq.ms_uops                                                   (57,13%)
           414?037      dsb2mite_switches.penalty_cycles                                     (57,30%)
        79?592?371      frontend_retired.dsb_miss                                     (57,30%)
     7?044?945?047      cycles                                                        (57,20%)

       1,787111485 seconds time elapsed

       1,787049000 seconds user
       0,000000000 seconds sys

所有这些实验都让我认为观察结果对应于 uops 缓存的真实行为。我还进行了另一个实验,通过计数器 br_inst_retired.near_takenbr_inst_retired.not_taken 判断,结果与观察结果相关。

考虑以下 inhibit_uops_cache 的实现:

1
2
3
4
5
6
7
8
9
10
align 32
inhibit_uops_cache:
t0:
    ;nops 0-9
    jmp t1
t1:
    ;nop 0-6
    dec rdi
    ja t0
    ret

收集 dsb2mite_switches.penalty_cyclesfrontend_retired.dsb_miss 我们有:

enter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
align 32
inhibit_uops_cache:
t0:
    nop
    nop
    nop
    nop
    jmp t1
t1:
    nop
    nop
    dec rdi
    ja t0
    ret

从我来的情节来看

观察 2:如果 32 字节区域内有 2 个预计将被采用的分支,则在 dsb2mite 开关和 dsb 未命中之间没有可观察到的相关性。因此 dsb 未命中可能独立于 dsb2mite 开关发生。

增加 frontend_retired.dsb_miss 速率与增加 idq.mite_uops 速率和减小 idq.dsb_uops 密切相关。这可以在下面的图中看到:

enter