assembly optimization x86 micro-optimization

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.


, 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 porque test / and siempre hace eso, y para cmp porque restar cero no puede desbordarse o cargarse.
  • ZF , SF , PF configurados de acuerdo con el resultado (es decir, reg ): reg&reg para prueba, o reg - 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ón l ( SF!=OF ) es equivalente a SF . Cada CPU que puede macro-fuse TEST / JL también puede fusionar macro TEST / JS, incluso Core2. Pero después del CMP 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.