¿Mediciones negativas del ciclo del reloj con rdtsc adosado?

Estoy escribiendo un código C para medir el número de ciclos de reloj necesarios para adquirir un semáforo. Estoy usando rdtsc, y antes de hacer la medición en el semáforo, llamo a rdtsc dos veces consecutivas, para medir la sobrecarga. Repito esto muchas veces, en un bucle for, y luego uso el valor promedio como overhead de rdtsc.

¿Es correcto, usar el valor promedio, antes que nada?

No obstante, el gran problema aquí es que a veces obtengo valores negativos para la sobrecarga (no necesariamente el promedio, pero al menos los parciales dentro del ciclo for).

Esto también afecta el cálculo consecutivo de la cantidad de ciclos de CPU necesarios para la operación sem_wait() , que a veces también resulta negativa. Si lo que escribí no está claro, aquí hay una parte del código en el que estoy trabajando.

¿Por qué estoy obteniendo esos valores negativos?


(Nota del editor: consulte Obtener recuento de ciclos de la CPU para obtener una marca correcta y portátil de la marca de tiempo completa de 64 bits. Una restricción de asm "=A" solo obtendrá los 32 bits bajos o altos cuando se compile para x86-64, dependiendo de si la asignación de registro pasa a seleccionar RAX o RDX para la salida uint64_t . No seleccionará edx:eax ).

(Segunda nota del editor: ¡Vaya, esa es la respuesta a por qué estamos obteniendo resultados negativos. Todavía vale la pena dejar una nota aquí como una advertencia para no copiar esta implementación de rdtsc ).


 #include  #include  #include  #include  #include  static inline uint64_t get_cycles() { uint64_t t; // editor's note: "=A" is unsafe for this in x86-64 __asm volatile ("rdtsc" : "=A"(t)); return t; } int num_measures = 10; int main () { int i, value, res1, res2; uint64_t c1, c2; int tsccost, tot, a; tot=0; for(i=0; i<num_measures; i++) { c1 = get_cycles(); c2 = get_cycles(); tsccost=(int)(c2-c1); if(tsccost<0) { printf("#### ERROR!!! "); printf("rdtsc took %d clock cycles\n", tsccost); return 1; } tot = tot+tsccost; } tsccost=tot/num_measures; printf("rdtsc takes on average: %d clock cycles\n", tsccost); return EXIT_SUCCESS; } 

Cuando Intel inventó por primera vez el TSC midió los ciclos de CPU. Debido a varias funciones de administración de energía, “ciclos por segundo” no es constante; por lo que el TSC fue originalmente bueno para medir el rendimiento del código (y malo para medir el tiempo pasado).

Para bien o para mal; En aquel entonces, las CPU realmente no tenían demasiada administración de energía, a menudo las CPU corrían a “ciclos por segundo” fijos de todos modos. Algunos progtwigdores obtuvieron la idea equivocada y usaron mal el TSC para medir el tiempo y no los ciclos. Más tarde (cuando el uso de las funciones de administración de energía se volvió más común), estas personas hicieron un uso indebido del CET para medir el tiempo que gimió acerca de todos los problemas que causaba su uso indebido. Los fabricantes de CPU (comenzando con AMD) cambiaron el TSC, por lo que mide el tiempo y no los ciclos (lo que hace que se corte para medir el rendimiento del código, pero es correcto para medir el tiempo pasado). Esto causó confusión (fue difícil para el software determinar lo que el TSC realmente midió), así que un poco más tarde AMD agregó el indicador “TSC Invariant” a CPUID, de modo que si este indicador está configurado, los progtwigdores saben que el TSC está roto (para medir ciclos) o fijo (para medir el tiempo).

Intel siguió a AMD y cambió el comportamiento de su TSC para medir también el tiempo, y también adoptó la bandera “TSC Invariant” de AMD.

Esto da 4 casos diferentes:

  • TSC mide el tiempo y el rendimiento (los ciclos por segundo son constantes)

  • TSC mide el rendimiento, no el tiempo

  • TSC mide el tiempo y no el rendimiento, pero no utiliza la bandera “TSC invariante” para decirlo

  • TSC mide el tiempo y no el rendimiento y utiliza la bandera “TSC invariante” para decirlo (CPU más modernas)

Para los casos en que TSC mide el tiempo, para medir el rendimiento / ciclos de forma adecuada, debe utilizar contadores de control de rendimiento. Lamentablemente, los contadores de supervisión de rendimiento son diferentes para diferentes CPU (modelo específico) y requieren acceso a MSR (código privilegiado). Esto hace que sea considerablemente poco práctico para las aplicaciones medir “ciclos”.

También tenga en cuenta que si el TSC mide el tiempo, no puede saber qué escala de tiempo devuelve (cuántos nanosegundos en un “ciclo pretendido”) sin utilizar alguna otra fuente de tiempo para determinar un factor de escala.

