Выровненная 32-байтовая подпрограмма не помещается в кэш мопов - PullRequest
7 голосов
/ 03 апреля 2020

KbL i7-8550U

Я исследую поведение uops-кеша и обнаружил недопонимание относительно него.

Как указано в Руководстве по оптимизации Intel 2.5.2.2 (emp. Mine ):

Декодированный ICache состоит из 32 наборов. Каждый набор содержит восемь способов. Каждый Путь может содержать до шести микроопераций.

-

Все микрооперации в Пути представляют собой инструкции, которые являются статически смежными в код и имеют свои EIP в пределах одной выровненной 32-байтовой области.

-

Для одного 32-байтового выровненного блока может быть выделено до трех путей, позволяя кэшировать в общей сложности 18 микроопераций в 32-байтовой области исходной программы IA.

-

Необусловленная ветвь является последней микро -op в пути.


CASE 1:

Рассмотрим следующую процедуру:

uop.h

void inhibit_uops_cache(size_t);

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

Чтобы убедиться, что код процедуры на самом деле выровнен на 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             

работает как

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

Я получил счетчики

 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

Это именно то, что я ожидал получить.

Подавляющее большинство мопов было получено из кеша мопов , Также число мопов идеально совпадает с моим ожиданием

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


CASE 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

disas:

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                      

, работающей как

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

I получил счетчики

 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% мопов было получено из Legacy Decode Pipeline.

ВОПРОС: Что не так с Случай 2? Какие счетчики посмотреть, чтобы понять, что происходит?


UPD: Следуя идее @PeterCordes, я проверил 32-байтовое выравнивание безусловной цели ветвления decrement_jmp_tgt. Вот результат:

CASE 3:

Выравнивание onconditional jump target в 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

disas:

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                                              

и работает как

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

Я получил следующие счетчики

 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 = 17 145 751 147 / 4 296 298 295 = 3.99

Что близко к пиковая полоса пропускания.

Ответы [ 2 ]

3 голосов
/ 03 апреля 2020

Это не ответ на проблему ОП, но следует остерегаться

Другие наблюдения: блок из 6 mov инструкций должен заполнить строку кэша UOP, с jmp в линия сама по себе. В случае 2 5 mov + jmp должны помещаться в одну строку кэша (или, точнее, «в порядке»). те же симптомы, но другая причина. Я сразу понял, когда закончил писать, что 0x...30 - это , а не 32-байтовая граница, только 0x...20 и 40, поэтому это Ошибка в коде, о котором идет речь, не должна быть проблемой. )


Недавнее (в конце 2019 года) обновление микрокода представило новую проблему производительности. Это работает вокруг Ошибка Intel * J CC в микроархитектурах Skylake. (KBL142 на вашем Kaby-Lake специально).

Обновление микрокода (MCU) для смягчения J CC Ошибка

Эта ошибка может быть предотвращена обновление микрокода (MCU). MCU предотвращает кэширование инструкций перехода в декодированном ICache, когда инструкции перехода пересекают 32-байтовую границу или когда они заканчиваются 32-байтовой границей . В этом контексте инструкции по переходу включают в себя все типы переходов: условный переход (J cc), макрофузированный op-J cc (где op - один из cmp, test, add, sub и, в c или de c), прямой безусловный переход, косвенный переход, прямой / косвенный вызов и возврат.

Технический документ Intel также включает в себя диаграмму случаев, которые запускают этот non-uop- кешируемый эффект. (Скриншот PDF заимствован из статьи Phoronix с тестами до / после и после с перестройкой с некоторыми обходными путями в GCC / GAS, которые пытаются избежать этой новой ошибки производительности).

JCC


Последний байт 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

Этот раздел не полностью обновлен, все еще говорится о том, что он охватывает границу 32B

JA сам расширяет границу.

Вставка NOP после dec rdi должна сработать, полностью поместив 2-байтовый ja после границы с новым 32-байтовым фрагментом. Макро-слияние dec / ja было невозможно в любом случае, потому что JA читает CF (и ZF), но DE C не записывает CF.

Использование sub rdi, 1 для перемещения JA не работа; это приведет к слиянию макросов, и объединенные 6 байтов кода x86, соответствующего этой инструкции, все еще будут выходить за границы.

Вы можете использовать однобайтовые nops вместо mov перед jmp, чтобы переместить все ранее, если он получает все до последнего байта блока.


ASLR может изменить код, с которого выполняется код виртуальной страницы (бит 12 и выше адреса), но не выравнивание внутри страница или относительно строки кэша. Так что то, что мы видим в разборке в одном случае, будет происходить каждый раз.

1 голос
/ 07 апреля 2020

НАБЛЮДЕНИЕ 1: Ветвь с целью в той же 32-байтовой области, которая, как ожидается, будет взята, во многом похожа на безусловную ветвь с точки зрения кэша мопов (т.е. это должен быть последний моп в строка).

Рассмотрим следующую реализацию inhibit_uops_cache:

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:

 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:

 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 составляет 7 494 052 019, что значительно меньше, чем 4096 * 4096 * 128 * 4 = 8 589 934 592.

Замена любого из jmp ветвью, которая, как ожидается, не будет взята дает результат, который значительно отличается. Например:

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

приводит к следующим счетчикам:

 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 :

align 32
inhibit_uops_cache:
    nop
    nop
    nop
    nop
    nop
    xor eax, eax
    jmp t1
t1:
    dec rdi
    ja inhibit_uops_cache
    ret

приводит к

 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:

 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:

 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

Все эти эксперименты заставили меня думать, что наблюдение соответствует реальному поведению кэша мопов. Я также провел еще один эксперимент и, судя по счетчикам br_inst_retired.near_taken и br_inst_retired.not_taken, результат коррелирует с наблюдением.


Рассмотрим следующую реализацию inhibit_uops_cache:

align 32
inhibit_uops_cache:
t0:
    ;nops 0-9
    jmp t1
t1:
    ;nop 0-6
    dec rdi
    ja t0
    ret

Собирая dsb2mite_switches.penalty_cycles и frontend_retired.dsb_miss имеем:

enter image description here

Ось X графика обозначает число nop s, например, 24 означает 2 nop s после метки t1, 4 nop s после метки t0 :

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 image description here

НАБЛЮДЕНИЕ 3: Происходит промах DSB по какой-то (неясной?) Причине приводит к появлению пузырьков IDQ и, следовательно, к потере RAT.


Вывод: Принимая во внимание все измерения, безусловно, существуют некоторые различия между поведением, определенным в Intel Optimization Manual, 2.5.2.2 Decoded ICache

...