Le préfiltre L2 HW est-il vraiment utile?


10

Je suis sur Whisky Lake i7-8565U et les compteurs de et le temps nécessaire pour copier 512 Ko de données (deux fois plus que la taille du cache L2) et fait face à un malentendu concernant le travail du préfiltre L2 HW.

Dans le manuel Intel vol.4 MSR, il y a MSR dont 0x1A4le bit 0 sert à contrôler le préfiltre L2 HW (1 à désactiver).


Considérez le repère suivant:

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++;
    }
}

Considérez 2 exécutions du compilé main.c

Je .

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

J'ai remarqué le comptoir:

12 611 038 018 cycle_activity.stalls_l2_miss contre
25 019 148 253 cycle_activity.stalls_l2_miss

suggérant que le MSR désactivant le préfiltre L2 HW est appliqué. Les autres éléments liés à l2 / LLC diffèrent également de manière significative. La différence est reproductible sur différentes séries . Le problème est qu'il n'y a presque pas de différence dans les total timecycles:

48 914 478 241 cycles contre
49 877 044 086 cycles

12,155774555 seconds time elapsed contre
12,231406658 seconds time elapsed

QUESTION: Les
échecs L2 sont-ils cachés par d'autres limiteurs de performances?
Si oui, pouvez-vous suggérer quels compteurs regarder pour le comprendre?


4
En règle générale: toute copie de mémoire non implémentée de manière abyssale est liée à la mémoire. Même lorsqu'il ne touche que le cache L1. Les frais généraux de tout accès à la mémoire sont tout simplement tellement plus élevés que ce qu'il faut à un processeur pour en ajouter deux et deux ensemble. Dans votre cas, vous utilisez même des instructions AVX pour réduire la quantité d'instructions par octet copié. Où que se trouvent vos données (L1, L2, LLC, mémoire), le débit du composant mémoire associé sera votre goulot d'étranglement.
— cmaster - réintègre monica le

Réponses:


5

Oui, le streamer L2 est vraiment utile la plupart du temps.

memcpy n'a pas de latence de calcul à masquer, donc je suppose qu'il peut se permettre de laisser les ressources d'exécution OoO (taille ROB) gérer la latence de charge supplémentaire que vous obtenez à partir de plus de missiles L2, au moins dans ce cas où vous obtenez tous les hits L3 de en utilisant un ensemble de travail de taille moyenne (1 Mo) compatible avec L3, aucune prélecture n'est nécessaire pour que les hits L3 se produisent.

Et les seules instructions sont load / store (et boucle overhead), donc la fenêtre OoO inclut les charges de demande pour assez loin.

IDK si le préfixeur spatial L2 et le préfiltre L1d sont utiles ici.


Prédiction pour tester cette hypothèse : agrandissez votre tableau pour que vous obteniez des échecs L3 et vous verrez probablement une différence dans le temps global une fois que OoO exec ne suffira pas à masquer la latence de charge d'aller jusqu'à la DRAM. HW prefetch se déclenchant plus loin peut aider certains.

Les autres grands avantages de la pré-lecture HW viennent quand cela peut suivre votre calcul, vous obtenez donc des hits L2. (Dans une boucle qui a un calcul avec une chaîne de dépendance de longueur moyenne mais non portée par la boucle.)

Les charges de demande et les exécutions OoO peuvent faire beaucoup en ce qui concerne l'utilisation de la bande passante mémoire disponible (à un seul thread), lorsqu'il n'y a pas d'autre pression sur la capacité ROB.


Notez également que sur les processeurs Intel, chaque échec de cache peut coûter une relecture back-end (à partir du RS / ordonnanceur) d' uops dépendants , un pour les échecs L1d et L2 lorsque les données devraient arriver. Et après cela, apparemment, le noyau spams optimiste uops en attendant que les données arrivent de L3.

(Voir https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th et Are load ops deallocated from the RS quand ils expédient, se terminent ou à un autre moment? )

Pas le cache-miss se charge lui-même; dans ce cas, ce serait les instructions du magasin. Plus précisément, l'uop store-data pour le port 4. Cela n'a pas d'importance ici; l'utilisation de magasins de 32 octets et d'un goulot d'étranglement sur la bande passante L3 signifie que nous ne sommes pas proches de 1 port 4 uop par horloge.


2
@ St.Antario: hein? Ça n'a aucun sens; vous êtes lié à la mémoire, vous n'avez donc pas de goulot d'étranglement frontal, donc le LSD n'est pas pertinent. (Cela évite de les récupérer à partir du cache uop, ce qui permet d'économiser de l'énergie). Ils prennent toujours de la place dans le ROB jusqu'à ce qu'ils puissent prendre leur retraite. Ils ne sont pas si importants, mais pas négligeables non plus.
— Peter Cordes

2
agrandissez votre tableau pour que vous obteniez des échecs en L3 et vous verrez probablement une différence. J'ai exécuté un certain nombre de tests avec 16MiBtampon et 10itérations et j'ai en fait obtenu 14,186868883 secondsvs 43,731360909 secondset 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: en vous inscrivant en renommant! C'est l'un des éléments les plus importants de l'exécutif OoO, en particulier sur un ISA pauvre en registres comme x86. Voir Pourquoi le mulss ne prend-il que 3 cycles sur Haswell, différent des tableaux d'instructions d'Agner? (Déroulement des boucles FP avec plusieurs accumulateurs) . Et BTW, normalement vous voudriez faire 2 chargements puis 2 magasins, pas charger / stocker charge / stocker. Meilleures chances d'éviter ou d'atténuer les blocages d'alias 4k, car les charges ultérieures (que le matériel doit détecter comme chevauchant ou non les magasins précédents) sont plus éloignées.
— Peter Cordes

