Hice algunos perfiles con la siguiente configuración: la máquina de prueba (AMD Athlon64 x2 3800+) se inició, cambió a modo largo (interrupciones deshabilitadas) y la instrucción de interés se ejecutó en un bucle, 100 iteraciones desenrolladas y 1,000 ciclos de bucle El cuerpo del bucle estaba alineado a 16 bytes. El tiempo se midió con una instrucción rdtsc antes y después del ciclo. Además, se ejecutó un bucle ficticio sin ninguna instrucción (que midió 2 ciclos por iteración de bucle y 14 ciclos para el resto) y el resultado se resta del resultado del tiempo de creación de la instrucción. Se midieron
Las siguientes instrucciones:
- "
lock cmpxchg [rsp - 8], rdx
" (ambos con partido comparación y falta de coincidencia),
- "
lock xadd [rsp - 8], rdx
",
- "
lock bts qword ptr [rsp - 8], 1
"
En todos los casos el tiempo medido fue de aproximadamente 310 ciclos, el error fue de aproximadamente +/- 8 ciclos
Este es el valor para la ejecución repetida en la misma memoria (en caché). Con una falta de caché adicional, los tiempos son considerablemente más altos. También esto se hizo con solo uno de los 2 núcleos activos, por lo que el caché era de propiedad exclusiva y no se requería sincronización de caché.
Para evaluar el costo de una instrucción encerrado en un error de caché, que añade una instrucción wbinvld
antes de la instrucción bloqueada y poner el wbinvld
más un add [rsp - 8], rax
en el circuito de comparación. En ambos casos, el costo fue de aproximadamente 80,000 ciclos por par de instrucciones. En caso de bloqueo bts, la diferencia de tiempo fue de aproximadamente 180 ciclos por instrucción.
Tenga en cuenta que este es el rendimiento recíproco, pero dado que las operaciones bloqueadas son operaciones de serialización, probablemente no exista diferencia con respecto a la latencia.
Conclusión: una operación bloqueada es pesada, pero una falta de memoria caché puede ser mucho más pesada. Además: una operación bloqueada no causa fallas en la memoria caché. Solo puede causar tráfico de sincronización de caché, cuando una línea de caché no es de propiedad exclusiva.
Para arrancar la máquina, utilicé una versión x64 de FreeLdr del proyecto ReactOS. Aquí está el código fuente del ASM:
#define LOOP_COUNT 1000
#define UNROLLED_COUNT 100
PUBLIC ProfileDummy
ProfileDummy:
cli
// Get current TSC value into r8
rdtsc
mov r8, rdx
shl r8, 32
or r8, rax
mov rcx, LOOP_COUNT
jmp looper1
.align 16
looper1:
REPEAT UNROLLED_COUNT
// nothing, or add something to compare against
ENDR
dec rcx
jnz looper1
// Put new TSC minus old TSC into rax
rdtsc
shl rdx, 32
or rax, rdx
sub rax, r8
ret
PUBLIC ProfileFunction
ProfileFunction:
cli
rdtsc
mov r8, rdx
shl r8, 32
or r8, rax
mov rcx, LOOP_COUNT
jmp looper2
.align 16
looper2:
REPEAT UNROLLED_COUNT
// Put here the code you want to profile
// make sure it doesn't mess up non-volatiles or r8
lock bts qword ptr [rsp - 8], 1
ENDR
dec rcx
jnz looper2
rdtsc
shl rdx, 32
or rax, rdx
sub rax, r8
ret
¿Qué operación atómica? –
@Jason S, Any. Una diferencia entre cas y atómica inc/dec es insignificante. – osgx
Las operaciones atómicas en un x86 se vuelven más lentas a medida que se pone más contención en la dirección de la memoria. Creo que, en general, son de un orden de magnitud más lento que la operación no bloqueada, pero claramente esto variará dependiendo de la operación, la contención y las barreras de memoria utilizadas. –