Действительно ли L2 HW prefetcher действительно полезен?

10

Я нахожусь на Whiskey Lake i7-8565U и анализирую счетчики перфектов и время для копирования 512 КБ данных (в два раза больше, чем размер кэша L2), и столкнулся с некоторым недоразумением относительно работы устройства предварительной выборки L2 HW.

В Руководстве Intel Vol.4 MSR есть MSR, 0x1A4бит 0 которого предназначен для управления устройством предварительной выборки L2 HW (1 для отключения).


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

memcopy.h:

void *avx_memcpy_forward_lsls(void *restrict, const void *restrict, size_t);

memcopy.S:

avx_memcpy_forward_lsls:
    shr rdx, 0x3
    xor rcx, rcx
avx_memcpy_forward_loop_lsls:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa [rdi + rcx*8], ymm0
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovdqa [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_memcpy_forward_loop_lsls
    ret

main.c:

#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"

#define ITERATIONS 1000
#define BUF_SIZE 512 * 1024

_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz);

#define run_benchmark(runs, run_iterations, fn, dest, src, sz) \
    do{\
        printf("Benchmarking " #fn "\n");\
        __run_benchmark(runs, run_iterations, fn, dest, src, sz);\
    }while(0)

int main(void){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, src, sizeof src);
    run_benchmark(20, ITERATIONS, avx_memcpy_forward_lsls, dest, src, BUF_SIZE);
}

static inline void benchmark_copy_function(unsigned iterations, void *(*fn)(void *, const void *, size_t),
                                               void *restrict dest, const void *restrict src, size_t sz){
    while(iterations --> 0){
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
    }
}

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz){
    unsigned current_run = 1;
    while(current_run <= runs){
        benchmark_copy_function(run_iterations, fn, dest, src, sz);
        printf("Run %d finished\n", current_run);
        current_run++;
    }
}

Рассмотрим 2 прогона скомпилированного main.c

Я .

MSR:

$ sudo rdmsr -p 0 0x1A4
0

Run:

$ taskset -c 0 sudo ../profile.sh ./bin 

 Performance counter stats for './bin':

    10486164071      L1-dcache-loads                                               (12,13%)
    10461354384      L1-dcache-load-misses     #   99,76% of all L1-dcache hits    (12,05%)
    10481930413      L1-dcache-stores                                              (12,05%)
    10461136686      l1d.replacement                                               (12,12%)
    31466394422      l1d_pend_miss.fb_full                                         (12,11%)
   211853643294      l1d_pend_miss.pending                                         (12,09%)
     1759204317      LLC-loads                                                     (12,16%)
            31007      LLC-load-misses           #    0,00% of all LL-cache hits     (12,16%)
     3154901630      LLC-stores                                                    (6,19%)
    15867315545      l2_rqsts.all_pf                                               (9,22%)
                 0      sw_prefetch_access.t1_t2                                      (12,22%)
         1393306      l2_lines_out.useless_hwpf                                     (12,16%)
     3549170919      l2_rqsts.pf_hit                                               (12,09%)
    12356247643      l2_rqsts.pf_miss                                              (12,06%)
                 0      load_hit_pre.sw_pf                                            (12,09%)
     3159712695      l2_rqsts.rfo_hit                                              (12,06%)
     1207642335      l2_rqsts.rfo_miss                                             (12,02%)
     4366526618      l2_rqsts.all_rfo                                              (12,06%)
     5240013774      offcore_requests.all_data_rd                                     (12,06%)
    19936657118      offcore_requests.all_requests                                     (12,09%)
     1761660763      offcore_response.demand_data_rd.any_response                                     (12,12%)
       287044397      bus-cycles                                                    (12,15%)
    36816767779      resource_stalls.any                                           (12,15%)
    36553997653      resource_stalls.sb                                            (12,15%)
    38035066210      uops_retired.stall_cycles                                     (12,12%)
    24766225119      uops_executed.stall_cycles                                     (12,09%)
    40478455041      uops_issued.stall_cycles                                      (12,05%)
    24497256548      cycle_activity.stalls_l1d_miss                                     (12,02%)
    12611038018      cycle_activity.stalls_l2_miss                                     (12,09%)
        10228869      cycle_activity.stalls_l3_miss                                     (12,12%)
    24707614483      cycle_activity.stalls_mem_any                                     (12,22%)
    24776110104      cycle_activity.stalls_total                                     (12,22%)
    48914478241      cycles                                                        (12,19%)

      12,155774555 seconds time elapsed

      11,984577000 seconds user
       0,015984000 seconds sys

