2012-09-28 21 views
50

Supongamos que estamos intentando utilizar el tsc para la supervisión del rendimiento y queremos evitar el reordenamiento de las instrucciones.Diferencia entre rdtscp, rdtsc: memoria y cpuid/rdtsc?

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 sólo está disponible en los nuevos CPUs. Entonces, en este caso, tenemos que usar rdtsc. Pero rdtsc no se serializa, por lo que su uso por sí solo no evitará que la CPU vuelva a ordenarlo.

así que podemos usar cualquiera de estas dos opciones para evitar la reordenación:

2: Este es un llamado a cpuid y luego 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: Este es un llamado a rdtsc con memory en la lista clobber, lo 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 entendimiento para la tercera opción es la siguiente:

Hacer el call __volatile__ impide que el optimizador elimine el asm o lo mueva a través de las instrucciones que puedan 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.

Informe a todo el compilador de memoria está siendo clobbered: : "memory"). El "memory" clobber significa que GCC no puede hacer ninguna suposición acerca de que el contenido de la memoria permanezca igual en toda la memoria y, por lo tanto, no se reordenará a su alrededor.

Así que mis preguntas son:

  • 1: ¿Es mi comprensión de __volatile__ y "memory" correcta?
  • 2: ¿Las dos segundas llamadas hacen lo mismo?
  • 3: El uso de "memory" parece mucho más simple que utilizar otra instrucción de serialización. ¿Por qué alguien usaría la tercera opción sobre la segunda opción?
+9

Parece que confunde el reordenamiento de las instrucciones generadas por el compilador, lo cual puede evitar usando 'volátil' y' memoria' y reordenando las instrucciones ejecutadas por el procesador (aka _out of order execution_), que puede evitar al usar ' cpuid'. – hirschhornsalz

+0

@hirschhornsalz pero no tener 'memoria' en la lista de clobber impide que el procesador vuelva a ordenar las instrucciones? ¿La 'memoria' no actúa como una valla de memoria? –

+0

o tal vez la 'memoria' en la lista de clobber solo se emite a gcc, y el código de máquina resultante no expone esto al procesador? –

Respuesta

35

Como se mencionó en un comentario, hay una diferencia entre una barrera compilador y una barrera procesador. volatile y memory en la instrucción asm actúan como una barrera de compilación, pero el procesador aún puede reordenar las instrucciones.

Barrera del procesador son instrucciones especiales que se deben proporcionar explícitamente, p. rdtscp, cpuid, instrucciones de valla de memoria (mfence, lfence, ...) etc.

Como un aparte, mientras que usando cpuid como barrera antes de rdtsc es común, sino que también puede ser muy malo desde una perspectiva de rendimiento, ya que las plataformas de la máquina virtual a menudo trampa y emular la instrucción cpuid con el fin de imponer un conjunto común de CPU características 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 desea molestarse en distinguir entre estos, mfence;rdtsc funciona en ambos aunque es un poco más lento ya que mfence es una barrera más fuerte que lfence.

+5

El 'cpuid; rdtsc' no se trata de vallas de memoria, se trata de serializar la secuencia de instrucciones. Por lo general, se utiliza con fines de evaluación comparativa para garantizar que no queden instrucciones "antiguas" en la estación de reserva/reserva de reordenación. El tiempo de ejecución de 'cpuid' (que es bastante largo, recuerdo> 200 ciclos) se debe restar. Si el resultado es más "exacto" de esta manera no es del todo claro para mí, experimenté con y sin y las diferencias parecen ser menos el error natural de medición, incluso en el modo de usuario único sin nada más que correr en absoluto. – hirschhornsalz

+0

No estoy seguro, pero posiblemente las instrucciones de la valla utilizadas de esta manera en el kernel no son útiles ^^ – hirschhornsalz

+4

@hirschhornsalz: Según los registros de confirmación de git, AMD e Intel confirmaron que m/lfence serializará rdtsc en la actualidad CPU disponibles Supongo que Andi Kleen puede proporcionar más detalles sobre lo que se dijo exactamente, si está interesado y preguntarle. – janneb

5

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 CPUID implementa una barrera para evitar la ejecución fuera de orden de las instrucciones anteriores y por debajo de 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 lee el registro de la 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.

+12

Hola, parece que copió esta respuesta del documento técnico de Gabriele Paolinis "Cómo comparar los tiempos de ejecución del código en las arquitecturas de conjuntos de instrucciones Intel® IA-32 y IA-64" (aunque se perdió un salto de línea). Estás usando el trabajo de otra persona sin darle crédito al autor. ¿Por qué no agregar una atribución? –

+0

Sí, de hecho, está cubierto. También me pregunto si los dos movimientos en la lectura de la hora de inicio son necesarios: http://stackoverflow.com/questions/38994549/is-intels-timestamp-reading-asm-code-example-using-two-more-registers -than-are –

+0

¿Hay alguna razón específica para tener dos variables altas y bajas? – ExOfDe