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:
__volatile__
et "memory"
correct?"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?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
.
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.