II.

MSR:

$ sudo rdmsr -p 0 0x1A4
1

Run:

$ taskset -c 0 sudo ../profile.sh ./bin

 Performance counter stats for './bin':

    10508027832      L1-dcache-loads                                               (12,05%)
    10463643206      L1-dcache-load-misses     #   99,58% of all L1-dcache hits    (12,09%)
    10481296605      L1-dcache-stores                                              (12,12%)
    10444854468      l1d.replacement                                               (12,15%)
    29287445744      l1d_pend_miss.fb_full                                         (12,17%)
   205569630707      l1d_pend_miss.pending                                         (12,17%)
     5103444329      LLC-loads                                                     (12,17%)
            33406      LLC-load-misses           #    0,00% of all LL-cache hits     (12,17%)
     9567917742      LLC-stores                                                    (6,08%)
     1157237980      l2_rqsts.all_pf                                               (9,12%)
                 0      sw_prefetch_access.t1_t2                                      (12,17%)
           301471      l2_lines_out.useless_hwpf                                     (12,17%)
       218528985      l2_rqsts.pf_hit                                               (12,17%)
       938735722      l2_rqsts.pf_miss                                              (12,17%)
                 0      load_hit_pre.sw_pf                                            (12,17%)
         4096281      l2_rqsts.rfo_hit                                              (12,17%)
     4972640931      l2_rqsts.rfo_miss                                             (12,17%)
     4976006805      l2_rqsts.all_rfo                                              (12,17%)
     5175544191      offcore_requests.all_data_rd                                     (12,17%)
    15772124082      offcore_requests.all_requests                                     (12,17%)
     5120635892      offcore_response.demand_data_rd.any_response                                     (12,17%)
       292980395      bus-cycles                                                    (12,17%)
    37592020151      resource_stalls.any                                           (12,14%)
    37317091982      resource_stalls.sb                                            (12,11%)
    38121826730      uops_retired.stall_cycles                                     (12,08%)
    25430699605      uops_executed.stall_cycles                                     (12,04%)
    41416190037      uops_issued.stall_cycles                                      (12,04%)
    25326579070      cycle_activity.stalls_l1d_miss                                     (12,04%)
    25019148253      cycle_activity.stalls_l2_miss                                     (12,03%)
         7384770      cycle_activity.stalls_l3_miss                                     (12,03%)
    25442709033      cycle_activity.stalls_mem_any                                     (12,03%)
    25406897956      cycle_activity.stalls_total                                     (12,03%)
    49877044086      cycles                                                        (12,03%)

      12,231406658 seconds time elapsed

      12,226386000 seconds user
       0,004000000 seconds sys

Я заметил счетчик:

12 611 038 018 cycle_activity.stalls_l2_miss V / S
25 019 148 253 cycle_activity.stalls_l2_miss

предполагая, что MSR отключает L2 HW prefetcher применяется. Также другие вещи, связанные с l2 / LLC, существенно отличаются. Разница воспроизводима в разных сериях . Проблема в том, что различий total timeи циклов почти нет :

48 914 478 241 cycles V / S
49 877 044 086 cycles

12,155774555 seconds time elapsed V / S
12,231406658 seconds time elapsed

ВОПРОС:
Недостатки L2 скрыты другими ограничителями производительности?
Если да, можете ли вы подсказать, на какие счетчики посмотреть, чтобы понять это?

