web-dev-qa-db-fra.com

Performance inopinément médiocre et étrangement bimodale pour la boucle de magasin sur Intel Skylake

Je constate des performances inattendues pour une boucle de magasin simple comportant deux magasins: l'un avec un pas en avant de 16 octets et l'autre toujours au même endroit.1, comme ça:

volatile uint32_t value;

void weirdo_cpp(size_t iters, uint32_t* output) {

    uint32_t x = value;
    uint32_t          *rdx = output;
    volatile uint32_t *rsi = output;
    do {
        *rdx    = x;
        *rsi = x;

        rdx += 4;  // 16 byte stride
    } while (--iters > 0);
}

En Assemblée cette boucle probablement3 ressemble à:

weirdo_cpp:

...

align 16
.top:
    mov    [rdx], eax  ; stride 16
    mov    [rsi], eax  ; never changes

    add    rdx, 16

    dec    rdi
    jne    .top

    ret

Lorsque la région de mémoire consultée est en L2, je pense que cela s'exécutera à moins de 3 cycles par itération. Le deuxième magasin continue de frapper le même emplacement et devrait ajouter environ un cycle. Le premier magasin implique l’introduction d’une ligne à partir de L2 et donc l’éviction d’une ligne une fois toutes les 4 itérations. Je ne sais pas comment vous évaluez le coût de la couche 2, mais même si vous estimez de manière prudente que la couche 1 ne peut effectuer que l'un des cycles suivants: (a) valider un magasin ou (b) recevoir une ligne de la part de L2 ou (c) expulser une ligne vers L2, vous obtiendrez quelque chose comme 1 + 0,25 + 0,25 = 1,5 cycle pour le flux de magasin stride-16.

En effet, si vous commentez un magasin, vous obtenez environ 1,25 cycle par itération pour le premier magasin uniquement et environ 1,01 cycle par itération pour le deuxième magasin. 2,5 cycles par itération semblent donc être une estimation prudente.

La performance réelle est cependant très étrange. Voici un exemple typique du test harnais:

Estimated CPU speed:  2.60 GHz
output size     :   64 KiB
output alignment:   32
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.89 cycles/iter,  1.49 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 4.73 cycles/iter,  1.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.34 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.26 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.31 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.27 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0

Deux choses sont bizarres ici.

Il y a d’abord les synchronisations bimodales: il y a un mode rapide et un mode lent. Nous commençons en mode lent en prenant environ 7,3 cycles par itération et, à un moment donné, une transition à environ 3,9 cycles par itération. Ce comportement est cohérent et reproductible et les deux minutages sont toujours assez cohérents regroupés autour des deux valeurs. La transition apparaît dans les deux sens de mode lent à mode rapide et vice-versa (et parfois de plusieurs transitions en une fois).

L'autre chose étrange est la très mauvaise performance. Même en mode rapide, à environ 3,9 cycles, les performances sont bien pires que les 1,0 + 1,3 = 2,3 cycles de la distribution la plus mauvaise que vous attendiez en additionnant chacun des cas avec un seul magasin (et en supposant que absolument zéro travaillé peut être superposé lorsque les deux magasins sont dans la boucle). En mode lent, les performances sont médiocres par rapport à ce que vous attendez des premiers principes: il faut 7,3 cycles pour effectuer 2 magasins, et si vous le définissez en termes de largeur de bande de magasin L2, cela correspond à peu près à 29 cycles par magasin N2 (puisque nous ne stockons qu'une seule ligne de cache toutes les 4 itérations).

Skylake est enregistré comme ayant un débit de 64 B/cycle entre L1 et L2, ce qui est chemin supérieur au débit observé ici (environ 2 octets/cycle en mode lent) .

Qu'est-ce qui explique le faible débit et les performances bimodales et puis-je l'éviter?

Je suis également curieux de savoir si cela se reproduit sur d'autres architectures et même sur d'autres box Skylake. N'hésitez pas à inclure des résultats locaux dans les commentaires.

Vous pouvez trouver le code test et le harnais sur github . Il existe une Makefile pour les plates-formes Linux ou Unix, mais il devrait également être relativement facile à compiler sous Windows. Si vous voulez exécuter la variante asm, vous aurez besoin de nasm ou yasm pour l'assembly.4 - Si vous ne l'avez pas, vous pouvez simplement essayer la version C++.

Possibilités éliminées

