c++ c performance assembly rdtsc

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.