¿Probar si un registro es cero con CMP reg,0 vs OR reg,reg?
¿Existe alguna diferencia en la velocidad de ejecución usando 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 sólo quería estar seguro. Esto podría ser una compensación entre tamaño y velocidad, o una situación en la que todos ganan (por supuesto, el código será más opaco).
Sí , hay una diferencia en el rendimiento.
La mejor opción para comparar un registro con cero es test reg, reg
. Establece BANDERAS de la misma manera cmp reg,0
que lo haría, y es al menos tan rápido como cualquier otra forma, con un tamaño de código más pequeño.
(Aún mejor es cuando ZF
ya está configurado apropiadamente mediante la instrucción que lo configuró, reg
por lo que puede simplemente bifurcar, setcc o cmovcc directamente. Por ejemplo, la parte inferior de un bucle normal a menudo se parece a dec ecx
/ jnz .loop_top
. La mayoría de las instrucciones de enteros x86 "establecen indicadores de acuerdo con el resultado", incluido ZF=1 si la salida fue 0
.).
or reg,reg
no se puede realizar una macrofusión con un JCC en una sola uop en ninguna CPU x86 existente y agrega latencia para cualquier cosa que se lea posteriormente reg
porque reescribe el valor en el registro. cmp
La desventaja de suele ser el tamaño del código.
Los resultados FLAGStest reg,reg
de / and reg,reg
/ or reg,reg
son
idénticos cmp reg, 0
en todos los casos (excepto AF) porque :
CF = OF = 0
porquetest
siempreand
hago eso, y porquecmp
restar cero no puede desbordarse ni acarrearse.ZF
,SF
,PF
se establece según el resultado (es decir,reg
):reg®
para prueba oreg - 0
para cmp.
( AF
no está definido después test
, pero se establece de acuerdo con el resultado para cmp
. Lo estoy ignorando porque es muy oscuro: las únicas instrucciones que leen AF son las instrucciones BCD empaquetadas de ajuste ASCII como AAS
, y lahf
/ pushf
.)
Por supuesto, puede comprobar condiciones distintas a reg == 0
(ZF), por ejemplo, comprobar si hay números enteros con signo negativo mirando SF. Pero un dato curioso: jl
, la condición menor que firmada, es más eficiente que js
en algunas CPU después de un cmp
. Son equivalentes después de compararlos con cero porque OF=0, por lo que la l
condición ( SF!=OF
) es equivalente a SF
.
Cada CPU que puede fusionar macro TEST/JL también puede fusionar macro TEST/JS, incluso Core 2. Pero después CMP byte [mem], 0
, siempre use JL, no JS, para bifurcar en el bit de signo porque Core 2 no puede fusionar eso. (Al menos en modo de 32 bits; Core 2 no puede realizar ninguna macrofusión en modo de 64 bits).
Las condiciones de comparación firmada también te permiten hacer cosas como jle
ojg
, mirando ZF y SF!=OF.
test
es más corto de codificar que cmp
con el 0 inmediato, en todos los casos excepto en el cmp al, imm8
caso especial que sigue siendo de dos bytes.
Incluso entonces, test
es preferible por razones de macrofusión (con jle
y similar en Core2), y porque no tener nada inmediato puede ayudar a la densidad de caché uop al dejar una ranura que otra instrucción puede tomar prestada si necesita más espacio (familia SnB). ).
Macrofusión de test/jcc en un solo uop en los decodificadores
Los decodificadores de las CPU Intel y AMD pueden fusionarse macro test
internamente y cmp
con algunas instrucciones de bifurcación condicionales en una única operación de comparación y bifurcación. Esto le brinda un rendimiento máximo de 5 instrucciones por ciclo cuando ocurre la macrofusión, frente a 4 sin macrofusión. (Para CPU 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 fusionarse test
y cmp
con un JCC. Consulte x86_64 - Ensamblaje - condiciones de bucle y fuera de servicio , o simplemente consulte directamente los documentos de microarquitectura de Agner Fog para obtener detalles sobre qué CPU puede fusionar qué macro. test
Puede realizar una macrofusión en algunos casos en los que cmp
no es posible, por ejemplo, con js
.
Casi todas las operaciones ALU simples (booleanas bit a bit, agregar/subir, etc.) se ejecutan en un solo ciclo. Todos tienen el mismo "costo" al rastrearlos a través del proceso de ejecución fuera de orden. Intel y AMD gastan los transistores para crear unidades de ejecución rápida para agregar/subir/lo que sea en un solo ciclo. Sí, bit a bit OR
o AND
es más simple, y probablemente usa un poco menos de energía, pero aún así no puede funcionar más rápido que un ciclo de reloj.
or reg, reg
agrega otro ciclo de latencia a la cadena de dependencia para seguir instrucciones que necesitan leer el registro. Es una parte x |= x
de la cadena de operaciones que conducen al valor que deseas más adelante.
Se podría pensar que la escritura de registro adicional también necesitaría una entrada de archivo de registro físico (PRF) adicional en comparación con test
, pero probablemente ese no sea el caso. (Consulte https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/ para obtener más información sobre el impacto de la capacidad de PRF en el ejecutivo fuera de servicio).
test
tiene que producir su salida FLAGS en alguna parte. Al menos en las CPU de la familia Intel Sandybridge, cuando una instrucción produce un registro y un resultado FLAGS, ambos se almacenan juntos en la misma entrada PRF. (Fuente: creo que una patente de Intel. Esto es de memoria, pero parece un diseño obviamente sensato ya que la mayoría de las instrucciones de ALU x86 escriben FLAG).
Una instrucción como cmp
o test
que solo produce un resultado FLAGS también necesita una entrada PRF para su salida. Presumiblemente esto es un poco peor : el antiguo registro físico todavía está "vivo", referenciado como el poseedor del valor del registro arquitectónico escrito por alguna instrucción más antigua. Y ahora los EFLAGS arquitectónicos (en realidad, los grupos de banderas CF y SPAZO , renombrados por separado ) apuntan a este nuevo registro físico en la RAT (tabla de asignación de registros) actualizada por el renombrador. Por supuesto, la siguiente instrucción de escritura FLAGS sobrescribirá eso, permitiendo que el PR se libere una vez que todos sus lectores lo hayan leído y ejecutado. Esto no es algo en lo que pienso al optimizar y no creo que tenga importancia en la práctica.
Nota a pie de página 1: Puestos en pérdida de lectura de registros de la familia P6: posible ventaja paraor reg,reg
Esto solo se aplica a las CPU obsoletas de la familia P6 (Intel hasta Nehalem, reemplazadas por la familia Sandybridge en 2011). En otras CPU, no hay ningún beneficio en reescribir un registro consigo mismo or same,same
en lugar de simplemente leerlo con test same,same
.
Las CPU de la familia P6 (PPro / PII a Nehalem) tienen un número limitado de puertos de lectura de registro para que la etapa de emisión/cambio de nombre lea valores "fríos" (no reenviados desde una instrucción en vuelo) del archivo de registro permanente, pero recientemente Los valores escritos están disponibles directamente desde el ROB. Reescribir un registro innecesariamente puede hacer que vuelva a estar activo en la red de reenvío para ayudar a evitar paradas en la lectura de registros. (Ver el pdf del microarco de Agner Fog ).
Reescribir un registro con el mismo valor a propósito para mantenerlo "caliente" puede ser en realidad una optimización para algunos casos de código circundante, en P6. Las primeras CPU de la familia P6 no podían realizar macrofusión en absoluto, por lo que ni siquiera te lo estás perdiendo si usas and reg,reg
en lugar de test
. Pero Core 2 (en modo de 32 bits) y Nehalem (en cualquier modo) pueden fusionar macro test/jcc, por lo que te lo estás perdiendo.
( and
es equivalente a or
para este propósito en la familia P6, pero menos malo si su código alguna vez se ejecuta en una CPU de la familia Sandybridge: puede fusionar macro and
/ jcc
pero no or
/ jcc
. El ciclo adicional de latencia en la cadena dep para el registro sigue siendo una desventaja en P6, especialmente si la ruta crítica que lo involucra es el principal cuello de botella).
La familia P6 está muy obsoleta hoy en día (Sandybridge la reemplazó en 2011), y las CPU anteriores a Core 2 (Core, Pentium M, PIII, PII, PPro) están muy obsoletas y están entrando en territorio de retrocomputación, especialmente para cualquier cosa donde el rendimiento importe. Puede ignorar la familia P6 al optimizar a menos que tenga una máquina de destino específica en mente (por ejemplo, si tiene una vieja máquina Nehalem Xeon) o esté ajustando la -mtune=nehalem
configuración de un compilador para los pocos usuarios que aún quedan.
Si está ajustando algo para que sea rápido en Core 2/Nehalem, utilícelo test
a menos que la creación de perfiles muestre que los bloqueos de lectura de registros son un gran problema en un caso específico, y el uso and
realmente lo soluciona.
En la familia P6 anterior, and reg,reg
podría estar bien como opción de generación de código predeterminada cuando el valor no es parte de una cadena de depósito problemática transmitida por bucle, pero se lee más tarde. O si lo es, pero también hay un bloqueo de lectura de registros específico que puedes solucionar con and reg,reg
.
Si solo desea probar los 8 bits inferiores de un registro completo, test al,al
evite escribir un registro parcial, que en la familia P6 se renombra por separado del EAX/RAX completo. or al,al
es mucho peor si luego lees EAX o AX: bloqueo de registro parcial en la familia P6. (¿ Por qué GCC no utiliza registros parciales? )
Historia del or reg,reg
idioma desafortunado.
Es posible que el or reg,reg
modismo provenga de 8080 ORA A
, como se señala en un comentario .
El conjunto de instrucciones del 8080 no tiene test
instrucciones, por lo que sus opciones para configurar indicadores de acuerdo con un valor incluyen ORA A
y ANA A
. (Observe que el A
destino del registro está integrado en el mnemotécnico para ambas instrucciones, y no hay instrucciones para realizar O en diferentes registros: es una máquina de 1 dirección excepto mov
, mientras que 8086 es una máquina de 2 direcciones para la mayoría de las instrucciones. )
8080 ORA A
era la forma habitual de hacerlo, por lo que presumiblemente ese hábito se trasladó a la programación ensambladora de 8086 a medida que la gente portaba sus fuentes asm. (O herramientas automáticas usadas; 8086 fue diseñado intencionalmente para una transferencia fácil/automática de fuente asm desde el código 8080 ).
Este mal modismo sigue siendo utilizado ciegamente por principiantes, presumiblemente enseñado por personas que lo aprendieron en el pasado y lo transmitieron sin pensar en la desventaja obvia de la latencia del camino crítico para una ejecución fuera de orden. (U otros problemas más sutiles como la falta de macrofusión).
Según se informa , el compilador de Delphi usaor eax,eax
, lo que tal vez era una opción razonable en ese momento (antes de Core 2), asumiendo que las paradas de lectura de registros eran más importantes que alargar la cadena de depósito para lo que sea que se lea a continuación. IDK si eso es cierto o simplemente estaban usando el idioma antiguo sin pensar en ello.
Desafortunadamente, los escritores de compiladores en ese momento no conocían el futuro, porque and eax,eax
funciona exactamente de manera equivalente a or eax,eax
la familia Intel P6, pero es menos malo en otros uarches porque and
puede fusionarse macro en la familia Sandybridge. (Consulte la sección P6 anterior).
Valor en la memoria: tal vez usarlo cmp
o cargarlo en un registro.
Para probar un valor en la memoria , puede hacerlo cmp dword [mem], 0
, pero las CPU Intel no pueden fusionar macroinstrucciones de configuración de indicadores que tienen un operando inmediato y otro de memoria. Si va a utilizar el valor después de la comparación en un lado de la rama, debería mov eax, [mem]
/ test eax,eax
o algo así. De lo contrario, de cualquier manera son 2 uops de front-end, pero es una compensación entre el tamaño del código y el recuento de uops de back-end.
Aunque tenga en cuenta que algunos modos de direccionamiento no se microfusionarán en la familia SnB : RIP-relativo + inmediato no se microfusionará en los decodificadores, o un modo de direccionamiento indexado se deslaminará después del uop-cache. De cualquier manera, conduce a 3 uops de dominio fusionado para cmp dword [rsi + rcx*4], 0
/ jne
o [rel some_static_location]
.
En i7-6700k Skylake (probado con eventos de rendimiento uops_issued.any
y uops_executed.thread
):
mov reg, [mem]
(omovzx
) +test reg,reg / jnz
2 uops en dominios fusionados y no fusionados, independientemente del modo de direccionamiento, omovzx
en lugar de mov. Nada que microfusionar; hace macrofusión.cmp byte [rip+static_var], 0
+jne
. 3 fusionados, 3 no fusionados. (extremos delantero y trasero). La combinación RIP-relativa + inmediata previene la microfusión. Tampoco se fusiona macro. Tamaño de código más pequeño pero menos eficiente.cmp byte [rsi + rdi], 0
(modo de dirección indexada) /jne
3 fusionados, 3 no fusionados. Microfusibles en los decodificadores, pero se deslaminan en el momento de la emisión/cambio de nombre. No se fusiona macro.cmp byte [rdi + 16], 0
+jne
2 uops fusionados, 3 no fusionados. La microfusión de cmp load+ALU se produjo debido al modo de direccionamiento simple, pero lo inmediato impide la macrofusión. Casi tan bueno como load + test + jnz: tamaño de código más pequeño pero 1 uop de back-end adicional.
Si tiene un 0
registro (o 1
si desea comparar un bool), puede cmp [mem], reg
/ jne
para incluso menos uops, tan solo 1 dominio fusionado, 2 no fusionados. Pero los modos de direccionamiento relativos a RIP todavía no fusionan macro.
Los compiladores tienden a usar load + test/jcc incluso cuando el valor no se usa más adelante.
También puedes probar un valor en la memoria con , perotest dword [mem], -1
no lo hagas. Como test r/m16/32/64, sign-extended-imm8
no está disponible, el tamaño del código es peor que el cmp
de cualquier cosa mayor que bytes. (Creo que la idea del diseño fue que si solo desea probar el bit bajo de un registro, en lugar test cl, 1
de test ecx, 1
, y los casos de uso como test ecx, 0xfffffff0
son lo suficientemente raros como para que no valga la pena gastar un código de operación. Especialmente porque esa decisión se tomó para 8086 con código de 16 bits, donde solo era la diferencia entre un imm8 y un imm16, no un 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).
Relacionado:
- ¿Qué es la fusión de instrucciones en los procesadores x86 contemporáneos? (micro y macrofusión). TODO: mueva los resultados de la prueba allí (y actualice mi respuesta allí para corregir algunas cosas que no coinciden con mis resultados actuales).
- x86_64 - Ensamblaje - condiciones de bucle y fuera de servicio (qué instrucciones pueden fusionarse macro en la familia Sandybridge)