Voici quelques possibilités que j'ai considérées et en grande partie éliminées. La plupart des possibilités sont éliminées du simple fait que la transition de performance est vue de manière aléatoire au milieu de la boucle d'analyse comparative}, _ alors que de nombreuses choses n'ont simplement pas changé (par exemple, si cela était lié au tableau de sortie alignement, cela ne pourrait pas changer au milieu d’une exécution car le même tampon est utilisé tout le temps). Je parlerai de cela comme élimination par défaut ci-dessous (même pour les choses qui sont l'élimination par défaut, un autre argument doit souvent être avancé).

  • Facteurs d'alignement: le tableau de sortie est aligné sur 16 octets et j'ai essayé un alignement allant jusqu'à 2 Mo sans modification. Également éliminé par le élimination par défaut.
  • Conflit avec d’autres processus sur la machine: l’effet est observé de manière plus ou moins identique sur une machine inactive et même sur une machine fortement chargée (par exemple, en utilisant stress -vm 4). De toute façon, le repère lui-même devrait être complètement central, puisqu'il correspond à la N2 et perf confirme qu'il y a très peu de manquements de N2 par itération (environ 1 fois toutes les 300 à 400 itérations, probablement liées au code printf.).
  • TurboBoost: TurboBoost est complètement désactivé, confirmé par trois lectures différentes en MHz.
  • Éléments d'économie d'énergie: le régulateur de performances est intel_pstate en mode performance. Aucune variation de fréquence n’est observée pendant le test (la CPU reste essentiellement verrouillée à 2,59 GHz).
  • Effets TLB: l’effet est présent même lorsque le tampon de sortie est situé dans une page volumineuse de 2 Mo. Dans tous les cas, les 64 entrées TLB de 4 000 Ko couvrent plus que la mémoire tampon de sortie de 128 Ko. perf ne signale aucun comportement particulièrement étrange du TLB.
  • Aliasing 4k: les versions plus anciennes et plus complexes de ce test de performances affichaient un aliasing de 4k mais cela a été éliminé car il y a pas de charges (il s'agit de charges pouvant aliaser de manière incorrecte les magasins précédents). Également éliminé par le élimination par défaut.
  • Conflits d'associativité L2: éliminés par la élimination par défaut et par le fait que cela ne disparaît pas, même avec des pages de 2 Mo, où nous pouvons être sûrs que le tampon de sortie est disposé de manière linéaire dans la mémoire physique.
  • Effets hyperthreading: HT est désactivé.
  • Prétraitement: seuls deux des prétracteurs peuvent être impliqués ici (les "prcipeurs", "LDC <-> L2"), car toutes les données résident en L1 ou L2, mais les performances sont identiques avec tous les précodeurs activés ou tous désactivés.
  • Interruptions: aucune corrélation entre le nombre d'interruptions et le mode lent. Le nombre total d'interruptions est limité, principalement des ticks d'horloge.

toplev.py

J'ai utilisé toplev.py qui implémente la méthode d'analyse Top Down d'Intel et, sans surprise, il identifie le point de repère comme étant lié au magasin:

BE             Backend_Bound:                                                      82.11 % Slots      [  4.83%]
BE/Mem         Backend_Bound.Memory_Bound:                                         59.64 % Slots      [  4.83%]
BE/Core        Backend_Bound.Core_Bound:                                           22.47 % Slots      [  4.83%]
BE/Mem         Backend_Bound.Memory_Bound.L1_Bound:                                 0.03 % Stalls     [  4.92%]
    This metric estimates how often the CPU was stalled without
    loads missing the L1 data cache...
    Sampling events:  mem_load_retired.l1_hit:pp mem_load_retired.fb_hit:pp
BE/Mem         Backend_Bound.Memory_Bound.Store_Bound:                             74.91 % Stalls     [  4.96%] <==
    This metric estimates how often CPU was stalled  due to
    store memory accesses...
    Sampling events:  mem_inst_retired.all_stores:pp
BE/Core        Backend_Bound.Core_Bound.Ports_Utilization:                         28.20 % Clocks     [  4.93%]
BE/Core        Backend_Bound.Core_Bound.Ports_Utilization.1_Port_Utilized:         26.28 % CoreClocks [  4.83%]
    This metric represents Core cycles fraction where the CPU
    executed total of 1 uop per cycle on all execution ports...
               MUX:                                                                 4.65 %           
    PerfMon Event Multiplexing accuracy indicator

Cela n’apporte pas vraiment beaucoup de lumière: nous savions déjà que ce devait être les magasins qui gâchent les choses, mais pourquoi? La description d'Intel de l'état ne dit pas grand chose.

Voici un résumé raisonnable de certaines des questions liées à l’interaction L1-L2.


1 C'est un MCVE grandement simplifié de ma boucle originale, qui était au moins trois fois plus grande et qui a fait beaucoup de travail supplémentaire, mais a présenté exactement les mêmes performances que cette version simple, goulot d'étranglement sur le même problème mystérieux.

3 Cela ressemble en particulier à exactement si vous écrivez l’Assembly à la main, ou si vous le compilez avec gcc -O1 (version 5.4.1), et probablement les compilateurs les plus raisonnables (volatile est utilisé pour éviter de perdre la plupart du temps). -dead deuxième magasin en dehors de la boucle).

4 Nul doute que vous pourriez convertir cela en syntaxe MASM avec quelques modifications mineures, car l’Assemblée est si triviale. Les demandes de tirage sont acceptées.

23
BeeOnRope

Sandy Bridge dispose de "pré-récupérateurs de matériel de données L1". Cela signifie qu'au départ, lorsque vous effectuez votre magasin, le processeur doit extraire les données de L2 à L1; mais après que cela se soit produit plusieurs fois, le pré-récupérateur de matériel remarque le modèle séquentiel de Nice et commence à pré-extraire les données de L2 vers L1 pour vous, de sorte que les données se trouvent soit en L1, soit le magasin.

0
Brendan