c++ - Diferencia entre rdtscp, rdtsc: memoria y cpuid/rdtsc?
performance assembly (2)
Como se menciona en un comentario, hay una diferencia entre una barrera de compilación y una barrera de procesador . volatile
y la memory
en la sentencia asm actúan como una barrera de compilación, pero el procesador aún puede reordenar las instrucciones.
La barrera del procesador son instrucciones especiales que deben darse explícitamente, por ejemplo rdtscp, cpuid
, rdtscp, cpuid
, instrucciones de valla de memoria ( mfence, lfence,
...) etc.
Por otro lado, al usar cpuid
como barrera antes de que rdtsc
sea común, también puede ser muy malo desde una perspectiva de rendimiento, ya que las plataformas de máquina virtual a menudo atrapan y emulan la instrucción cpuid
para imponer un conjunto común de características de CPU en varias máquinas en un clúster (para garantizar que la migración en vivo funcione). Por lo tanto, es mejor usar una de las instrucciones de la valla de memoria.
El kernel de Linux usa mfence;rdtsc
en plataformas AMD y lfence;rdtsc
en Intel. Si no quiere molestarse en distinguir entre estos, mfence;rdtsc
funciona en ambos, aunque es un poco más lento ya que la mfence
es una barrera más fuerte que la lfence
.
Supongamos que estamos tratando de usar el tsc para la supervisión del rendimiento y queremos evitar el reordenamiento de la instrucción.
Estas son nuestras opciones:
1: rdtscp
es una llamada de serialización. Evita el reordenamiento de la llamada a 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
Sin embargo, rdtscp
solo está disponible en las CPU más nuevas. Entonces, en este caso, tenemos que usar rdtsc
. Pero rdtsc
no se serializa, por lo que usarlo solo no evitará que la CPU lo reordena.
Entonces podemos usar cualquiera de estas dos opciones para evitar el reordenamiento:
2: Esta es una llamada a cpuid
y luego a rdtsc
. cpuid
es una llamada de serialización.
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
3: Esta es una llamada a rdtsc
con memory
en la lista de clobber, que evita la reordenación
__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
Mi comprensión para la tercera opción es la siguiente:
Hacer la llamada __volatile__
evita que el optimizador elimine el asm o lo mueva a través de cualquier instrucción que pueda necesitar los resultados (o cambiar las entradas) del asm. Sin embargo, aún podría moverlo con respecto a operaciones no relacionadas. Entonces __volatile__
no es suficiente.
Dile a la memoria del compilador que está siendo destruida : "memory")
. El toque de "memory"
significa que GCC no puede hacer suposiciones acerca de que el contenido de la memoria permanezca igual a través del asm, y por lo tanto no se reordenará a su alrededor.
Entonces mis preguntas son:
- 1: ¿Es
__volatile__
mi entendimiento de__volatile__
y"memory"
? - 2: ¿las dos segundas llamadas hacen lo mismo?
- 3: Usar
"memory"
parece mucho más simple que usar otra instrucción de serialización. ¿Por qué alguien usaría la tercera opción sobre la segunda opción?
puedes usarlo como se muestra a continuación:
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");
En el código anterior, la primera llamada a CPUID implementa una barrera para evitar la ejecución fuera de orden de las instrucciones anteriores y posteriores a la instrucción RDTSC. Con este método evitamos llamar a una instrucción CPUID entre las lecturas de los registros en tiempo real
El primer RDTSC luego lee el registro de marca de tiempo y el valor se almacena en la memoria. Entonces se ejecuta el código que queremos medir. La instrucción RDTSCP lee el registro de marca de tiempo por segunda vez y garantiza que se complete la ejecución de todo el código que queríamos medir. Las dos instrucciones "mov" que vienen después almacenan los valores de edx y eax en la memoria. Finalmente, una llamada a CPUID garantiza que se implemente una barrera de nuevo, de modo que es imposible que cualquier instrucción posterior se ejecute antes de la CPUID.