El segundo problema es que para sistemas multi-CPU la mayoría de los sistemas operativos son malos. La forma correcta de que un SO maneje el TSC es evitar que las aplicaciones lo utilicen directamente (configurando el indicador TSD en CR4, de modo que la instrucción RDTSC cause una excepción). Esto evita varias vulnerabilidades de seguridad (temporización de canales laterales). También permite que el sistema operativo emule el TSC y se asegure de que arroje un resultado correcto. Por ejemplo, cuando una aplicación utiliza la instrucción RDTSC y causa una excepción, el manejador de excepciones del sistema operativo puede encontrar una “marca de tiempo global” correcta para devolver.

Por supuesto, diferentes CPU tienen su propio TSC. Esto significa que si una aplicación usa TSC directamente, obtienen diferentes valores en diferentes CPU. Para ayudar a las personas a solucionar el problema del sistema operativo (al emular RDTSC como deberían); AMD agregó la instrucción RDTSCP , que devuelve el TSC y una “ID del procesador” (Intel también adoptó la instrucción RDTSCP ). Una aplicación que se ejecuta en un sistema operativo quebrado puede usar la “ID del procesador” para detectar cuándo se están ejecutando en una CPU diferente de la última vez; y de esta forma (usando la instrucción RDTSCP ) pueden saber cuándo “transcurrido = TSC – previous_TSC” da un resultado válido. Sin embargo; el “ID del procesador” devuelto por esta instrucción es solo un valor en un MSR, y el sistema operativo tiene que establecer este valor en cada CPU a algo diferente; de ​​lo contrario, el RDTSCP dirá que el “ID del procesador” es cero en todas las CPU.

Básicamente; si las CPU admiten la instrucción RDTSCP , y si el sistema operativo ha configurado correctamente la “ID del procesador” (usando el MSR); entonces la instrucción RDTSCP puede ayudar a las aplicaciones a saber cuándo tienen un mal resultado de “tiempo transcurrido” (pero no proporciona de todos modos la reparación o evitar el mal resultado).

Asi que; para abreviar, si quiere una medición de rendimiento precisa, está casi totalmente jodido. Lo mejor que puedes esperar de manera realista es una medición de tiempo precisa; pero solo en algunos casos (por ejemplo, cuando se ejecuta en una máquina con una sola CPU o se “ancla” a una CPU específica, o cuando se usa RDTSCP en sistemas operativos que lo configuran correctamente siempre que detecte y descarte valores no válidos).

Por supuesto, incluso entonces obtendrá mediciones dudosas debido a cosas como IRQ. Por esta razón; es mejor ejecutar su código muchas veces en un ciclo y descartar cualquier resultado que sea demasiado más alto que otros resultados.

Finalmente, si realmente quieres hacerlo correctamente, debes medir la sobrecarga de la medición. Para hacer esto medirías cuánto tiempo lleva hacer nada (solo la instrucción RDTSC / RDTSCP sola, mientras desechas las mediciones dudosas); luego reste la sobrecarga de la medición de los resultados de “medición de algo”. Esto le da una mejor estimación del tiempo que realmente lleva “algo”.

Nota: Si puede desenterrar una copia de la Guía de progtwigción del sistema de Intel desde que se lanzó Pentium (mediados de la década de 1990, no estoy seguro si ya está disponible en línea), he copiado copias desde la década de 1980 y encontrará que Intel documentó la marca de tiempo contador como algo que “se puede usar para controlar e identificar el tiempo relativo de ocurrencia de eventos del procesador”. Garantizaron que (excluyendo el envolvimiento de 64 bits) boostía monótonamente (pero no que boostía a una tasa fija) y que tomaría un mínimo de 10 años antes de que se arreglara. La última revisión del manual documenta el contador de marca de tiempo con más detalles, indicando que para CPU antiguas (P6, Pentium M, Pentium 4 anterior) el contador de marca de tiempo “aumenta con cada ciclo de reloj interno del procesador” y que “Intel (r) Las transiciones de tecnología SpeedStep (r) pueden afectar el reloj del procesador “; y que las CPU más nuevas (Pentium 4, Core Solo, Core Duo, Core 2, Atom), el TSC se incrementa a una velocidad constante (y que este es el “comportamiento arquitectónico que avanza”). Esencialmente, desde el principio era un “contador de ciclo interno” (variable) para ser utilizado para un sello de tiempo (y no un contador de tiempo para ser usado para rastrear el tiempo de “reloj de pared”), y este comportamiento cambió poco después del año 2000 (basado en la fecha de lanzamiento de Pentium 4).

  1. no use valor promedio

    Utilice el más pequeño o el promedio de valores más pequeños en su lugar (para obtener promedio debido a CACHE) porque los más grandes han sido interrumpidos por la multitarea del sistema operativo.

    También podría recordar todos los valores y luego encontrar el límite de granularidad del proceso del sistema operativo y filtrar todos los valores después de este límite (generalmente> 1 1ms que es fácilmente detectable)

    enter image description here

  2. no es necesario medir los gastos generales de RDTSC

    Usted acaba de medir offseted por un tiempo y el mismo desplazamiento está presente en ambos tiempos y después de la sustracción se ha ido.

  3. para fuente de reloj variable de RDTS (como en las computadoras portátiles)

    Debería cambiar la velocidad de la CPU a su valor máximo mediante un ciclo de cálculo constante e intenso, por lo general bastan unos segundos. Debe medir continuamente la frecuencia de la CPU y comenzar a medir lo que está haciendo solo cuando sea lo suficientemente estable.