St.Antario
источник
4
Практическое правило. Любая неиспользуемая копия памяти ограничена памятью. Даже когда он попадает только в кэш L1. Издержки любого доступа к памяти просто намного выше, чем требуется процессору, чтобы сложить два и два вместе. В вашем случае вы даже используете инструкции AVX для уменьшения количества инструкций на скопированный байт. Где бы ни находились ваши данные (L1, L2, LLC, память), пропускная способность связанного компонента памяти будет вашим узким местом.
cmaster - восстановить монику

Ответы:

5

Да, стример L2 действительно полезен в большинстве случаев.

У memcpy нет скрытой задержки вычислений, поэтому я полагаю, что он может позволить OoO exec ресурсам (размер ROB) обрабатывать дополнительную задержку загрузки, которую вы получаете от большего количества пропусков L2, по крайней мере, в этом случае, когда вы получаете все попадания L3 из используя рабочий набор среднего размера (1 МБ), который умещается в L3, предварительная выборка не требуется для выполнения попаданий L3.

И единственными инструкциями являются загрузка / сохранение (и издержки цикла), поэтому окно OoO включает нагрузку по требованию для довольно большого продвижения вперед.

IDK, если пространственный предварительный выбор L2 и предварительный выбор L1d помогают здесь.


Предсказание, чтобы проверить эту гипотезу : увеличьте свой массив, чтобы получить пропуски L3, и вы, вероятно, увидите разницу в общем времени, как только OoO exec будет недостаточно, чтобы скрыть задержку загрузки при переходе к DRAM. Некоторым может помочь запуск предварительной загрузки HW дальше.

Другие большие преимущества предварительной выборки HW появляются, когда она может не отставать от ваших вычислений, поэтому вы получаете хиты L2. (В цикле, который имеет вычисления с цепочкой зависимостей средней длины, но не переносится циклом.)

Загрузки по требованию и OoO exec могут многое сделать, если использовать доступную (однопоточную) пропускную способность памяти, когда нет другой нагрузки на емкость ROB.


Также обратите внимание, что на процессорах Intel каждая ошибка кэша может стоить внутреннего воспроизведения (от RS / планировщика) зависимых мопов , по одному для L1d и L2, когда ожидается поступление данных. И после этого, очевидно, ядро ​​оптимистично спамит мопы в ожидании получения данных от L3.

(См. Https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th и Отключены ли операции загрузки из RS когда они отправят, завершат или как-нибудь в другой раз? )

Не саму загрузку кэша; в этом случае это будут инструкции магазина. В частности, хранилище данных для порта 4. Это не имеет значения; использование 32-байтовых хранилищ и узких мест в полосе пропускания L3 означает, что мы не близки к 1 порту 4 моп в такт.

