2012-12-26 5 views
2

pouvez-vous m'aider s'il vous plaît pour savoir si cela prend plus longtemps pour une écriture de cache pour finir quand il y a plus de cœurs/caches contenant une copie de cette ligne. Je veux aussi mesurer/quantifier combien de temps cela prend réellement.Est-ce qu'une écriture de cache prend plus de temps avec plus de caches à invalider?

Je n'ai rien trouvé d'utile sur google et j'ai du mal à le mesurer moi-même et à interpréter ce que je mesure à cause des nombreuses choses qui peuvent arriver sur un processeur moderne. (réordonnancement, préchargement, mise en mémoire tampon et Dieu sait quoi)

Détails:

Mon processus de base de la mesure, il est à peu près comme suit:

write soemthing to the cacheline on processor 0 
read it on processors 1 to n. 

rdtsc 
write it on process 0 
rdtsc 

Je ne suis même pas sûr instructions en fait utiliser pour la lecture/écriture sur le processus 0 afin de s'assurer que l'écriture/invalider est terminée avant la mesure du temps final. En ce moment je bidouille avec un échange atomique (__sync_fetch_and_add()), mais il semble que le nombre de threads est lui-même important pour la durée de cette opération (pas le nombre de threads à invalider) - qui est probablement pas ce que je veux mesurer?!.

J'ai également essayé une lecture, puis une écriture, puis une barrière de mémoire (__sync_synchronize()). Cela ressemble plus à ce que je m'attends à voir, mais ici, je ne suis pas sûr si l'écriture est terminée lorsque la finale rdtsc a lieu.

Comme vous pouvez le deviner ma connaissance des internes du CPU est quelque peu limitée.

Toute aide est très appréciée!

ps: * J'utilise linux, gcc et pthreads pour les mesures. * Je veux le savoir pour avoir modélisé un algorithme parallèle.

Edit:

En une semaine (vous partez en vacances demain) je vais faire quelques recherches et après mon code et les notes et le lier ici (au cas où quelqu'un est intéressé), parce que le le temps que je peux consacrer à cela est limité.

+1

Si votre version de gcc est suffisamment récente, vous pouvez utiliser les builtins [__atomic] (http://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html), beaucoup plus performants. Vous pouvez également lire le protocole de cohérence du cache [MESI] (http://en.wikipedia.org/wiki/MESI). Ce n'est pas le seul protocole mais cela vous donnera une meilleure idée de la façon dont les opérations atomiques sont implémentées. –

Répondre

4

J'ai commencé à écrire une très longue réponse, décrivant exactement comment cela fonctionne, puis réalisé, je ne connais probablement pas assez les détails exacts. Donc, je vais faire une réponse plus courte ....

Donc, quand vous écrivez quelque chose sur un processeur, si ce n'est pas déjà dans ce cache des processeurs, il devra être récupéré, et après que le processeur a lu le données, il va effectuer l'écriture réelle. Ce faisant, il enverra un message cache-invalidate à TOUS les autres processeurs du système. Ceux-ci jetteront alors n'importe quel contenu. Si un autre processeur a un contenu "sale", il écrira lui-même les données et demandera une invalidation - dans ce cas, le premier processeur devra RECHARGER les données avant de finir son écriture (sinon, un autre élément dans la même cacheline peut être détruit). La lecture dans le cache sera requise sur tous les autres processeurs qui sont intéressés par cette ligne de cache.Le __sync_fetch_and_add() wilol utilise un préfixe "lock" [sur x86, les autres processeurs peuvent varier, mais l'idée générale sur les processeurs qui prennent en charge les verrous "par instruction" est sensiblement la même] - cela donnera un "Je veux cette cacheline EXCLUSIVEMENT, tout le monde s'il vous plaît abandonner et l'invalider ". Tout comme dans le premier cas, il se peut que le processeur doive relire tout ce qu'un autre processeur a pu souiller. Une barrière de mémoire ne garantit pas que les données sont mises à jour "en toute sécurité" - elle s'assurera simplement que "tout ce qui est arrivé à la mémoire est visible à tous les processeurs avant la fin de cet apprentissage".

La meilleure façon d'optimiser l'utilisation des processeurs est de partager le moins possible, et en particulier, d'éviter les "faux-partagés". Dans une référence il y a plusieurs années, il y avait une structure comme [simplifed] ceci:

struct stuff { 
    int x[2]; 
    ... other data ... total data a few cachelines. 
} data; 

void thread1() 
{ 
    for(... big number ...) 
     data.x[0]++; 
} 

void thread2() 
{ 
    for(... big number ...) 
     data.x[1]++; 
} 

int main() 
{ 
    start = timenow(); 

    create(thread1); 
    create(thread2); 

    end = timenow() - start; 
} 

Depuis CHAQUE temps thread1 écrit aux x [0], le processeur de thread2 a dû se débarrasser de lui est une copie de x [1] , et vice versa, le résultat est que le test SMP [vs juste en cours d'exécution thread1] était environ 15 fois plus lent. En modifiant la struct comme ceci:

struct stuff { 
    int x; 
    ... other data ... 
} data[2]; 

et

void thread1() 
{ 
    for(... big number ...) 
     data[0].x++; 
} 

nous avons obtenu 200% de la variante 1 fil [donner ou prendre quelques pour cent]

droit, de sorte que le processeur a des files d'attente de tampons où les opérations d'écriture sont stockées lorsque le processeur écrit en mémoire. Une instruction de barrière de mémoire (mfence, sfence ou lfence) est là pour s'assurer que toute opération exceptionnelle de type lecture/écriture, écriture ou lecture est complètement terminée avant que le processeur ne passe à l'instruction suivante. Normalement, le processeur continuerait simplement à suivre les instructions suivantes, et éventuellement l'opération de mémoire serait accomplie d'une manière ou d'une autre. Étant donné que les processeurs modernes ont beaucoup d'opérations parallèles et de tampons partout, il peut s'écouler un certain temps avant que quelque chose ne se répercute sur l'endroit où il finira par arriver. Donc, quand il est CRITIQUE de s'assurer que quelque chose a effectivement été fait avant de procéder (par exemple, si nous avons écrit un tas d'instructions à la mémoire vidéo, et nous voulons maintenant lancer la série de ces instructions, nous devons faire Assurez-vous que l'instruction 'instruction' a bien été finie, et qu'une autre partie du processeur ne travaille pas encore à la finition, alors utilisez un sfence pour vous assurer que l'écriture est vraiment arrivée - ce n'est peut-être pas un exemple très réaliste, mais je pense que vous avez l'idée.)

+0

Donc l'échange atomique serait la bonne opération pour la mesure? Dans ma mesure, il semble que quel que soit le nombre de cœurs à invalider, le nombre total de threads est important pour cela (le nombre de threads inactifs engendrés dans le programme - ce qui est bizarre, n'est-ce pas?). –

+0

Vous n'avez pas vraiment posté autant de détails sur ce que vous attendez, et comment les chiffres diffèrent de ce que vous attendez. Voyez-vous une grande différence si vous n'utilisez pas les opérations verrouillées (__sync ...)? –

+0

Si je fais une lecture, écriture, barrière de mémoire (__sync_synchronize) il n'augmente pas avec l'augmentation du nombre de threads inactifs, mais il augmente avec le nombre de noyaux invalidé (ce qui est ce que j'attends --- Mais je ne suis pas sûr si c'est ce que je veux mesurer? Est-ce que je ne sais pas.). Si je lis juste, écris ça sort par magie et je reçois seulement les cycles rdtsc eux-mêmes ~ 20-40, pas de différence que ce soit. Je vais passer quelques heures de plus (dans une semaine environ) à ce sujet, mais comme ce n'est pas une question très importante, mon temps est limité. Mais je peux poster mes trucs et notes à la fin si vous le souhaitez. –

4

Les écritures de cache doivent obtenir la propriété de la ligne avant de salir la ligne de cache. En fonction du modèle de cohérence de cache implémenté dans l'architecture de processeur, le temps pris pour cette étape varie. Les protocoles de cohérence les plus courants que je connais sont:

  • Espionne Protocole Cohérence: tous les caches de surveiller les lignes d'adresse pour les lignes de mémoire cache à savoir toutes les demandes de mémoire doivent être diffusées à tous CPUs dire non évolutive en augmentation de CPUs.
  • Protocole de cohérence basé sur l'annuaire: toutes les lignes de cache partagées entre plusieurs processeurs sont conservées dans un répertoire; donc, invalider/acquérir la propriété est une requête cpu point à point plutôt qu'une diffusion, c'est-à-dire plus évolutive, mais la latence souffre parce que le répertoire est un seul point de contention.

La plupart des architectures de CPU supportent quelque chose appelé PMU (Perf Monitoring Unit).Cette unité exporte les compteurs pour de nombreuses choses comme: les résultats de cache, les échecs, la latence d'écriture dans le cache, la latence de lecture, les hits tlb, etc. Veuillez consulter le manuel de CPU pour voir si cette information est disponible.

+0

Alors, que fait-on dans les processeurs x86 modernes? Et cela prend-il plus de temps sur plus de cœurs? Et comment puis-je le mesurer? –

Questions connexes