Utilisation du compteur d’horodatage et de clock_gettime pour les erreurs de cache

Pour faire suite à cette rubrique , afin de calculer la latence de mémoire manquante, j’ai écrit le code suivant en utilisant _mm_clflush , __rdtsc et _mm_lfence (basé sur le code de cette question / réponse ).

Comme vous pouvez le constater dans le code, je commence par charger le tableau dans le cache. Ensuite, je vide un élément et par conséquent la ligne de cache est expulsée de tous les niveaux de cache. Je mets _mm_lfence afin de préserver l’ordre pendant -O3 .

Ensuite, j’ai utilisé un compteur d’horodatage pour calculer le array[0] latence ou de lecture array[0] . Comme vous pouvez le constater entre deux lfence , il existe trois instructions: deux lfence et une read . Je dois donc soustraire les lfence généraux de l’ lfence . La dernière section du code calcule cette surcharge.

À la fin du code, le temps système et le temps de latence manqué sont imprimés. Cependant, le résultat n’est pas valide!

 #include  #include  #include  int main() { int array[ 100 ]; for ( int i = 0; i < 100; i++ ) array[ i ] = i; uint64_t t1, t2, ov, diff; _mm_lfence(); _mm_clflush( &array[ 0 ] ); _mm_lfence(); _mm_lfence(); t1 = __rdtsc(); _mm_lfence(); int tmp = array[ 0 ]; _mm_lfence(); t2 = __rdtsc(); _mm_lfence(); diff = t2 - t1; printf( "diff is %lu\n", diff ); _mm_lfence(); t1 = __rdtsc(); _mm_lfence(); _mm_lfence(); t2 = __rdtsc(); _mm_lfence(); ov = t2 - t1; printf( "lfence overhead is %lu\n", ov ); printf( "miss cycles is %lu\n", diff-ov ); return 0; } 

Cependant, la sortie n’est pas valide

 $ gcc -O3 -o flush1 flush1.c $ taskset -c 0 ./flush1 diff is 161 lfence overhead is 147 miss cycles is 14 $ taskset -c 0 ./flush1 diff is 161 lfence overhead is 154 miss cycles is 7 $ taskset -c 0 ./flush1 diff is 147 lfence overhead is 154 miss cycles is 18446744073709551609 

Toute pensée?

Ensuite, j’ai essayé la fonction clock_gettime afin de calculer la latence de miss comme ci-dessous

  _mm_lfence(); _mm_clflush( &array[ 0 ] ); _mm_lfence(); struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); _mm_lfence(); int tmp = array[ 0 ]; _mm_lfence(); clock_gettime(CLOCK_MONOTONIC, &end); diff = 1000000000 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec; printf("miss elapsed time = %lu nanoseconds\n", diff); 

La sortie est miss elapsed time = 578 nanoseconds . Est-ce fiable?

UPDATE1:

Merci à Peter et Hadi, pour résumer les réponses jusqu’à présent, j’ai découvert

1- Les variables non utilisées sont omises dans la phase d’optimisation et c’est la raison pour laquelle j’ai vu des valeurs étranges dans la sortie. Grâce à la réponse de Peter, il existe des moyens de résoudre ce problème.

2- clock_gettime n’est pas adapté à une telle résolution et cette fonction est utilisée pour des délais plus longs.