Si el código comienza en un procesador y cambia a otro, la diferencia de marca de tiempo puede ser negativa debido a que los procesadores están durmiendo, etc.

Intente establecer la afinidad del procesador antes de comenzar a medir.

No puedo ver si se está ejecutando bajo Windows o Linux a partir de la pregunta, entonces responderé por ambos.

Windows:

 DWORD affinityMask = 0x00000001L; SetProcessAffinityMask(GetCurrentProcessId(), affinityMask); 

Linux:

 cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); sched_setaffinity (getpid(), sizeof(cpuset), &cpuset) 

El punto principal de mi pregunta no era la precisión del resultado, sino el hecho de que obtengo valores negativos de vez en cuando (la primera llamada a rdstc da más valor que la segunda llamada). Investigando más (y leyendo otras preguntas en este sitio web), descubrí que una forma de hacer que las cosas funcionen al usar rdtsc es poner un comando cpuid justo antes. Este comando serializa el código. Así es como estoy haciendo las cosas ahora:

 static inline uint64_t get_cycles() { uint64_t t; volatile int dont_remove __attribute__((unused)); unsigned tmp; __asm volatile ("cpuid" : "=a"(tmp), "=b"(tmp), "=c"(tmp), "=d"(tmp) : "a" (0)); dont_remove = tmp; __asm volatile ("rdtsc" : "=A"(t)); return t; } 

Todavía recibo una diferencia NEGATIVA entre la segunda llamada y la primera llamada de la función get_cycles. ¿POR QUÉ? No estoy 100% seguro de la syntax del código en línea del ensamblaje cpuid, esto es lo que encontré buscando en Internet.

Las otras respuestas son geniales (ve a leerlas), pero asume que rdtsc se lee correctamente. Esta respuesta está abordando el error en el asma en línea que conduce a resultados totalmente falsos, incluido el negativo.

La otra posibilidad es que esté comstackndo esto como un código de 32 bits, pero con muchas más repeticiones, y tiene un intervalo negativo ocasional en la migración de CPU en un sistema que no tiene TSC invariable (TSC sincronizados en todos los núcleos). Ya sea un sistema multi-socket, o un multi-core antiguo. La operación de búsqueda TSC de la CPU especialmente en el entorno multinúcleo-procesador múltiple .


Si estaba comstackndo para x86-64, sus resultados negativos se explican completamente por su restricción incorrecta de salida "=A" para asm . Consulte Obtener recuento de ciclos de la CPU? para conocer las formas correctas de usar rdtsc que son portátiles para todos los comstackdores y el modo de 32 contra 64 bits. O use salidas "=a" y "=d" y simplemente ignore la salida de la mitad alta, para intervalos cortos que no desborden 32 bits).

(Me sorprende que no hayas mencionado que también son enormes y variopintos, así como desbordan tot para dar un promedio negativo incluso si ninguna medición individual fue negativa. Estoy viendo promedios como -63421899 o 69374170 , o 115365476 )

Comstackrlo con gcc -O3 -m32 hace que funcione como se espera, imprimiendo promedios de 24 a 26 (si se ejecuta en un bucle para que la CPU se mantenga a la velocidad máxima, de lo contrario 125 ciclos de referencia para los 24 ciclos de reloj de núcleo entre back-to- volver rdtsc en Skylake). https://agner.org/optimize/ para tablas de instrucciones.


Detalles de Asm de lo que salió mal con la restricción "=A"

rdtsc (entrada manual insn ref) siempre produce los dos bits de hi:lo definición de 32 bits de su resultado de 64 bits en edx:eax , incluso en el modo de 64 bits donde realmente preferimos tenerlo en un solo registro de 64 bits .

Esperaba que la restricción de salida "=A" seleccionara edx:eax para uint64_t t . Pero eso no es lo que sucede. Para una variable que se ajusta en un registro, el comstackdor selecciona RAX o RDX y supone que el otro no está modificado , al igual que una restricción "=r" selecciona un registro y supone que el rest no está modificado. O una restricción "=Q" elige uno de a, b, c o d. (Ver restricciones x86 ).