Питер Кордес
источник
2
@ St.Antario: да? Это бессмысленно; Вы ограничены в памяти, поэтому у вас нет узкого места во внешнем интерфейсе, поэтому ЛСД не имеет значения. (Это позволяет избежать повторного извлечения их из кэша UOP, экономя энергию). Они все еще занимают место в ROB, пока не смогут уйти в отставку. Они не что существенно, но нельзя пренебрегать ни.
Питер Кордес
2
увеличьте массив, чтобы вы пропустили L3, и вы, вероятно, увидите разницу. Я провел ряд тестов с 16MiBбуфером и 10итерациями и действительно получил 14,186868883 secondsvs 43,731360909 secondsи 46,76% of all LL-cache hitsvs 99,32% of all LL-cache hits; 1 028 664 372 LLC-loadsпротив 1 587 454 298 LLC-loads .
Святой Антарио
4
@ St.Antario: путем переименования регистра! Это одна из самых важных частей OoO exec, особенно на ISA с плохим регистром, например, x86. См. Почему mulss занимает всего 3 цикла в Haswell, в отличие от таблиц инструкций Агнера? (Развертывание петель FP с несколькими аккумуляторами) . И кстати, обычно вы хотите сделать 2 загрузки, а затем 2 магазина, а не загрузить / сохранить / загрузить / сохранить. Лучший шанс избежать или смягчить сбои с псевдонимами 4k, потому что более поздние нагрузки (которые HW должен обнаружить как перекрывающие предыдущие хранилища или нет) находятся дальше.
Питер Кордес
2
@ St.Antario: да, конечно. Руководство по оптимизации Agner Fog также объясняет OoO exec переименованием регистров, как и википедия. Кстати, переименование регистров также позволяет избежать опасностей WAW, оставляя только истинные зависимости (RAW). Таким образом, загрузка может даже завершиться не по порядку, не дожидаясь, пока предыдущая загрузка завершит запись того же архитектурного регистра. И да, единственная депонированная цепочка депо через RCX, так что цепочка может идти вперед. Вот почему адреса могут быть готовы на ранней стадии, в то время как операции загрузки / хранения все еще остаются узкими местами на пропускной способности порта 2/3.
Питер Кордес
3
Я удивлен, что предварительная выборка не помогла для memcpy в L3. Я думаю, что 10/12 LFB "достаточно" в этом случае. Хотя кажется странным: что является ограничивающим фактором? Базовое время -> L2 должно быть меньше, чем время L2 -> L3, поэтому в моей ментальной модели наличие большего количества буферов (больше общей загрузки) для второго этапа должно помочь.
BeeOnRope
3

Да, устройство предварительной загрузки L2 HW очень полезно!

Например, ниже приведены результаты на моей машине (i7-6700HQ), работающей на tinymembench . В первом столбце результатов все включенные средства предварительной выборки, во втором столбце результатов отключен стример L2 (но все остальные устройства предварительной выборки все еще включены).

В этом тесте используются 32 МБ буфера источника и назначения, которые намного больше, чем L3 на моей машине, поэтому он будет тестировать в основном пропуски в DRAM.

==========================================================================
== Memory bandwidth tests                                               ==
==                                                                      ==
== Note 1: 1MB = 1000000 bytes                                          ==
== Note 2: Results for 'copy' tests show how many bytes can be          ==
==         copied per second (adding together read and writen           ==
==         bytes would have provided twice higher numbers)              ==
== Note 3: 2-pass copy means that we are using a small temporary buffer ==
==         to first fetch data into it, and only then write it to the   ==
==         destination (source -> L1 cache, L1 cache -> destination)    ==
== Note 4: If sample standard deviation exceeds 0.1%, it is shown in    ==
==         brackets                                                     ==
==========================================================================

                                                       L2 streamer ON            OFF
 C copy backwards                                     :   7962.4 MB/s    4430.5 MB/s
 C copy backwards (32 byte blocks)                    :   7993.5 MB/s    4467.0 MB/s
 C copy backwards (64 byte blocks)                    :   7989.9 MB/s    4438.0 MB/s
 C copy                                               :   8503.1 MB/s    4466.6 MB/s
 C copy prefetched (32 bytes step)                    :   8729.2 MB/s    4958.4 MB/s
 C copy prefetched (64 bytes step)                    :   8730.7 MB/s    4958.4 MB/s
 C 2-pass copy                                        :   6171.2 MB/s    3368.7 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6193.1 MB/s    4104.2 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6198.8 MB/s    4101.6 MB/s
 C fill                                               :  13372.4 MB/s   10610.5 MB/s
 C fill (shuffle within 16 byte blocks)               :  13379.4 MB/s   10547.5 MB/s
 C fill (shuffle within 32 byte blocks)               :  13365.8 MB/s   10636.9 MB/s
 C fill (shuffle within 64 byte blocks)               :  13588.7 MB/s   10588.3 MB/s
 -
 standard memcpy                                      :  11550.7 MB/s    8216.3 MB/s
 standard memset                                      :  23188.7 MB/s   22686.8 MB/s
 -
 MOVSB copy                                           :   9458.4 MB/s    6523.7 MB/s
 MOVSD copy                                           :   9474.5 MB/s    6510.7 MB/s
 STOSB fill                                           :  23329.0 MB/s   22901.5 MB/s
 SSE2 copy                                            :   9073.1 MB/s    4970.3 MB/s
 SSE2 nontemporal copy                                :  12647.1 MB/s    7492.5 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   9106.0 MB/s    5069.8 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   9113.5 MB/s    5063.1 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11770.8 MB/s    7453.4 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11937.1 MB/s    7712.1 MB/s
 SSE2 2-pass copy                                     :   7092.8 MB/s    4355.2 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   7001.4 MB/s    4585.1 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   7055.1 MB/s    4557.9 MB/s
 SSE2 2-pass nontemporal copy                         :   5043.2 MB/s    3263.3 MB/s
 SSE2 fill                                            :  14087.3 MB/s   10947.1 MB/s
 SSE2 nontemporal fill                                :  33134.5 MB/s   32774.3 MB/s

