O pré-buscador L2 HW é realmente útil?

10

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 0x1A4o 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':

    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

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 timee 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?

St.Antario
fonte
4
Como regra geral: qualquer cópia de memória implementada de maneira não abismal é vinculada à memória. Mesmo quando atinge apenas o cache L1. As despesas gerais de qualquer acesso à memória são simplesmente muito mais altas do que o necessário para uma CPU adicionar duas e duas juntas. No seu caso, você está mesmo usando as instruções do AVX para reduzir a quantidade de instruções por byte copiado. Onde quer que seus dados sejam encontrados (L1, L2, LLC, memória), a taxa de transferência do componente de memória associado será seu gargalo.
cmaster - reinstate monica em

Respostas:

5

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.

Peter Cordes
fonte
2
@ St.Antario: hein? Isso não faz sentido; você está vinculado à memória para não ter um gargalo no front-end; portanto, o LSD é irrelevante. (Evita buscá-los novamente no cache uop, economizando energia). Eles ainda ocupam espaço no ROB até poderem se aposentar. Eles não são que significativa, mas não desprezível quer.
Peter Cordes
2
aumente sua matriz para obter erros L3 e provavelmente verá uma diferença. Eu executei vários testes com 16MiBbuffer e 10iterações e, de fato, obtive 14,186868883 secondsvs 43,731360909 secondse 46,76% of all LL-cache hitsvs 99,32% of all LL-cache hits; 1 028 664 372 LLC-loadsvs 1 587 454 298 LLC-loads .
St.Antario
4
@ St.Antario: renomeando o registro! Essa é uma das partes mais importantes do exec de OoO, especialmente em um ISA com poucos registros, como o x86. Veja Por que os mulss levam apenas 3 ciclos no Haswell, diferente das tabelas de instruções de Agner? (Desenrolando loops FP com vários acumuladores) . E, BTW, normalmente você deseja fazer 2 cargas e 2 lojas, não carregar / armazenar carga / armazenar. Melhor chance de evitar ou atenuar o aliasing de 4k para porque as cargas posteriores (que o HW deve detectar como se sobrepõem às lojas anteriores ou não) estão mais distantes.
Peter Cordes em
2
@ St.Antario: sim, é claro. O guia de otimização do Agner Fog também explica o OoO exec com a renomeação de registros, assim como a wikipedia. BTW, renomear o registro também evita os perigos WAW, deixando apenas dependências verdadeiras (RAW). Assim, as cargas podem ser concluídas fora de ordem, sem esperar que uma carga anterior termine de escrever o mesmo registro de arquitetura. E sim, a única cadeia dep transportada por loop é através do RCX, para que a cadeia possa avançar. É por isso que os endereços podem estar prontos cedo, enquanto os carregamentos de armazenamento / armazenamento ainda estão com gargalo na taxa de transferência da porta 2/3.
Peter Cordes
3
Estou surpreso que a pré-busca não tenha ajudado o memcpy no L3. Eu acho que os 10/12 LFBs são "suficientes" nesse caso. Parece estranho: qual é o fator limitante lá? O tempo do núcleo -> L2 deve ser menor que o tempo do L2 -> L3, portanto, no meu modelo mental, ter mais amortecedores (maior ocupação total) para a segunda perna deve ajudar.
BeeOnRope
3

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.

==========================================================================
== 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

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:

  • As cópias geralmente parecem ser mais afetadas do que preenchimentos.
  • O standard memsete STOSB 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.
  • O Standard 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.

BeeOnRope
fonte
(Curiosidade: o vmovdqaAVX1 é 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?
Peter Cordes
@ PeterCordes - desculpe, eu provavelmente deveria ter sido claro: esse teste foi entre 32 buffers MiB, por isso está testando DRAM hits e não L3. Embora tmb produza o tamanho do buffer, mas vejo que não - oops! Isso foi intencional: eu não estava tentando explicar exatamente o cenário de 512 KiB do OP, mas apenas respondi à pergunta principal se o streamer L2 é útil com um cenário que mostra que é. Eu acho que usei um tamanho de buffer menor que eu poderia reproduzir mais ou menos os resultados (eu já vi um resultado semelhante uarch-benchmencionado nos comentários).
BeeOnRope
11
Eu adicionei o tamanho do buffer à resposta.
BeeOnRope
11
@ St.Antario: Não, não é um problema. Não faço ideia por que você acha que isso pode ser um problema; não há penalidade por misturar as instruções AVX1 e AVX2. O ponto do meu comentário foi que esse loop requer apenas o AVX1, mas essa resposta menciona o uso das instruções do AVX2. A Intel ampliou os caminhos de carregamento / armazenamento de dados L1d para 32 bytes ao mesmo tempo em que introduziu o AVX2; portanto, você pode usar a disponibilidade do AVX2 como parte de como você seleciona uma implementação de memória se estiver fazendo o envio em tempo de execução ...
Peter Cordes
11
Como você desligou o pré-buscador e qual? Foi software.intel.com/en-us/articles/… ? O fórum software.intel.com/en-us/forums/intel-isa-extensions/topic/… diz que alguns bits têm um significado diferente.
osgx 22/02