web-dev-qa-db-fra.com

Différence entre rdtscp, rdtsc: mémoire et cpuid / rdtsc?

Supposons que nous essayons d'utiliser le tsc pour la surveillance des performances et que nous voulons empêcher la réorganisation des instructions.

Ce sont nos options:

1:rdtscp est un appel de sérialisation. Il empêche la réorganisation autour de l'appel à rdtscp.

__asm__ __volatile__("rdtscp; "         // serializing read of tsc
                     "shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
                     "or %%rdx,%%rax"   // and or onto rax
                     : "=a"(tsc)        // output to tsc variable
                     :
                     : "%rcx", "%rdx"); // rcx and rdx are clobbered

Cependant, rdtscp n'est disponible que sur les nouveaux processeurs. Dans ce cas, nous devons donc utiliser rdtsc. Mais rdtsc n'est pas une sérialisation, donc l'utiliser seul n'empêchera pas le CPU de le réorganiser.

Nous pouvons donc utiliser l'une de ces deux options pour empêcher la réorganisation:

2: Ceci est un appel à cpuid puis rdtsc. cpuid est un appel de sérialisation.

volatile int dont_remove __attribute__((unused)); // volatile to stop optimizing
unsigned tmp;
__cpuid(0, tmp, tmp, tmp, tmp);                   // cpuid is a serialising call
dont_remove = tmp;                                // prevent optimizing out cpuid

__asm__ __volatile__("rdtsc; "          // read of tsc
                     "shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
                     "or %%rdx,%%rax"   // and or onto rax
                     : "=a"(tsc)        // output to tsc
                     :
                     : "%rcx", "%rdx"); // rcx and rdx are clobbered

: Ceci est un appel à rdtsc avec memory dans la liste de clobber, ce qui empêche la réorganisation

__asm__ __volatile__("rdtsc; "          // read of tsc
                     "shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
                     "or %%rdx,%%rax"   // and or onto rax
                     : "=a"(tsc)        // output to tsc
                     :
                     : "%rcx", "%rdx", "memory"); // rcx and rdx are clobbered
                                                  // memory to prevent reordering

Ma compréhension de la 3e option est la suivante:

Passer l'appel __volatile__ empêche l'optimiseur de supprimer l'asm ou de le déplacer sur les instructions qui pourraient avoir besoin des résultats (ou modifier les entrées) de l'asm. Cependant, il pourrait toujours le déplacer en ce qui concerne les opérations non liées. Donc __volatile__ n'est pas assez.

Dire que la mémoire du compilateur est assaillie: : "memory"). Le "memory" clobber signifie que GCC ne peut faire aucune supposition sur le fait que le contenu de la mémoire reste le même à travers l'asm, et ne sera donc pas réorganisé autour de lui.

Mes questions sont donc:

  • 1: Ma compréhension de __volatile__ et "memory" correct?
  • 2: Les deux seconds appels font-ils la même chose?
  • 3: Utilisation de "memory" semble beaucoup plus simple que d'utiliser une autre instruction de sérialisation. Pourquoi utiliser la 3e option plutôt que la 2e option?
59
Steve Lorimer

Comme mentionné dans un commentaire, il y a une différence entre barrière du compilateur et barrière du processeur. volatile et memory dans l'instruction asm agissent comme une barrière de compilation, mais le processeur est toujours libre de réorganiser les instructions.

La barrière du processeur sont des instructions spéciales qui doivent être explicitement données, par exemple rdtscp, cpuid, instructions de barrière de mémoire (mfence, lfence, ...) etc.

En passant, tout en utilisant cpuid comme barrière avant que rdtsc soit courant, cela peut également être très mauvais du point de vue des performances, car les plates-formes de machines virtuelles piègent et émulent souvent le cpuid instruction afin d'imposer un ensemble commun de fonctionnalités CPU sur plusieurs machines d'un cluster (pour garantir le fonctionnement de la migration en direct). Il est donc préférable d'utiliser l'une des instructions de clôture de mémoire.

Le noyau Linux utilise mfence;rdtsc sur les plateformes AMD et lfence;rdtsc sur Intel. Si vous ne voulez pas vous soucier de les distinguer, mfence;rdtsc fonctionne sur les deux, bien qu'il soit légèrement plus lent car mfence est une barrière plus forte que lfence.

42
janneb

vous pouvez l'utiliser comme indiqué ci-dessous:

asm volatile (
"CPUID\n\t"/*serialize*/
"RDTSC\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r"
(cycles_low):: "%rax", "%rbx", "%rcx", "%rdx");
/*
Call the function to benchmark
*/
asm volatile (
"RDTSCP\n\t"/*read the clock*/
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
"CPUID\n\t": "=r" (cycles_high1), "=r"
(cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx");

Dans le code ci-dessus, le premier appel CPUID implémente une barrière pour éviter l'exécution dans le désordre des instructions au-dessus et au-dessous de l'instruction RDTSC. Avec cette méthode, nous évitons d'appeler une instruction CPUID entre les lectures des registres en temps réel

Le premier RDTSC lit ensuite le registre d'horodatage et la valeur est stockée en mémoire. Ensuite, le code que nous voulons mesurer est exécuté. L'instruction RDTSCP lit le registre d'horodatage pour la deuxième fois et garantit que l'exécution de tout le code que nous voulions mesurer est terminée. Les deux instructions "mov" qui suivent enregistrent les valeurs des registres edx et eax en mémoire. Enfin, un appel CPUID garantit qu'une barrière est implémentée à nouveau de sorte qu'il est impossible que toute instruction venant ensuite soit exécutée avant CPUID elle-même.

5
Pranjal Verma