¿Es realmente útil el prefetcher L2 HW?

10

Estoy en Whisky Lake i7-8565U y analizo los contadores de rendimiento y el tiempo para copiar 512 KiB de datos (dos veces más que el tamaño de caché L2) y enfrenté algunos malentendidos con respecto al trabajo de captación previa de L2 HW.

En el Manual Intel Vol.4 MSR hay MSR, 0x1A4el bit 0 es para controlar la captura previa de L2 HW (1 para deshabilitar).


Considere el siguiente punto de referencia:

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 ejecuciones del 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

Me di cuenta del mostrador:

12 611 038 018 cycle_activity.stalls_l2_miss v / s
25 019 148 253 cycle_activity.stalls_l2_miss

lo que sugiere que se está aplicando el MSR que desactiva el prefetcher L2 HW. También otras cosas relacionadas con l2 / LLC difieren significativamente. La diferencia es reproducible en diferentes ejecuciones . El problema es que casi no hay diferencia en total timey 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

PREGUNTA:
¿Los errores de L2 están ocultos por otros limitadores de rendimiento?
Si es así, ¿puede sugerir qué contadores mirar para entenderlo?

St.Antario
fuente
44
Como regla general: cualquier copia de memoria implementada de manera no abismal está vinculada a la memoria. Incluso cuando solo alcanza el caché L1. Los gastos generales de cualquier acceso a la memoria son simplemente mucho más altos que los que necesita una CPU para agregar dos y dos juntos. En su caso, incluso está utilizando instrucciones AVX para reducir la cantidad de instrucciones por byte copiado. Dondequiera que se encuentren sus datos (L1, L2, LLC, memoria), el rendimiento del componente de memoria asociado será su cuello de botella.
cmaster - reinstalar a monica

Respuestas:

5

Sí, el streamer L2 es realmente útil la mayor parte del tiempo.

memcpy no tiene ninguna latencia computacional que ocultar, así que supongo que puede permitirse el lujo de permitir que los recursos ejecutivos de OoO (tamaño ROB) manejen la latencia de carga adicional que obtienes de más fallas L2, al menos en este caso donde obtienes todos los golpes L3 de utilizando un conjunto de trabajo de tamaño mediano (1MiB) que se ajusta a L3, no es necesario realizar una captación previa para que los golpes de L3 sucedan.

Y las únicas instrucciones son cargar / almacenar (y sobrecarga de bucle), por lo que la ventana OoO incluye cargas de demanda para bastante adelante.

IDK si el prefetcher espacial L2 y el prefetcher L1d están ayudando a alguno aquí.


Predicción para probar esta hipótesis : haga que su matriz sea más grande para que pierda L3 y probablemente verá una diferencia en el tiempo general una vez que OoO exec no sea suficiente para ocultar la latencia de carga de ir a DRAM. La captación previa de HW que se activa más adelante puede ayudar a algunos.

Los otros grandes beneficios de la captación previa de HW vienen cuando puede mantenerse al día con su cálculo, por lo que obtiene golpes L2. (En un bucle que tiene cálculo con una cadena de dependencia de longitud media pero no transportada por bucle).

Las cargas de demanda y OoO exec pueden hacer mucho en cuanto al uso del ancho de banda de memoria disponible (un solo subproceso), cuando no hay otra presión sobre la capacidad de ROB.


También tenga en cuenta que en las CPU de Intel, cada pérdida de caché puede costar una reproducción de fondo (desde el RS / planificador) de uops dependientes , uno para L1d y L2 falla cuando se espera que lleguen los datos. Y después de eso, aparentemente el núcleo optimistamente espanta uops mientras espera que lleguen datos desde L3.

(Consulte https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th y las operaciones de carga se desasignan del ¿RS cuando envían, completa o en otro momento? )

No la carga de caché-miss en sí misma; en este caso serían las instrucciones de la tienda. Más específicamente, el almacenamiento de datos de la tienda para el puerto 4. Eso no importa aquí; El uso de tiendas de 32 bytes y el cuello de botella en el ancho de banda L3 significa que no estamos cerca de 1 puerto 4 uop por reloj.