En x86-64, normalmente solo querría "=A" para un operando unsigned __int128 , como un resultado múltiple o entrada div . Es una especie de truco porque el uso de %0 en la plantilla de asm solo se expande hasta el registro bajo, y no hay advertencia cuando "=A" no usa los registros d .

Para ver exactamente cómo esto causa un problema, agregué un comentario dentro de la plantilla de asm:
__asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t)); . Entonces podemos ver lo que el comstackdor espera, basado en lo que le contamos con los operandos.

El bucle resultante (en la syntax de Intel) se ve así, desde la comstackción de una versión limpiada de su código en el explorador del comstackdor Godbolt para gcc de 64 bits y clang de 32 bits:

 # the main loop from gcc -O3 targeting x86-64, my comments added .L6: rdtsc # compiler picked rax # c1 = rax rdtsc # compiler picked rdx # c2 = rdx, not realizing that rdtsc clobbers rax(c1) # compiler thinks RAX=c1, RDX=c2 # actual situation: RAX=low half of c2, RDX=high half of c2 sub edx, eax # tsccost = edx-eax js .L3 # jump if the sign-bit is set in tsccost ... rest of loop back to .L6 

Cuando el comstackdor está calculando c2-c1 , en realidad está calculando hi-lo desde el segundo rdtsc , porque mentimos al comstackdor sobre lo que hace el enunciado asm. El segundo rdtsc c1

Le dijimos que tenía una opción de qué registro para obtener la salida, por lo que eligió un registro la primera vez, y el otro la segunda vez, por lo que no necesitaría ninguna instrucción mov .

El TSC cuenta los ciclos de referencia desde el último reinicio. Pero el código no depende de hi , solo depende del signo de hi-lo . Como lo envuelve cada segundo o dos (2 ^ 32 Hz está cerca de 4.3GHz), ejecutar el progtwig en cualquier momento dado tiene aproximadamente un 50% de posibilidades de ver un resultado negativo.

No depende del valor actual de hi ; hay quizás una parte en 2^32 sesgo en una dirección u otra porque hi cambia en uno cuando lo envuelve.

Como el hi-lo es un entero de 32 bits distribuido de manera casi uniforme, el desbordamiento del promedio es muy común. Tu código está bien si el promedio es normalmente pequeño. (Pero vea otras respuestas de por qué no quiere la media; desea una mediana o algo para excluir valores atípicos).

En vista de la aceleración térmica e inactiva, el movimiento del mouse y las interrupciones del tráfico de red, lo que sea que esté haciendo con la GPU y todos los demás gastos generales que un moderno sistema multinúcleo puede absorber sin preocuparse demasiado, creo que su único curso razonable para esto es para acumular unos pocos miles de muestras individuales y simplemente arrojar los valores atípicos antes de tomar la mediana o la media (no es un estadístico, pero me atrevería a decir que no habrá mucha diferencia aquí).

Creo que cualquier cosa que haga para eliminar el ruido de un sistema en ejecución sesgará los resultados mucho peor que simplemente aceptar que no hay manera de que pueda predecir de manera confiable cuánto tardará en completarse en estos días.

rdtsc se puede utilizar para obtener un tiempo transcurrido confiable y muy preciso. Si utiliza Linux, puede ver si su procesador admite una velocidad constante tsc buscando en / proc / cpuinfo para ver si tiene constant_tsc definido.

Asegúrate de mantenerte en el mismo núcleo. Cada núcleo tiene su propio tsc que tiene su propio valor. Para usar rdtsc, asegúrese de que sea un conjunto de tareas , o SetThreadAffinityMask (windows) o pthread_setaffinity_np para asegurarse de que su proceso permanezca en el mismo núcleo.

A continuación, divida esto por su frecuencia de reloj principal, que en Linux se puede encontrar en / proc / cpuinfo o puede hacerlo en tiempo de ejecución mediante

rdtsc
clock_gettime
dormir por 1 segundo
clock_gettime
rdtsc

a continuación, vea cuántas marcas por segundo, y luego puede dividir cualquier diferencia en ticks para averiguar cuánto tiempo ha transcurrido.

Si el hilo que está ejecutando su código se mueve entre núcleos, entonces es posible que el valor de rdtsc devuelto sea menor que el valor leído en otro núcleo. El núcleo no establece el contador en 0 exactamente al mismo tiempo cuando el paquete se enciende. Por lo tanto, asegúrese de establecer la afinidad de subprocesos en un núcleo específico cuando ejecuta su prueba.

Probé tu código en mi máquina y pensé que durante la reducción RDTSC solo uint32_t es razonable.

Hago lo siguiente en mi código para corregirlo:

 if(before_t