Pour contourner le problème, j’ai essayé de placer le tableau dans le cache, puis de vider tous les éléments pour être sûr que tous les éléments sont expulsés de tous les niveaux de cache. Ensuite, j’ai mesuré la latence du array[0] , puis du array[20] . Puisque chaque élément est de 4 octets, la distance est de 80 octets. Je m’attends à avoir deux erreurs de cache. Cependant, la latence de array[20] est similaire à un hit en cache. Une hypothèse sûre est que la ligne de cache ne fait pas 80 octets. Donc, peut-être que array[20] est pré-téléchargé par le matériel. Pas toujours, mais je vois encore des résultats étranges

  for ( int i = 0; i < 100; i++ ) { _mm_lfence(); _mm_clflush( &array[ i ] ); _mm_lfence(); } _mm_lfence(); t1 = __rdtsc(); _mm_lfence(); int tmp = array[ 0 ]; _mm_lfence(); t2 = __rdtsc(); _mm_lfence(); diff1 = t2 - t1; printf( "tmp is %d\ndiff1 is %lu\n", tmp, diff1 ); _mm_lfence(); t1 = __rdtsc(); tmp = array[ 20 ]; _mm_lfence(); t2 = __rdtsc(); _mm_lfence(); diff2 = t2 - t1; printf( "tmp is %d\ndiff2 is %lu\n", tmp, diff2 ); _mm_lfence(); t1 = __rdtsc(); _mm_lfence(); _mm_lfence(); t2 = __rdtsc(); _mm_lfence(); ov = t2 - t1; printf( "lfence overhead is %lu\n", ov ); printf( "TSC1 is %lu\n", diff1-ov ); printf( "TSC2 is %lu\n", diff2-ov ); 

La sortie est

 $ ./flush1 tmp is 0 diff1 is 371 tmp is 20 diff2 is 280 lfence overhead is 147 TSC1 is 224 TSC2 is 133 $ ./flush1 tmp is 0 diff1 is 399 tmp is 20 diff2 is 280 lfence overhead is 154 TSC1 is 245 TSC2 is 126 $ ./flush1 tmp is 0 diff1 is 392 tmp is 20 diff2 is 840 lfence overhead is 147 TSC1 is 245 TSC2 is 693 $ ./flush1 tmp is 0 diff1 is 364 tmp is 20 diff2 is 140 lfence overhead is 154 TSC1 is 210 TSC2 is 18446744073709551602 

