¿Probar si un registro es cero con CMP reg,0 vs OR reg,reg?

Resuelto sadljkfhalskdjfh asked hace 8 años • 2 respuestas

¿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).

sadljkfhalskdjfh avatar Nov 15 '15 22:11 sadljkfhalskdjfh
Aceptado

, 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,0que 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 ZFya está configurado apropiadamente mediante la instrucción que lo configuró, regpor 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,regno 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 regporque reescribe el valor en el registro. cmpLa desventaja de suele ser el tamaño del código.


Los resultados FLAGStest reg,reg de / and reg,reg/ or reg,regson
idénticos cmp reg, 0en todos los casos (excepto AF) porque :

  • CF = OF = 0porque testsiempre andhago eso, y porque cmprestar cero no puede desbordarse ni acarrearse.
  • ZF, SF, PFse establece según el resultado (es decir, reg): reg&regpara prueba o reg - 0para cmp.

( AFno 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 jsen algunas CPU después de un cmp. Son equivalentes después de compararlos con cero porque OF=0, por lo que la lcondició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 jleojg , mirando ZF y SF!=OF.


testes más corto de codificar que cmpcon el 0 inmediato, en todos los casos excepto en el cmp al, imm8caso especial que sigue siendo de dos bytes.

Incluso entonces, testes preferible por razones de macrofusión (con jley 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 cmpcon 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 andy add/ sub), así como testy cmp, pero orno es una de ellas. Las CPU AMD solo pueden fusionarse testy cmpcon 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. testPuede realizar una macrofusión en algunos casos en los que cmpno 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 ORo ANDes 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, regagrega otro ciclo de latencia a la cadena de dependencia para seguir instrucciones que necesitan leer el registro. Es una parte x |= xde 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).

testtiene 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 cmpo testque 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,sameen 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,regen 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.

( andes equivalente a orpara 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/ jccpero 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=nehalemconfiguració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 testa 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 andrealmente lo soluciona.

En la familia P6 anterior, and reg,regpodrí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,alevite escribir un registro parcial, que en la familia P6 se ​​renombra por separado del EAX/RAX completo. or al,ales 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,regidioma desafortunado.

Es posible que el or reg,regmodismo provenga de 8080 ORA A, como se señala en un comentario .

El conjunto de instrucciones del 8080 no tiene testinstrucciones, por lo que sus opciones para configurar indicadores de acuerdo con un valor incluyen ORA Ay ANA A. (Observe que el Adestino 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 Aera 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,eaxfunciona exactamente de manera equivalente a or eax,eaxla familia Intel P6, pero es menos malo en otros uarches porque andpuede fusionarse macro en la familia Sandybridge. (Consulte la sección P6 anterior).


Valor en la memoria: tal vez usarlo cmpo 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,eaxo 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/ jneo [rel some_static_location].

En i7-6700k Skylake (probado con eventos de rendimiento uops_issued.anyy uops_executed.thread):

  • mov reg, [mem](o movzx) + test reg,reg / jnz2 uops en dominios fusionados y no fusionados, independientemente del modo de direccionamiento, o movzxen 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) / jne3 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+ jne2 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 0registro (o 1si desea comparar un bool), puede cmp [mem], reg/ jnepara 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-imm8no está disponible, el tamaño del código es peor que el cmpde 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, 1de test ecx, 1, y los casos de uso como test ecx, 0xfffffff0son 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 byteo qword. ~0Serí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)
Peter Cordes avatar Nov 15 '2015 20:11 Peter Cordes