Peter Cordes
fuente
2
@ St.Antario: ¿eh? Eso no tiene sentido; está limitado a la memoria, por lo que no tiene un cuello de botella frontal, por lo que el LSD es irrelevante. (Evita volver a buscarlos desde la caché de uop, ahorrando algo de energía). Todavía toman espacio en el ROB hasta que puedan retirarse. No son que significativa, pero no despreciable tampoco.
Peter Cordes
2
haga que su matriz sea más grande para que pierda L3 y probablemente vea una diferencia . Ejecuté varias pruebas con 16MiBbuffer e 10iteraciones y de hecho obtuve 14,186868883 secondsvs 43,731360909 secondsy 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
44
@ St.Antario: por cambio de nombre de registro! Esta es una de las piezas más importantes de OoO exec, especialmente en un ISA de registro pobre como x86. Vea ¿Por qué mulss solo toma 3 ciclos en Haswell, diferente de las tablas de instrucciones de Agner? (Desenrollar bucles FP con múltiples acumuladores) . Y por cierto, normalmente querrías hacer 2 cargas y luego 2 tiendas, no cargar / almacenar cargar / almacenar. Mayor probabilidad de evitar o mitigar los puestos de alias de 4k porque las cargas posteriores (que el HW tiene que detectar como superpuestas a las tiendas anteriores o no) están más lejos.
Peter Cordes
2
@ St.Antario: sí, por supuesto. La guía de optimización de Agner Fog también explica el ejecutivo de OoO con cambio de nombre de registro, al igual que wikipedia. Por cierto, el cambio de nombre del registro también evita los riesgos WAW, dejando solo dependencias verdaderas (RAW). Por lo tanto, las cargas pueden completarse sin orden, sin esperar a que una carga anterior termine de escribir el mismo registro arquitectónico. Y sí, la única cadena dep transportada en bucle es a través de RCX, por lo que la cadena puede funcionar por delante. Es por eso que las direcciones pueden estar listas temprano, mientras que los uops de carga / almacenamiento todavía tienen un cuello de botella en el rendimiento del puerto 2/3.
Peter Cordes
3
Me sorprende que la captación previa no haya ayudado para la memoria en L3. Supongo que el 10/12 LFBs es "suficiente" en ese caso. Sin embargo, parece extraño: ¿cuál es el factor limitante allí? El núcleo -> el tiempo L2 debería ser menor que el tiempo L2 -> L3, por lo que en mi modelo mental tener más amortiguadores (más ocupación total) para el segundo tramo debería ayudar.
BeeOnRope
3

Sí, ¡el prefetcher L2 HW es muy útil!

Por ejemplo, encuentre los siguientes resultados en mi máquina (i7-6700HQ) que ejecuta tinymembench . La primera columna de resultados está con todos los prefetchers encendidos, la segunda columna de resultados está con el streamer L2 apagado (pero todos los otros prefetchers todavía están encendidos).

Esta prueba utiliza 32 búferes de origen y destino MiB, que son mucho más grandes que el L3 en mi máquina, por lo que probará la mayoría de las fallas en 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

En estas pruebas, tener el streamer L2 nunca es más lento y suele ser casi el doble de rápido.

En general, puede observar los siguientes patrones en los resultados:

  • Las copias generalmente parecen estar más afectadas que los rellenos.
  • Los standard memsety STOSB fill(estos se reducen a lo mismo en esta plataforma) son los menos afectados, con el resultado obtenido previamente solo un poco más rápido que sin ellos.
  • Standard memcpyes probablemente la única copia aquí que usa instrucciones AVX de 32 bytes, y está entre las menos afectadas, pero la captación previa sigue siendo ~ 40% más rápida que sin ella.

También intenté encender y apagar los otros tres captadores previos, pero en general casi no tuvieron un efecto medible para este punto de referencia.

BeeOnRope
fuente
(Dato vmovdqacurioso : es AVX1 a pesar de ser "entero"). ¿Crees que el bucle del OP estaba dando un ancho de banda menor que la memoria glibc? Y es por eso que 12 LFB fueron suficientes para mantenerse al día con las cargas de demanda que van a L3, sin aprovechar el MLP adicional de la supercuesta L2 <-> L3 que el transmisor L2 puede mantener ocupado. Esa es probablemente la diferencia en su prueba. L3 debe funcionar a la misma velocidad que el núcleo; Ambos tienen microarquitecturas equivalentes al cliente Skylake de cuatro núcleos, ¿es probable que tengan una latencia L3 similar?
Peter Cordes
@PeterCordes: lo siento, probablemente debería haber sido claro: esta prueba fue entre 32 búferes MiB, por lo que está probando golpes DRAM, no golpes L3. Pensé que tmb generaba el tamaño del búfer, pero veo que no, ¡vaya! Eso fue intencional: no estaba tratando de explicar exactamente el escenario de 512 KiB del OP, sino simplemente responder a la pregunta principal de si el streamer L2 es útil con un escenario que lo demuestre. Supongo que usé un tamaño de búfer más pequeño, pude reproducir más o menos los resultados (ya vi un resultado similar uarch-benchen los comentarios).
BeeOnRope
1
Agregué el tamaño del búfer a la respuesta.
BeeOnRope
1
@ St.Antario: No, no es un problema. No tengo idea de por qué crees que podría ser un problema; No es que haya una penalización por mezclar las instrucciones AVX1 y AVX2. El punto de mi comentario fue que este bucle solo requiere AVX1, sin embargo, esta respuesta menciona el uso de instrucciones AVX2. Intel amplió las rutas de datos de carga / almacenamiento de L1d a 32 bytes al mismo tiempo que presentaba AVX2, por lo que podría usar la disponibilidad de AVX2 como parte de cómo seleccionar una implementación de memoria si está haciendo un despacho de tiempo de ejecución ...
Peter Cordes
1
¿Cómo apagaste el prefetcher y cuál? ¿Fue software.intel.com/en-us/articles/… ? Forum software.intel.com/en-us/forums/intel-isa-extensions/topic/… dice que algunos bits tienen un significado diferente.
osgx