La déclaration selon laquelle “HW prefetcher apporte d’autres blocs” est alors correcte à environ 80%. Qu’est-ce qui se passe alors? Une déclaration plus précise?

    Vous avez cassé le code de Hadi en supprimant la lecture de tmp à la fin, donc il est optimisé par gcc. Il n’y a pas de charge dans votre région chronométrée. Les instructions C ne sont pas des instructions asm.

    Regardez l’asm généré par le compilateur , par exemple sur l’explorateur du compilateur Godbolt . Vous devriez toujours procéder de la sorte lorsque vous essayez de faire de la micro-parsing de tels niveaux très bas, en particulier si les résultats de votre minutage sont inattendus.

      lfence clflush [rcx] lfence lfence rdtsc # start of first timed region lfence # nothing because tmp=array[0] optimized away. lfence mov rcx, rax sal rdx, 32 or rcx, rdx rdtsc # end of first timed region mov edi, OFFSET FLAT:.LC2 lfence sal rdx, 32 or rax, rdx sub rax, rcx mov rsi, rax mov rbx, rax xor eax, eax call printf 

    Vous obtenez un avertissement du compilateur à propos d’une variable inutilisée de -Wall , mais vous pouvez l’exclure de manière à l’optimiser encore. Par exemple, votre tmp++ ne met pas tmp disposition de tout ce qui est en dehors de la fonction, il optimise donc toujours. La désactivation de l’avertissement n’est pas suffisante: imprimez la valeur, renvoyez-la ou affectez-la à une variable volatile dehors de la zone temporisée. (Ou utilisez inline asm volatile pour demander au compilateur de le placer dans un registre à un moment donné. La CppCon2015 de Chandler Carruth concernant l’utilisation de perf décrit quelques astuces: https://www.youtube.com/watch?v=nXaxk27zwlk )


    Dans GNU C (au moins avec gcc et clang -O3 ), vous pouvez forcer une lecture en lançant vers (volatile int*) , comme ceci:

     // int tmp = array[0]; // replace this (void) *(volatile int*)array; // with this 

    Le (void) consiste à éviter un avertissement pour évaluer une expression dans un contexte vide, comme écrire x; .

    Cela ressemble à un aliasing ssortingct, mais ma compréhension est que gcc définit ce comportement. Le kernel Linux utilise un pointeur pour append un qualificatif volatile dans sa macro ACCESS_ONCE . Il est donc utilisé dans l’une des bases de code que gcc tient absolument à prendre en charge. Vous pouvez toujours rendre le tableau entier volatile ; peu importe si son initialisation ne peut pas être vectorisée automatiquement.

    Quoi qu’il en soit, cela comstack pour

      # gcc8.2 -O3 lfence rdtsc lfence mov rcx, rax sal rdx, 32 mov eax, DWORD PTR [rsp] # the load which wasn't there before. lfence or rcx, rdx rdtsc mov edi, OFFSET FLAT:.LC2 lfence 

    Ensuite, vous n’avez pas à vous soucier de l’utilisation de tmp , ni à vous soucier de l’élimination des mémoires mortes, du CST ou de la propagation constante. En pratique, la _mm_mfence() ou quelque chose d’autre dans la réponse originale de Hadi incluait une barrière de mémoire suffisante pour que gcc rétablisse la charge pour le cas cache-miss + cache-hit, mais aurait facilement pu optimiser l’un des rechargements.


    Notez que cela peut avoir pour résultat que asm se charge dans un registre mais ne le lit jamais. Les processeurs actuels attendent toujours le résultat (surtout s’il y a une lfence ), mais le remplacer pourrait laisser un processeur hypothétique ignorer la charge et ne pas l’attendre. (C’est au compilateur de décider s’il doit faire autre chose avec le registre avant la prochaine lfence , comme une partie du résultat rdtsc .)

    Ceci est difficile / peu probable pour le matériel, car le processeur doit être prêt pour les exceptions, voir la discussion dans les commentaires ici .) RDRAND fonctionnerait de cette manière ( quelle est la latence et le débit de l’instruction RDRAND sur Ivy Bridge? ), mais c’est probablement un cas particulier.

    J’ai moi-même testé cela sur Skylake en ajoutant un xor eax,eax à la sortie asm du compilateur, juste après le mov eax, DWORD PTR [rsp] , pour supprimer le résultat de la charge cache-miss. Cela n’a pas affecté le timing.

    Néanmoins, il s’agit là d’un potentiel d’atteinte avec le rejet des résultats d’une charge volatile ; les futurs processeurs pourraient se comporter différemment. Il serait peut-être préférable de faire la sum des résultats de charge (en dehors de la région chronométrée) et de les affecter à la fin à un volatile int sink , au cas où les futures CPU commenceraient à rejeter les Uops qui produiraient des résultats non lus. Mais utilisez toujours volatile pour les charges afin de vous assurer qu’elles se produisent où vous le souhaitez.


    N’oubliez pas non plus de faire une sorte de boucle de préchauffage pour que le processeur atteigne sa vitesse maximale , à moins que vous ne souhaitiez mesurer le temps d’exécution cache-miss au ralenti. Il semble que votre région temporisée vide prenne beaucoup de cycles de référence, votre processeur a donc probablement été très lent.


    Alors, comment exactement les attaques en cache, par exemple la fusion et le spectre, surmontent-elles un tel problème? En gros, ils doivent désactiver het prefetcher, car ils essaient de mesurer les adresses adjacentes afin de déterminer si elles sont manquantes ou non.

    Le canal latéral à lecture en cache dans le cadre d’une attaque Meltdown ou Spectre utilise généralement une foulée suffisamment grande pour que le prélecture HW ne puisse pas détecter le modèle d’access. par exemple sur des pages séparées au lieu de lignes contiguës. https://medium.com/@mattklein123/meltdown-spectre-explained-6bc8634cc0c2 , qui utilise une foulée de 4096, a été l’un des premiers résultats de Google sur le meltdown cache read prefetch ssortingde à la merci des “gadgets”, vous pouvez trouver dans le processus cible.