Estou no Whiskey Lake i7-8565U e estou analisando os contadores de perf e o tempo para copiar 512 KiB de dados (duas vezes mais que o tamanho do cache L2) e enfrentei alguns mal-entendidos sobre o trabalho do pré-buscador L2 HW.
No manual Intel Vol.4 MSR, há MSR que 0x1A4
o bit 0 é para controlar o pré-buscador L2 HW (1 para desativar).
Considere a seguinte referência:
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++;
}
}
Considere 2 execuções do compilado main.c
I .
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
Eu notei o contador:
12 611 038 018 cycle_activity.stalls_l2_miss
v / s
25 019 148 253 cycle_activity.stalls_l2_miss
sugerindo que o MSR que desabilita o pré-buscador L2 HW está sendo aplicado. Também outras coisas relacionadas à l2 / LLC diferem significativamente. A diferença é reproduzível em diferentes execuções . O problema é que quase não há diferença total time
e ciclos:
48 914 478 241 cycles
v / s
49 877 044 086 cycles
12,155774555 seconds time elapsed
v / s
12,231406658 seconds time elapsed
PERGUNTA: As
falhas L2 estão ocultas por outros limitadores de desempenho?
Em caso afirmativo, você pode sugerir quais contadores procurar para entender?
Respostas:
Sim, a serpentina L2 é realmente útil a maior parte do tempo.
O memcpy não tem nenhuma latência computacional para ocultar, então acho que pode permitir que os recursos de execução de OoO (tamanho do ROB) lidem com a latência de carga extra obtida de mais falhas de L2, pelo menos nesse caso em que você obtém todos os hits L3 de usando um conjunto de trabalho de tamanho médio (1MiB) que se encaixa no L3, nenhuma pré-busca é necessária para que os hits do L3 ocorram.
E as únicas instruções são carga / armazenamento (e sobrecarga de loop), portanto, a janela OoO inclui cargas de demanda por muito tempo à frente.
IDK se o pré-buscador espacial L2 e o pré-buscador L1d estiverem ajudando algum aqui.
Previsão para testar esta hipótese : aumente sua matriz para que você perca L3 e provavelmente verá uma diferença no tempo geral, uma vez que o exec OoO não seja suficiente para ocultar a latência de carga de todo o caminho para a DRAM. A pré-busca de HW desencadeando mais adiante pode ajudar alguns.
Os outros grandes benefícios da pré-busca de HW surgem quando ele pode acompanhar o seu cálculo, para que você obtenha hits de L2. (Em um loop que possui computação com uma cadeia de dependência de tamanho médio, mas não carregada por loop.)
Cargas de demanda e exec OoO podem fazer muito, tanto quanto usar a largura de banda de memória disponível (thread único), quando não houver outra pressão na capacidade do ROB.
Observe também que, nas CPUs Intel, cada falta de cache pode custar uma repetição de back-end (do RS / agendador) de Uops dependentes , um para L1d e L2, quando os dados chegarem. E depois disso, aparentemente, o núcleo otimiza spam ups enquanto aguarda a chegada de dados do L3.
(Consulte https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th e as operações de carga são desalocadas a partir do RS quando eles despacham, completam ou em outra hora? )
Não a falta de cache é carregada; nesse caso, seriam as instruções da loja. Mais especificamente, o armazenamento de dados para a porta 4. Isso não importa aqui; o uso de lojas de 32 bytes e o gargalo na largura de banda L3 significa que não estamos perto de 1 porta 4 uop por relógio.
fonte
16MiB
buffer e10
iterações e, de fato, obtive14,186868883 seconds
vs43,731360909 seconds
e46,76% of all LL-cache hits
vs99,32% of all LL-cache hits
;1 028 664 372 LLC-loads
vs1 587 454 298 LLC-loads
.Sim, o pré-buscador L2 HW é muito útil!
Por exemplo, encontre abaixo os resultados na minha máquina (i7-6700HQ) executando o tinymembench . A primeira coluna de resultados está com todos os pré-buscadores ativados, a segunda coluna de resultados está com o streamer L2 desativado (mas todos os outros pré-buscadores ainda ativados).
Este teste usa 32 buffers de origem e destino MiB, que são muito maiores que o L3 da minha máquina, portanto, ele testará principalmente falhas na DRAM.
Nesses testes, o streamer L2 nunca é mais lento e geralmente é quase o dobro da velocidade.
Em geral, você pode observar os seguintes padrões nos resultados:
standard memset
eSTOSB fill
(eles se resumem à mesma coisa nesta plataforma) são os menos afetados, com o resultado pré-buscado sendo apenas alguns% mais rápido do que sem.memcpy
é provavelmente a única cópia aqui que usa instruções AVX de 32 bytes e está entre as menos afetadas - mas a pré-busca ainda é ~ 40% mais rápida que a anterior.Também tentei ligar e desligar os outros três pré-buscadores, mas eles geralmente não tiveram quase nenhum efeito mensurável para esse benchmark.
fonte
vmovdqa
AVX1 é apesar de ser "inteiro".) Você acha que o loop do OP estava fornecendo largura de banda menor que o glibc memcpy? E é por isso que 12 LFBs foram suficientes para acompanhar as cargas de demanda indo para L3, sem tirar vantagem do MLP extra da super fila L2 <-> L3 que a serpentina L2 pode manter ocupada? Presumivelmente, essa é a diferença em seu teste. L3 deve correr na mesma velocidade que o núcleo; vocês dois têm microarquiteturas equivalentes a clientes Skylake de quatro núcleos, provavelmente latência L3 semelhante?uarch-bench
mencionado nos comentários).