c++ - segundo - Obtener el recuento de ciclos de CPU?
rendimiento del procesador (4)
A partir de GCC 4.5 y __rdtsc()
posteriores, el intrínseco __rdtsc()
ahora es compatible con MSVC y GCC.
Pero la inclusión que se necesita es diferente:
#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif
Aquí está la respuesta original antes de GCC 4.5.
Sacado directamente de uno de mis proyectos:
#include <stdint.h>
// Windows
#ifdef _WIN32
#include <intrin.h>
uint64_t rdtsc(){
return __rdtsc();
}
// Linux/GCC
#else
uint64_t rdtsc(){
unsigned int lo,hi;
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) | lo;
}
#endif
Vi esta publicación en SO que contiene código C para obtener el último recuento de ciclos de CPU:
Perfiles basados en el conteo de ciclos de CPU en C / C ++ Linux x86_64
¿Hay alguna forma de usar este código en C ++ (las soluciones de Windows y Linux son bienvenidas)? Aunque está escrito en C (y C es un subconjunto de C ++) no estoy muy seguro de si este código funcionaría en un proyecto de C ++ y, de no ser así, ¿cómo traducirlo?
Estoy usando x86-64
EDIT2:
Encontré esta función, pero no puedo conseguir que VS2010 reconozca el ensamblador. ¿Necesito incluir algo? (Creo que tengo que cambiar uint64_t
a long long
por windows ...?)
static inline uint64_t get_cycles()
{
uint64_t t;
__asm volatile ("rdtsc" : "=A"(t));
return t;
}
EDIT3:
Desde el código anterior me sale el error:
"error C2400: error de sintaxis del ensamblador en línea en ''código de operación''; se encontró ''tipo de datos''"
¿Alguien podría ayudar por favor?
Para Windows, Visual Studio proporciona un "compilador intrínseco" conveniente (es decir, una función especial, que el compilador entiende) que ejecuta la instrucción RDTSC por usted y le devuelve el resultado:
unsigned __int64 __rdtsc(void);
VC ++ utiliza una sintaxis completamente diferente para el ensamblaje en línea, pero solo en las versiones de 32 bits. El compilador de 64 bits no soporta el ensamblaje en línea en absoluto.
En este caso, probablemente sea igual de bueno: rdtsc
tiene (al menos) dos problemas principales cuando se trata de secuencias de código de tiempo. Primero (como la mayoría de las instrucciones) puede ejecutarse fuera de orden, por lo que si está tratando de rdtsc
una secuencia corta de código, el rdtsc
antes y después de ese código podrían ejecutarse antes, o ambos después, o lo que haya Usted (estoy bastante seguro de que los dos siempre se ejecutarán en orden uno con respecto al otro, de modo que al menos la diferencia nunca será negativa).
Segundo, en un sistema de múltiples núcleos (o multiprocesador), un rdtsc podría ejecutarse en un núcleo / procesador y el otro en un núcleo / procesador diferente. En tal caso, un resultado negativo es completamente posible.
En general, si desea un temporizador preciso en Windows, será mejor que utilice QueryPerformanceCounter
.
Si realmente insiste en usar rdtsc
, creo que tendrá que hacerlo en un módulo separado escrito completamente en lenguaje ensamblador (o usar un compilador intrínseco), luego vinculado con su C o C ++. Nunca he escrito ese código para el modo de 64 bits, pero en el modo de 32 bits se ve algo así:
xor eax, eax
cpuid
xor eax, eax
cpuid
xor eax, eax
cpuid
rdtsc
; save eax, edx
; code you''re going to time goes here
xor eax, eax
cpuid
rdtsc
Sé que esto parece extraño, pero en realidad es correcto. Ejecuta CPUID porque es una instrucción de serialización (no puede ejecutarse fuera de orden) y está disponible en modo de usuario. Lo ejecuta tres veces antes de comenzar a cronometrar porque Intel documenta el hecho de que la primera ejecución puede ejecutarse a una velocidad diferente a la segunda (y lo que recomiendan es tres, de modo que tres lo es).
Luego ejecuta su código bajo prueba, otro cpuid para forzar la serialización y el rdtsc final para obtener el tiempo después de que el código haya terminado.
Junto con eso, desea utilizar cualquier medio que su sistema operativo ofrezca para forzar que todo esto se ejecute en un proceso / núcleo. En la mayoría de los casos, también desea forzar la alineación del código: los cambios en la alineación pueden llevar a diferencias bastante sustanciales en la ejecución.
Finalmente, desea ejecutarlo varias veces, y siempre es posible que se interrumpa en medio de las cosas (por ejemplo, un cambio de tarea), por lo que debe estar preparado para la posibilidad de que una ejecución tome un poco de tiempo. más largo que el resto - por ejemplo, 5 carreras que toman ~ 40-43 ciclos de reloj cada una, y una sexta que toma más de 10000 ciclos de reloj. Claramente, en este último caso, simplemente arrojas lo atípico, no es de tu código.
Resumen: lograr ejecutar la instrucción rdtsc en sí mismo es (casi) la menor de sus preocupaciones. Hay mucho más que debes hacer antes de poder obtener resultados de rdtsc
que realmente significarán cualquier cosa.
No necesitas inline asm para esto . No hay beneficio; los compiladores tienen incorporados para rdtsc
y rdtscp
, y (al menos en estos días) todos definen un intrínseco __rdtsc
si se incluyen los encabezados correctos. Pero, a diferencia de casi todos los demás casos ( https://gcc.gnu.org/wiki/DontUseInlineAsm ), no hay un inconveniente grave para asm, siempre y cuando esté utilizando una implementación buena y segura como @ Mysticial''s , no una con una falla. Restricción "=A"
.
Desafortunadamente, MSVC no está de acuerdo con todos los demás acerca de qué encabezado usar para los intrínsecos que no son SIMD.
La guía de intriniscs de Intel dice que _rdtsc
(con un guión bajo) está en <immintrin.h>
, pero eso no funciona en gcc y clang. Solo definen los intrínsecos de SIMD en <immintrin.h>
, así que nos quedamos con <intrin.h>
(MSVC) vs. <x86intrin.h>
(todo lo demás, incluido el ICC reciente). Para compatibilizar con MSVC y la documentación de Intel, gcc y clang definen las versiones de la función con un guión bajo y un guión bajo.
Dato curioso: la versión con doble guión bajo devuelve un entero de 64 bits sin firmar, mientras que Intel documenta _rdtsc()
como devuelto (firmado) __int64
.
// valid C99 and C++
#include <stdint.h> // <cstdint> is preferred in C++, but stdint.h works.
#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#endif
// optional wrapper if you don''t want to just use __rdtsc() everywhere
inline
uint64_t readTSC() {
// _mm_lfence(); // optionally wait for earlier insns to retire before reading the clock
uint64_t tsc = __rdtsc();
// _mm_lfence(); // optionally block later instructions until rdtsc retires
return tsc;
}
// requires a Nehalem or newer CPU. Not Core2 or earlier. IDK when AMD added it.
inline
uint64_t readTSCp() {
unsigned dummy;
return __rdtscp(&dummy); // waits for earlier insns to retire, but allows later to start
}
Compila con los 4 compiladores principales: gcc / clang / ICC / MSVC, para 32 o 64 bits. Vea los resultados en el explorador del compilador Godbolt , incluyendo un par de llamadas de prueba.
Estos intrínsecos fueron nuevos en gcc4.5 (de 2010) y clang3.5 (de 2014) . gcc4.4 y clang 3.4 en Godbolt no compilan esto, pero gcc4.5.3 (abril de 2011) sí lo hace. Es posible que vea asm en línea en el código anterior, pero puede y debe reemplazarlo con __rdtsc()
. Los compiladores con más de una década de antigüedad generalmente hacen un código más lento que gcc6, gcc7 o gcc8, y tienen mensajes de error menos útiles.
El intrínseco de MSVC (creo) ha existido por mucho más tiempo, porque MSVC nunca admitió asm en línea para x86-64. ICC13 tiene __rdtsc
en immintrin.h
, pero no tiene una x86intrin.h
en absoluto. Los ICC más recientes tienen x86intrin.h
, al menos la forma en que Godbolt los instala para Linux.
Es posible que desee definirlos como firmados long long
, especialmente si desea restarlos y convertirlos a flotar. int64_t
-> float / double es más eficiente que uint64_t
en x86 sin AVX512. Además, los resultados negativos pequeños podrían ser posibles debido a las migraciones de la CPU si los TSC no están perfectamente sincronizados, y eso probablemente tenga más sentido que los grandes números sin firmar.
Por cierto, clang también tiene un __builtin_readcyclecounter()
portátil que funciona en cualquier arquitectura. (Siempre devuelve cero en arquitecturas sin un contador de ciclos). Consulte la documentación de extensión de lenguaje clang / LLVM
Para obtener más información sobre el uso de lfence
(o cpuid
) para mejorar la repetibilidad de rdtsc
y controlar exactamente qué instrucciones están / no están en el intervalo de tiempo bloqueando la ejecución fuera de orden , consulte la respuesta de @HadiBrais en clflush para invalidar la línea de caché a través de C Función y los comentarios para un ejemplo de la diferencia que hace.
Consulte también ¿Se está serializando LFENCE en los procesadores AMD? (TL: DR sí con la mitigación Specter habilitada; de lo contrario, los núcleos dejan el MSR relevante sin configurar, por lo que debería usar cpuid
para serializar). Siempre se ha definido como serialización parcial en Intel.
Cómo comparar los tiempos de ejecución de código en las arquitecturas de conjuntos de instrucciones Intel® IA-32 e IA-64 , un informe de Intel de 2010.
rdtsc
cuenta ciclos de referencia , no ciclos de reloj de núcleo de CPU
Cuenta a una frecuencia fija, independientemente del turbo / ahorro de energía, por lo que si desea un análisis por segundo, use contadores de rendimiento. rdtsc
está correlacionada exactamente con la hora del reloj de pared (excepto por los ajustes del reloj del sistema, por lo que es una fuente de steady_clock
perfecta para steady_clock
). Cumple con la frecuencia nominal de la CPU, es decir, la frecuencia de la etiqueta anunciada. (O casi eso, por ejemplo, 2592 MHz en un i7-6700HQ 2.6 GHz Skylake).
Si lo usa para microbenchmarking, incluya primero un período de calentamiento para asegurarse de que su CPU ya esté a la velocidad máxima del reloj antes de comenzar a cronometrar. (Y, opcionalmente, desactive el turbo y dígale a su sistema operativo que prefiera la velocidad máxima del reloj para evitar los cambios de frecuencia de la CPU durante su marca microbiológica). O mejor aún, use una biblioteca que le brinde acceso a los contadores de rendimiento del hardware, o un truco como perf stat para parte del programa si su región cronometrada es lo suficientemente larga como para adjuntar un perf stat -p PID
.
Sin embargo, por lo general, aún querrá mantener el reloj de la CPU fijo para las microbenchmarks, a menos que quiera ver cómo las diferentes cargas harán que Skylake se desactive cuando está enlazado a la memoria o lo que sea. (Tenga en cuenta que el ancho de banda / latencia de la memoria es en su mayoría fijo, con un reloj diferente al de los núcleos. A velocidad de reloj inactivo, una falla de caché L2 o L3 requiere muchos menos ciclos de reloj de núcleo).
- ¿Mediciones negativas del ciclo de reloj con rdtsc consecutivo? la historia de RDTSC: originalmente las CPU no ahorraban energía, por lo que el TSC era tanto de tiempo real como de relojes centrales. Luego evolucionó a través de varios pasos poco útiles en su forma actual de una fuente de tiempo útil y de baja sobrecarga desconectada de los ciclos de reloj del núcleo (
constant_tsc
), que no se detiene cuando el reloj se detiene (nonstop_tsc
). También algunos consejos, por ejemplo, no tome el tiempo promedio, tome la mediana (habrá valores atípicos muy altos). - std :: chrono :: reloj, hardware reloj y ciclo cuenta
- Obtención de ciclos de CPU con RDTSC: ¿por qué el valor de RDTSC siempre aumenta?
- ¿Ciclos perdidos en Intel? Una inconsistencia entre rdtsc y CPU_CLK_UNHALTED.REF_TSC
- la medición de los tiempos de ejecución de código en C usando la instrucción RDTSC enumera algunos errores, incluyendo SMI (interrupciones de administración del sistema) que no se pueden evitar incluso en modo kernel con
cli
), y la virtualización derdtsc
en una máquina virtual. Y, por supuesto, es posible utilizar elementos básicos, como interrupciones regulares, así que repita el tiempo varias veces y deseche los valores atípicos. Determine la frecuencia de TSC en Linux . Programar la consulta de la frecuencia de TSC es difícil y quizás no sea posible, especialmente en el espacio de usuario, o puede dar un resultado peor que calibrarlo . Calibrarlo usando otra fuente de tiempo conocida lleva tiempo. Vea esa pregunta para obtener más información acerca de lo difícil que es convertir TSC a nanosegundos (y sería bueno si pudiera preguntarle al sistema operativo cuál es el porcentaje de conversión, porque el sistema operativo ya lo hizo en el arranque).
Si está utilizando micro-marcados con RDTSC para propósitos de ajuste, lo mejor es usar solo tics y omitir incluso intentar convertirlos en nanosegundos. De lo contrario, use una función de tiempo de biblioteca de alta resolución como
std::chrono
oclock_gettime
. Vea un equivalente más rápido de gettimeofday para una discusión / comparación de las funciones de marca de hora, o lea una marca de hora compartida de la memoria para evitar elrdtsc
completo si su requisito de precisión es lo suficientemente bajo como para que una secuencia o interrupción del temporizador la actualice.Vea también Calcular el tiempo del sistema usando rdtsc para encontrar la frecuencia de cristal y el multiplicador.
Tampoco se garantiza que los TSC de todos los núcleos estén sincronizados . Entonces, si tu hilo migra a otro núcleo de CPU entre __rdtsc()
, puede haber un sesgo extra. (Sin embargo, la mayoría de los sistemas operativos intentan sincronizar los TSC de todos los núcleos, por lo que normalmente estarán muy cerca). Si está usando rdtsc
directamente, es probable que desee anclar su programa o hilo a un núcleo, por ejemplo, con taskset -c 0 ./myprogram
en Linux.
La operación de recuperación de TSC de la CPU, especialmente en un entorno de multiprocesador multinúcleo, indica que Nehalem y las más recientes tienen el TSC sincronizado y bloqueado para todos los núcleos de un paquete (es decir, el TSC invariante). Pero los sistemas multi-socket todavía pueden ser un problema. Incluso los sistemas más antiguos (como antes de Core2 en 2007) pueden tener un TSC que se detiene cuando el reloj central se detiene, o que está vinculado a la frecuencia real del reloj central en lugar de a los ciclos de referencia. (Las CPU más nuevas siempre tienen TSC constante y TSC ininterrumpido). Consulte la respuesta de @amdn en esa pregunta para obtener más detalles.
¿Qué tan bueno es el asm de usar el intrínseco?
Es tan bueno como el que obtendrías de @mstic en línea GNU C de Mysticial, o mejor, porque sabe que los bits superiores de RAX están en cero. La razón principal por la que querría mantener el inline asm es para compat con compiladores viejos y crujientes.
Una versión no en línea de la función readTSC
se compila con MSVC para x86-64 de esta forma:
unsigned __int64 readTSC(void) PROC ; readTSC
rdtsc
shl rdx, 32 ; 00000020H
or rax, rdx
ret 0
; return in RAX
Para las convenciones de llamada de 32 bits que devuelven enteros de 64 bits en edx:eax
, es solo rdtsc
/ ret
. No es que importe, siempre quieres que esto esté en línea.
En una llamada de prueba que lo usa dos veces y resta para calcular un intervalo de tiempo:
uint64_t time_something() {
uint64_t start = readTSC();
// even when empty, back-to-back __rdtsc() don''t optimize away
return readTSC() - start;
}
Todos los 4 compiladores hacen un código bastante similar. Esta es la salida de 32 bits de GCC:
# gcc8.2 -O3 -m32
time_something():
push ebx # save a call-preserved reg: 32-bit only has 3 scratch regs
rdtsc
mov ecx, eax
mov ebx, edx # start in ebx:ecx
# timed region (empty)
rdtsc
sub eax, ecx
sbb edx, ebx # edx:eax -= ebx:ecx
pop ebx
ret # return value in edx:eax
Esta es la salida x86-64 de MSVC (con desmangling de nombres aplicado). gcc / clang / ICC todos emiten código idéntico.
# MSVC 19 2017 -Ox
unsigned __int64 time_something(void) PROC ; time_something
rdtsc
shl rdx, 32 ; high <<= 32
or rax, rdx
mov rcx, rax ; missed optimization: lea rcx, [rdx+rax]
; rcx = start
;; timed region (empty)
rdtsc
shl rdx, 32
or rax, rdx ; rax = end
sub rax, rcx ; end -= start
ret 0
unsigned __int64 time_something(void) ENDP ; time_something
Los 4 compiladores usan or
+ mov
lugar de lea
para combinar las mitades bajas y altas en un registro diferente. Supongo que es una especie de secuencia enlatada que no logran optimizar.
Pero escribir un turno / lea inline asm no es mejor. Usted privaría al compilador de la oportunidad de ignorar los 32 bits altos del resultado en EDX, si está cronometrando un intervalo tan corto que solo mantiene un resultado de 32 bits. O si el compilador decide almacenar la hora de inicio en la memoria, podría usar dos almacenes de 32 bits en lugar de shift / o / mov. Si te molesta 1 uop adicional como parte de tu tiempo, es mejor que escribas toda tu marca microbiológica en ASM puro.
Sin embargo, tal vez podamos obtener lo mejor de ambos mundos con una versión modificada del código de @Mysticial:
// More efficient than __rdtsc() in some case, but maybe worse in others
uint64_t rdtsc(){
// long and uintptr_t are 32-bit on the x32 ABI (32-bit pointers in 64-bit mode), so #ifdef would be better if we care about this trick there.
unsigned long lo,hi; // let the compiler know that zero-extension to 64 bits isn''t required
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) + lo;
// + allows LEA or ADD instead of OR
}
En Godbolt , esto a veces da mejor asm que a __rdtsc()
para gcc / clang / ICC, pero otras veces engaña a los compiladores para que usen un registro adicional para guardar lo y hi por separado, por lo que clang puede optimizar en ((end_hi-start_hi)<<32) + (end_lo-start_lo)
. Esperemos que si hay una presión de registro real, los compiladores se combinarán antes. (gcc y ICC aún guardan lo / hi por separado, pero no optimizan también).
Pero gcc8 de 32 bits lo complica, compilando solo la función rdtsc()
con un add/adc
real con ceros en lugar de solo devolver el resultado en edx: eax como clang. (gcc6 y anteriores lo hacen bien con |
lugar de +
, pero definitivamente prefieren el intrínseco __rdtsc()
si te interesan los 32 bits de código-gen de gcc).