В этих тестах наличие стримера L2 никогда не бывает медленным и часто почти вдвое быстрее.

В общем, вы можете заметить следующие закономерности в результатах:

  • Копии, как правило, больше подвержены влиянию, чем заливки.
  • standard memsetИ STOSB fill(они сводятся к тому же на этой платформе) являются наименее пострадали, загружается предварительно результатом является лишь несколько% быстрее , чем без него .
  • Стандарт memcpy, вероятно, является единственной копией, в которой используются 32-байтовые инструкции AVX, и он является одним из наименее затронутых копий, но предварительная загрузка по-прежнему на 40% быстрее, чем без.

Я также попытался включить и выключить три других устройства предварительной выборки, но они, как правило, не оказали практически никакого измеримого эффекта для этого теста.

BeeOnRope
источник
( vmovdqaИнтересный факт: AVX1 несмотря на то, что он "целочисленный".) Как вы думаете, цикл OP давал меньшую пропускную способность, чем glibc memcpy? И именно поэтому 12 LFB было достаточно, чтобы не отставать от требуемых нагрузок, идущих к L3, без использования дополнительной MLP из супер-очереди L2 <-> L3, которую может занимать стример L2? Это, вероятно, разница в вашем тесте. L3 должен работать с той же скоростью, что и ядро; у вас обоих есть микроархитектуры, эквивалентные четырёхъядерному Skylake-клиенту, поэтому, вероятно, похожая задержка L3?
Питер Кордес
@PeterCordes - извините, я, вероятно, должен был прояснить: этот тест был между 32 МБ буферами, поэтому он проверяет попадания DRAM, а не попадания L3. Я, хотя tmb вывести размер буфера, но я вижу, что нет - упс! Это было сделано намеренно: я не пытался объяснить точно сценарий 512 КиБ ОП, а просто ответил на главный вопрос о том, полезен ли стример L2 в сценарии, который показывает, что это так. Я думаю, что я использовал меньший размер буфера, чтобы более или менее воспроизвести результаты (я уже видел аналогичный результат в uarch-benchупомянутых в комментариях).
BeeOnRope
1
Я добавил размер буфера в ответ.
BeeOnRope
1
@ St.Antario: нет, это не проблема. Не знаю, почему вы думаете, что это может быть проблемой; Это не значит, что за смешивание инструкций AVX1 и AVX2 есть штраф. Суть моего комментария заключалась в том, что этот цикл требует только AVX1, но в этом ответе упоминается использование инструкций AVX2. Корпорация Intel расширила пути загрузки / хранения данных L1d до 32 байтов одновременно с внедрением AVX2, поэтому вы можете использовать доступность AVX2 как часть того, как вы выбираете реализацию memcpy, если вы выполняете диспетчеризацию во время выполнения ...
Питер Кордес
1
Как вы отключили prefetcher и какой? Было ли это software.intel.com/en-us/articles/… ? Форум software.intel.com/en-us/forums/intel-isa-extensions/topic/… говорит, что некоторые биты имеют разное значение.
osgx