Я нахожусь на 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':
10 486 164 071 L1-dcache-loads (12,13%)
10 461 354 384 L1-dcache-load-misses # 99,76% of all L1-dcache hits (12,05%)
10 481 930 413 L1-dcache-stores (12,05%)
10 461 136 686 l1d.replacement (12,12%)
31 466 394 422 l1d_pend_miss.fb_full (12,11%)
211 853 643 294 l1d_pend_miss.pending (12,09%)
1 759 204 317 LLC-loads (12,16%)
31 007 LLC-load-misses # 0,00% of all LL-cache hits (12,16%)
3 154 901 630 LLC-stores (6,19%)
15 867 315 545 l2_rqsts.all_pf (9,22%)
0 sw_prefetch_access.t1_t2 (12,22%)
1 393 306 l2_lines_out.useless_hwpf (12,16%)
3 549 170 919 l2_rqsts.pf_hit (12,09%)
12 356 247 643 l2_rqsts.pf_miss (12,06%)
0 load_hit_pre.sw_pf (12,09%)
3 159 712 695 l2_rqsts.rfo_hit (12,06%)
1 207 642 335 l2_rqsts.rfo_miss (12,02%)
4 366 526 618 l2_rqsts.all_rfo (12,06%)
5 240 013 774 offcore_requests.all_data_rd (12,06%)
19 936 657 118 offcore_requests.all_requests (12,09%)
1 761 660 763 offcore_response.demand_data_rd.any_response (12,12%)
287 044 397 bus-cycles (12,15%)
36 816 767 779 resource_stalls.any (12,15%)
36 553 997 653 resource_stalls.sb (12,15%)
38 035 066 210 uops_retired.stall_cycles (12,12%)
24 766 225 119 uops_executed.stall_cycles (12,09%)
40 478 455 041 uops_issued.stall_cycles (12,05%)
24 497 256 548 cycle_activity.stalls_l1d_miss (12,02%)
12 611 038 018 cycle_activity.stalls_l2_miss (12,09%)
10 228 869 cycle_activity.stalls_l3_miss (12,12%)
24 707 614 483 cycle_activity.stalls_mem_any (12,22%)
24 776 110 104 cycle_activity.stalls_total (12,22%)
48 914 478 241 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':
10 508 027 832 L1-dcache-loads (12,05%)
10 463 643 206 L1-dcache-load-misses # 99,58% of all L1-dcache hits (12,09%)
10 481 296 605 L1-dcache-stores (12,12%)
10 444 854 468 l1d.replacement (12,15%)
29 287 445 744 l1d_pend_miss.fb_full (12,17%)
205 569 630 707 l1d_pend_miss.pending (12,17%)
5 103 444 329 LLC-loads (12,17%)
33 406 LLC-load-misses # 0,00% of all LL-cache hits (12,17%)
9 567 917 742 LLC-stores (6,08%)
1 157 237 980 l2_rqsts.all_pf (9,12%)
0 sw_prefetch_access.t1_t2 (12,17%)
301 471 l2_lines_out.useless_hwpf (12,17%)
218 528 985 l2_rqsts.pf_hit (12,17%)
938 735 722 l2_rqsts.pf_miss (12,17%)
0 load_hit_pre.sw_pf (12,17%)
4 096 281 l2_rqsts.rfo_hit (12,17%)
4 972 640 931 l2_rqsts.rfo_miss (12,17%)
4 976 006 805 l2_rqsts.all_rfo (12,17%)
5 175 544 191 offcore_requests.all_data_rd (12,17%)
15 772 124 082 offcore_requests.all_requests (12,17%)
5 120 635 892 offcore_response.demand_data_rd.any_response (12,17%)
292 980 395 bus-cycles (12,17%)
37 592 020 151 resource_stalls.any (12,14%)
37 317 091 982 resource_stalls.sb (12,11%)
38 121 826 730 uops_retired.stall_cycles (12,08%)
25 430 699 605 uops_executed.stall_cycles (12,04%)
41 416 190 037 uops_issued.stall_cycles (12,04%)
25 326 579 070 cycle_activity.stalls_l1d_miss (12,04%)
25 019 148 253 cycle_activity.stalls_l2_miss (12,03%)
7 384 770 cycle_activity.stalls_l3_miss (12,03%)
25 442 709 033 cycle_activity.stalls_mem_any (12,03%)
25 406 897 956 cycle_activity.stalls_total (12,03%)
49 877 044 086 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 скрыты другими ограничителями производительности?
Если да, можете ли вы подсказать, на какие счетчики посмотреть, чтобы понять это?
Ответы:
Да, стример 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 моп в такт.
источник
16MiB
буфером и10
итерациями и действительно получил14,186868883 seconds
vs43,731360909 seconds
и46,76% of all LL-cache hits
vs99,32% of all LL-cache hits
;1 028 664 372 LLC-loads
против1 587 454 298 LLC-loads
.Да, устройство предварительной загрузки L2 HW очень полезно!
Например, ниже приведены результаты на моей машине (i7-6700HQ), работающей на tinymembench . В первом столбце результатов все включенные средства предварительной выборки, во втором столбце результатов отключен стример L2 (но все остальные устройства предварительной выборки все еще включены).
В этом тесте используются 32 МБ буфера источника и назначения, которые намного больше, чем L3 на моей машине, поэтому он будет тестировать в основном пропуски в DRAM.
В этих тестах наличие стримера L2 никогда не бывает медленным и часто почти вдвое быстрее.
В общем, вы можете заметить следующие закономерности в результатах:
standard memset
ИSTOSB fill
(они сводятся к тому же на этой платформе) являются наименее пострадали, загружается предварительно результатом является лишь несколько% быстрее , чем без него .memcpy
, вероятно, является единственной копией, в которой используются 32-байтовые инструкции AVX, и он является одним из наименее затронутых копий, но предварительная загрузка по-прежнему на 40% быстрее, чем без.Я также попытался включить и выключить три других устройства предварительной выборки, но они, как правило, не оказали практически никакого измеримого эффекта для этого теста.
источник
vmovdqa
Интересный факт: AVX1 несмотря на то, что он "целочисленный".) Как вы думаете, цикл OP давал меньшую пропускную способность, чем glibc memcpy? И именно поэтому 12 LFB было достаточно, чтобы не отставать от требуемых нагрузок, идущих к L3, без использования дополнительной MLP из супер-очереди L2 <-> L3, которую может занимать стример L2? Это, вероятно, разница в вашем тесте. L3 должен работать с той же скоростью, что и ядро; у вас обоих есть микроархитектуры, эквивалентные четырёхъядерному Skylake-клиенту, поэтому, вероятно, похожая задержка L3?uarch-bench
упомянутых в комментариях).