2
@ St.Antario: oui, bien sûr. Le guide d'optimisation d'Agner Fog explique également OoO exec avec le changement de nom du registre, tout comme wikipedia. BTW, le renommage de registre évite également les dangers WAW, ne laissant que de vraies dépendances (RAW). Ainsi, les chargements peuvent même se terminer hors service, sans attendre qu'un chargement précédent ait fini d' écrire le même registre architectural. Et oui, la seule chaîne dep transportée en boucle est via RCX, afin que la chaîne puisse avancer. C'est pourquoi les adresses peuvent être prêtes tôt, tandis que les uops de chargement / stockage sont toujours goulot d'étranglement sur le débit du port 2/3.
— Peter Cordes

3
Je suis surpris que la prélecture n'ait pas aidé la mémoire dans L3. Je suppose que les 10/12 LFB sont "suffisants" dans ce cas. Cela semble étrange cependant: quel est le facteur limitant? Le temps de base -> L2 devrait être inférieur au temps L2 -> L3, donc dans mon modèle mental, avoir plus de tampons (plus d'occupation totale) pour la deuxième étape devrait aider.
— BeeOnRope

3

Oui, le préfiltre L2 HW est très utile!

Par exemple, trouvez ci-dessous les résultats sur ma machine (i7-6700HQ) exécutant tinymembench . La première colonne des résultats est avec tous les prefetchers activés, la deuxième colonne des résultats est avec le streamer L2 désactivé (mais tous les autres prefetchers toujours activés).

Ce test utilise des tampons de source et de destination de 32 Mio, qui sont beaucoup plus grands que le L3 sur ma machine, il testera donc principalement les manquements à la 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

Dans ces tests, le streamer L2 n'est jamais plus lent et est souvent presque deux fois plus rapide.

En général, vous pouvez remarquer les modèles suivants dans les résultats:

  • Les copies semblent généralement plus affectées que les remplissages.
  • Le standard memsetetSTOSB fill (ceux-ci se résument à la même chose sur cette plate-forme) sont les moins affectés, le résultat de la prélecture n'étant que de quelques% plus rapide que sans.
  • La norme memcpyest probablement la seule copie ici qui utilise des instructions AVX de 32 octets, et elle est parmi les moins affectées des copies - mais la prélecture est toujours ~ 40% plus rapide que sans.

J'ai également essayé d'activer et de désactiver les trois autres prefetchers, mais ils n'avaient généralement presque aucun effet mesurable pour cette référence.


(Fait amusant: vmovdqaAVX1 est-il un "entier"). Pensez-vous que la boucle de l'OP fournissait une bande passante inférieure à celle de la glibc memcpy? Et c'est pourquoi 12 LFB étaient suffisants pour suivre les charges de demande allant à L3, sans profiter du MLP supplémentaire de la superqueue L2 <-> L3 que le streamer L2 peut garder occupé? C'est probablement la différence dans votre test. L3 devrait fonctionner à la même vitesse que le noyau; vous avez tous les deux des micro-architectures quadricœurs équivalentes à Skylake, donc une latence L3 probablement similaire?
— Peter Cordes

@PeterCordes - désolé, j'aurais probablement dû être clair: ce test était entre 32 tampons MiB, donc il teste les hits DRAM et non les hits L3. Je pense que tmb affiche la taille du tampon, mais je vois que ce n'est pas le cas - oups! C'était intentionnel: je n'essayais pas d'expliquer exactement le scénario 512 Ko de l'OP, mais je répondais simplement à la question principale de savoir si le streamer L2 est utile avec un scénario qui le montre. Je suppose que j'ai utilisé une taille de tampon plus petite, je pourrais plus ou moins reproduire les résultats (j'ai déjà vu un résultat similaire uarch-benchmentionné dans les commentaires).
— BeeOnRope

1
J'ai ajouté la taille du tampon à la réponse.
— BeeOnRope

1
@ St.Antario: Non, ce n'est pas un problème. Aucune idée pourquoi vous pensez que cela pourrait être un problème; ce n'est pas comme s'il y avait une pénalité pour le mélange des instructions AVX1 et AVX2. Le point de mon commentaire était que cette boucle ne nécessite que AVX1, mais cette réponse mentionne l'utilisation des instructions AVX2. Il se trouve qu'Intel a élargi les chemins de données de chargement / stockage L1d à 32 octets en même temps que l'introduction d'AVX2, vous pouvez donc utiliser la disponibilité d'AVX2 dans le cadre de la façon dont vous sélectionnez une implémentation memcpy si vous effectuez une répartition à l'exécution ...
— Peter Cordes

1
Comment avez-vous désactivé le préfiltre et lequel? Était-ce software.intel.com/en-us/articles/… ? Le logiciel du forum.intel.com/en-us/forums/intel-isa-extensions/topic/… indique que certains bits ont une signification différente.
— osgx
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.