assembly - Pruebe si un registro es cero con CMP reg, 0 vs OR reg, reg?
optimization x86 (2)
¿Hay alguna diferencia de velocidad de ejecución con el siguiente código:
cmp al, 0
je done
y lo siguiente:
or al, al
jz done
Sé que las instrucciones JE y JZ son las mismas, y también que usar OR proporciona una mejora de tamaño de un byte. Sin embargo, también me preocupa la velocidad del código. Parece que los operadores lógicos serán más rápidos que un SUB o un CMP, pero solo quería asegurarme. Esto podría ser una compensación entre tamaño y velocidad, o ganar-ganar (por supuesto, el código será más opaco).
Depende de la secuencia de código exacta, qué CPU específica es y otros factores.
El principal problema con
or al, al,
es que "modifica"
EAX
, lo que significa que una instrucción posterior que usa
EAX
de alguna manera puede detenerse hasta que esta instrucción se complete.
Tenga en cuenta que la rama condicional (
jz
) también depende de la instrucción, pero los fabricantes de CPU hacen mucho trabajo (predicción de rama y ejecución especulativa) para mitigar eso.
También tenga en cuenta que, en teoría, sería posible que un fabricante de CPU diseñe una CPU que reconozca que
EAX
no ha cambiado en este caso específico, pero hay cientos de estos casos especiales y los beneficios de reconocer la mayoría de ellos son muy pocos.
El principal problema con
cmp al,0
es que es un poco más grande, lo que podría significar una recuperación de instrucciones más lenta / más presión de caché, y (si es un bucle) podría significar que el código ya no cabe en el "buffer de bucle" de alguna CPU.
Como Jester señaló en los comentarios;
test al,al
evita ambos problemas: es más pequeño que
cmp al,0
y no modifica
EAX
.
Por supuesto (dependiendo de la secuencia específica) el valor en
AL
debe provenir de algún lugar, y si proviene de una instrucción que establezca los indicadores de manera apropiada, es posible modificar el código para evitar usar otra instrucción para establecer nuevamente los indicadores más tarde.
Sí , hay una diferencia en el rendimiento.
La mejor opción para comparar un registro con cero
en el x86 moderno es
test reg, reg
(si
ZF
no está configurado correctamente por la instrucción que establece
reg
).
Es como
AND reg,reg
pero sin escribir el destino.
or reg,reg
no puede fusionar macro, agrega latencia para cualquier cosa que lo lea más tarde y necesita un nuevo registro físico para contener el resultado.
(Por lo tanto, utiliza recursos de cambio de nombre de registro donde la
test
no lo haría, lo que
limita la ventana de instrucciones fuera de orden de la CPU
).
(Reescribir el dst puede ser una victoria para la familia Intel P6, sin embargo, ver más abajo).
Los resultados de la
test reg,reg
de
test reg,reg
/
and reg,reg
/
or reg,reg
son idénticos a
cmp reg, 0
en todos los casos (excepto AF):
-
CF = OF = 0
porquetest
/and
siempre hace eso, y paracmp
porque restar cero no puede desbordarse o cargarse. -
ZF
,SF
,PF
configurados de acuerdo con el resultado (es decir,reg
):reg®
para prueba, oreg - 0
para cmp. Por lo tanto, puede probar los enteros con signo negativo o sin signo con el bit alto establecido mirando SF.O con
jl
, porque OF = 0, entonces la condiciónl
(SF!=OF
) es equivalente aSF
. Cada CPU que puede macro-fuse TEST / JL también puede fusionar macro TEST / JS, incluso Core2. Pero después delCMP byte [mem],0
, use siempre JL, no JS para bifurcarse en el bit de signo.
(
AF
no está definido después de la
test
, pero se establece de acuerdo con el resultado para
cmp
. Lo estoy ignorando porque es realmente oscuro: los únicos consumidores de AF son las instrucciones BCD empaquetadas con ajuste ASCII como
AAS
y
lahf
/
pushf
).
test
es más corta para codificar
que
cmp
con 0 inmediato, en todos los casos, excepto el caso especial
cmp al, imm8
que todavía tiene dos bytes.
Incluso entonces, la
test
es preferible por razones de macro fusión (con
jle
y similar en Core2), y porque no tener nada inmediato puede ayudar a la densidad uop-cache al dejar un espacio que otra instrucción puede tomar prestada si necesita más espacio (SnB -familia).
Los decodificadores en las CPU Intel y AMD pueden
fusionar
internamente
las
test
macro
y
cmp
con algunas instrucciones de bifurcación condicionales en una sola operación de comparación y bifurcación.
Esto le proporciona un rendimiento máximo de 5 instrucciones por ciclo cuando ocurre la macro fusión, frente a 4 sin macro fusión.
(Para CPU de Intel desde Core2).
Las CPU Intel recientes pueden fusionar macro algunas instrucciones (como
and
y
add
/
sub
), así como
test
y
cmp
, pero
or
no es una de ellas.
Las CPU AMD solo pueden fusionar
test
y
cmp
con un JCC.
Consulte
macro-fuse
, o simplemente consulte directamente
los documentos de microarquitectura de Agner Fog
para obtener detalles sobre qué CPU puede fusionar macro qué.
test
puede macro-fusionarse en algunos casos donde
cmp
no puede, por ejemplo, con
js
.
Casi todas las operaciones ALU simples (booleano a nivel de bit, agregar / sub, etc.) se ejecutan en un solo ciclo.
Todos tienen el mismo "costo" en rastrearlos a través de la tubería de ejecución fuera de orden.
Intel y AMD gastan los transistores para hacer unidades de ejecución rápida para agregar / sub / lo que sea en un solo ciclo.
Sí,
OR
o
AND
bit es más simple y probablemente usa menos energía, pero aún así no puede funcionar más rápido que un ciclo de reloj.
Además, como señala Brendan,
or reg, reg
agrega otro ciclo de latencia
a la cadena de dependencia para seguir las instrucciones que deben leer el registro.
Sin embargo, en las CPU de la familia P6 (PPro / PII a Nehalem), escribir el registro de destino puede ser realmente una ventaja . Hay un número limitado de puertos de lectura de registro para que la etapa de emisión / cambio de nombre se lea desde el archivo de registro permanente, pero los valores recientemente escritos están disponibles directamente desde el ROB. Reescribir un registro innecesariamente puede hacer que vuelva a estar vivo en la red de reenvío para ayudar a evitar paradas de lectura de registros. (Ver el microarchivo de Agner Fog en pdf .
Según los informes, el compilador de Delphi usa
or eax,eax
, que era una opción razonable en ese momento, suponiendo que las paradas de lectura de registro eran más importantes que alargar la cadena de dep para lo que se lea a continuación.
Desafortunadamente, los compiladores-escritores en ese momento no sabían el futuro, porque
and eax,eax
funciona exactamente igual
or eax,eax
en la familia Intel P6, pero es menos malo en otros uarches porque
and
puede fusionarse macro en Sandybridge- familia.
Para Core2 / Nehalem (las últimas 2 uarches de la familia P6), la
test
puede fusionarse macro pero no puede, por lo que (a diferencia de Pentium II / III / M) es una compensación entre macro fusión y posiblemente reducir el registro. leer puestos.
La evitación del registro-lectura-pérdida todavía tiene un costo de latencia adicional si el valor se lee después de ser probado, por lo que la
test
puede ser una mejor opción que,
and
en algunos casos, incluso antes de un
cmov
o
setcc
, no un
jcc
, o en CPU sin macro fusión.
Si está ajustando algo para que sea rápido en múltiples uarches, use la
test
menos que la creación de perfiles muestre que las paradas de lectura de registro son un gran problema en un caso específico en Core2 / Nehalem, y el uso
and
realidad lo corrige.
IDK de donde vino el idioma de
or reg,reg
, excepto tal vez que es más corto de escribir.
O tal vez se utilizó a propósito para las CPU P6 para reescribir un registro deliberadamente antes de usarlo un poco más.
Los codificadores en ese momento no podían predecir que terminaría siendo menos eficiente que
and
para ese propósito.
Pero, obviamente, nunca deberíamos usarlo durante una
test
o en un código nuevo.
(Solo hay una diferencia cuando es inmediatamente antes de un jcc en Sandybridge-family, pero es más simple olvidarse
or reg,reg
.)
Para probar un valor en la memoria
, está bien
cmp dword [mem], 0
, pero las CPU Intel no pueden fusionar macro las instrucciones de configuración del indicador que tienen un operando inmediato y uno de memoria.
Si va a usar el valor después de la comparación en un lado de la rama, probablemente debería
mov eax, [mem]
/
test eax,eax
o algo así.
Si no es así (por ejemplo, probar un booleano),
cmp
con un operando de memoria está bien.
Aunque tenga en cuenta que algunos modos de direccionamiento
no se fusionarán ni en la familia SnB
: relativo a RIP + inmediato no se fusionará en los decodificadores, o un modo de direccionamiento indexado se deslaminará.
De cualquier manera, se obtienen 3 uops de dominio fusionado para
cmp dword [rsi + rcx*4], 0
/
jne
o
[rel some_static_location]
.
También
puede
probar un valor en la memoria con
test dword [mem], -1
, pero no lo haga.
Dado que la
test r/m16/32/64, sign-extended-imm8
no está disponible, tiene un tamaño de código peor que
cmp
para algo más grande que bytes.
(Creo que la idea de diseño era que si solo desea probar el bit bajo de un registro, solo
test cl, 1
lugar de
test ecx, 1
, y use casos como
test ecx, 0xfffffff0
son lo suficientemente raros como para que no sea vale la pena gastar un código operativo. Especialmente porque esa decisión se tomó para 8086 con código de 16 bits, donde solo era la diferencia entre un imm8 e imm16, no imm32).
Escribí -1 en lugar de 0xFFFFFFFF, por lo que sería lo mismo con
byte
o
qword
.
~0
sería otra forma de escribirlo.