2016-12-01 1 views
2

Récemment, j'ai rencontré un problème avec l'allocateur scalable Intel TBB. Le mode d'utilisation de base est comme ce qui suit,Allocation de mémoire alignée sur le cache sur le processeur Intel

  1. Quelques vecteurs de taille N * sizeof(double) sont attribués
  2. Générer un entier aléatoire M de telle sorte que M >= N/2 && M <= N.
  3. Les premiers éléments M de chaque vecteur sont accédés.
  4. Répétez l'étape 2. pour 1000 fois.

J'ai défini M comme aléatoire parce que je ne voulais pas comparer les performances pour une longueur fixe. Au lieu de cela, je veux obtenir une performance moyenne sur une gamme de longueurs de vecteurs.

La performance du programme diffère considérablement pour différentes valeurs de N. Ce n'est pas rare car les fonctions que je suis en train de tester ont été conçues pour prioriser les performances pour les grands N. Cependant, quand j'ai essayé de comparer la relation entre la performance et N, je trouve que, à un certain point, il y a une différence de deux plis lorsque N augmente de 1016 à 1017. Mon premier instinct est que la dégénérescence des performances pour N = 1016 n'a rien à voir avec une taille de vecteur plus petite, mais plutôt quelque chose à faire en cache. Très probablement, il y a un faux partage. La fonction sous dégustation utilise des instructions SIMD mais pas de mémoire de pile. Il lit un élément de 32 octets du premier vecteur, et après le calcul, il écrit 32 octets sur le second (et le troisième) vecteur. Si un faux partage se produit, probablement quelques dizaines de cycles sont perdus, et c'est exactement la pénalité de performance que j'observe. Certains profils le confirme. À l'origine, j'alignais chaque vecteur avec une limite de 32 octets, pour les instructions AVX. Pour résoudre le problème, j'ai aligné les vecteurs avec une limite de 64 octets. Cependant, j'observe toujours la même pénalité de performance. Aligner par 128 octets résoudre le problème.

J'ai creusé un peu plus. Intel TBB a un cache_aligned_allocator. Dans sa source, la mémoire est également alignée sur 128 octets.

C'est ce que je ne comprends pas. Si je ne me trompe pas, les processeurs x86 modernes ont une ligne de cache de 64 octets. CPUID confirme cela. Ce qui suit est l'information de cache de base de la CPU en cours d'utilisation, extraite d'un petit programme que j'ai écrit en utilisant CPUID pour vérifier les caractéristiques,

Vendor GenuineIntel 
Brand  Intel(R) Core(TM) i7-4960HQ CPU @ 2.60GHz 

==================================================================================================== 
Deterministic Cache Parameters (EAX = 0x04, ECX = 0x00) 
---------------------------------------------------------------------------------------------------- 
Cache level        1   1   2   3   4   
Cache type        Data  Instruction Unified  Unified  Unified  
Cache size (byte)      32K   32K   256K  6M   128M   
Maximum Proc sharing     2   2   2   16   16   
Maximum Proc physical     8   8   8   8   8   
Coherency line size (byte)    64   64   64   64   64   
Physical line partitions    1   1   1   1   16   
Ways of associative      8   8   8   12   16   
Number of sets       64   64   512   8192  8192   
Self initializing      Yes   Yes   Yes   Yes   Yes   
Fully associative      No   No   No   No   No   
Write-back invalidate     No   No   No   No   No   
Cache inclusiveness      No   No   No   Yes   No   
Complex cache indexing     No   No   No   Yes   Yes   
---------------------------------------------------------------------------------------------------- 

En outre, la source d'Intel TBB, l'alignement 128 octets a été marqué par un commentaire disant que c'était pour la rétrocompatibilité. Alors, pourquoi l'alignement de 64 octets n'était-il pas suffisant dans mon cas?

Répondre

2

Vous êtes touché par conflict misses. La raison pour laquelle cela se produit lorsque vous passez de 1016 à 1017 est que vous commencez à utiliser la dernière ligne de cache dans la liste des associés.

Votre cache est 32K à 8 voies, donc chaque jeu est de 4K. Votre ligne de cache de 64 octets peut contenir 8 doubles. Mais votre vecteur 1017-1024 utilise 8K pas 4K ??? 1024 * sizeof (double), bien que vous utilisiez N/2-> N pour que vous utilisiez (sauf quand exactement N/2) certaines des mêmes combinaisons de bits d'adresse inférieure deux fois pour chaque vecteur.

Vous n'obtenez pas le problème des hits de conflit tant que vous n'avez pas utilisé tout votre cache L1, ce que vous êtes sur le point de faire maintenant. En utilisant 1 vecteur pour la lecture et 2 vecteurs pour l'écriture, tous les 8K de long, donc en utilisant 24K, si vous utilisez 8K + de données supplémentaires pendant votre calcul, vous aurez une chance croissante d'expulser vos données choisies. Notez que vous n'utilisez que la première partie des vecteurs, mais ils n'en sont pas moins en conflit. Vous pourrez observer cela comme une augmentation des échecs de cache L1 quand vous allez de 1016 à 1017. Quand vous dépassez 1024 doubles, la pénalité de performance devrait disparaître pendant un court moment, jusqu'à ce que vous atteigniez L1 - manque de capacité de cache .

< imagerie ici un graphique qui montre un pic à 8 lorsque tous les ensembles sont utilisés>

De l'article fantastique par Ulrich Drepper: « Memory part 5: What programmers can do » enter image description here

+0

Merci pour l'explication détaillée. C'est beaucoup ce que je savais bien expliqué plus clairement. Cependant, existe-t-il un moyen d'éviter ce conflit sans augmenter la taille de la mémoire allouée? J'avais l'impression que l'alignement par une limite de 63 octets est suffisant pour éviter que deux vecteurs partagent la même ligne de cache dans la plupart des cas. Au lieu de cela, j'ai observé qu'un alignement de 128 octets était nécessaire. –

+0

Merci pour le graphique et le lien. Je vais lire à ce sujet en premier. –

+0

L'alignement n'a pas d'importance car c'est l'ensemble-associatif qui est le problème ici, ce qui signifie la mémoire totale utilisée par les vecteurs. – Surt