¿Cuál es la mejor manera de establecer un registro a cero en el ensamblaje x86: xor, mov o and?


Todas las siguientes instrucciones hacen lo mismo: pon %eax a cero. ¿De qué manera es óptima (requiere menos ciclos de máquina)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax
Author: Peter Cordes, 2015-11-12

1 answers

TL; DR resumen: xor same, same es la mejor opción para todas las CPU. Ningún otro método tiene ninguna ventaja sobre él, y tiene al menos alguna ventaja sobre cualquier otro método. Está oficialmente recomendado por Intel y AMD. En el modo de 64 bits, todavía use xor r32, r32, porque escribiendo un reg ceros de 32 bits el superior 32. xor r64, r64 es un desperdicio de un byte, porque necesita un prefijo REX.

La puesta a cero de un registro vectorial suele hacerse mejor con pxor xmm, xmm. Eso es típicamente lo que hace gcc (incluso antes de usar con instrucciones de FP).

xorps xmm, xmm puede tener sentido. Es un byte más corto que pxor, pero xorps necesita el puerto de ejecución 5 en Intel Nehalem, mientras que pxor puede ejecutarse en cualquier puerto (0/1/5). (La latencia de retardo de bypass 2c de Nehalem entre integer y FP generalmente no es relevante, porque la ejecución fuera de orden generalmente puede ocultarla al inicio de una nueva cadena de dependencias).

En las microarquitecturas de la familia SnB, ni el sabor de xor-zeroing necesita siquiera un puerto de ejecución. En AMD, y pre-Nehalem P6/Core2 Intel, xorps y pxor se manejan de la misma manera (como instrucciones de enteros vectoriales).

Usando la versión AVX de una instrucción vectorial 128b ceros la parte superior del reg también, así que vpxor xmm, xmm, xmm es una buena opción para poner a cero YMM(AVX1/AVX2) o ZMM(AVX512), o cualquier extensión vectorial futura. vpxor ymm, ymm, ymm no toma ningún byte adicional para codificar, sin embargo, y ejecuta lo mismo. La puesta a cero de ZMM AVX512 requeriría bytes adicionales( para el prefijo EVEX), por lo que la puesta a cero de XMM o YMM debería ser preferido.


Algunas CPU reconocen a sub same,same como un modismo de reducción a cero como xor, pero todas las CPU que reconocen cualquier modismo de reducción a cero reconocen xor. Solo tiene que usar xor para no tener que preocuparse por qué CPU reconoce qué idioma de reducción a cero.

xor (ser un lenguaje de cero reconocido, a diferencia de mov reg, 0) tiene algunas ventajas obvias y algunas sutiles (lista de resumen, luego las ampliaré):

  • menor tamaño de código que mov reg,0. (Todo CPUs)
  • evita penalizaciones de registro parcial para códigos posteriores. (Intel P6-familia y SnB-familia).
  • no utiliza una unidad de ejecución, ahorrando energía y liberando recursos de ejecución. (Familia Intel SnB)
  • uop más pequeño (sin datos inmediatos) deja espacio en la línea de caché de uop para instrucciones cercanas para pedir prestado si es necesario. (Intel SnB-familia).
  • no utiliza entradas en el archivo de registro físico. (Intel SnB-familia (y P4) por lo menos, posiblemente AMD también, ya que utilizan un diseño PRF similar en lugar de mantener el estado de registro en las microarquitecturas ROB como Intel P6-family.)

El tamaño de código máquina más pequeño (2 bytes en lugar de 5) siempre es una ventaja: una mayor densidad de código conduce a menos errores de caché de instrucciones, y una mejor recuperación de instrucciones y potencialmente decodificar el ancho de banda.


El beneficio de no usar una unidad de ejecución para xor en microarquitecturas de la familia Intel SnB es menor, pero ahorra energía. Es más probable que importe en SnB o IvB, que solo tienen 3 puertos de ejecución ALU. Haswell y posteriores tienen 4 puertos de ejecución que pueden manejar instrucciones de enteros ALU, incluyendo mov r32, imm32, por lo que con una toma de decisiones perfecta por parte del programador (que no sucede en la práctica), HSW aún podría sostener 4 uops por reloj incluso cuando todos necesitan puertos de ejecución.

Vea mi respuesta a otra pregunta sobre la reducción a cero de registros para más detalles.

La entrada de blog de Bruce Dawson que Michael Petch enlazó (en un comentario sobre la pregunta) señala que xor se maneja en la etapa de cambio de nombre de registro sin necesidad de una unidad de ejecución (cero uops en el dominio no fusionado), pero se perdió el hecho de que todavía es un uop en el dominio fusionado. Las CPU Intel modernas pueden emitir y retirar 4 uops de dominio fusionado por reloj. De ahí viene el límite de 4 ceros por reloj. El aumento de la complejidad del hardware de cambio de nombre del registro es solo uno de las razones para limitar el ancho del diseño a 4. (Bruce ha escrito algunas entradas de blog muy excelentes, como su serie sobre FP math y x87 / SSE / rounding issues, que recomiendo encarecidamente).


En las cpu de la familia AMD Bulldozer, mov immediate se ejecuta en los mismos puertos de ejecución de enteros EX0 / EX1 que xor. mov reg,reg también se puede ejecutar en AGU0/1, pero eso es solo para copiar registros, no para configurar desde immediates. Así AFAIK, en AMD la única ventaja a xor sobre mov es la codificación más corta. También podría ahorrar recursos de registro físico, pero no he visto ninguna prueba.


Expresiones de reducción a cero reconocidas evitan penalizaciones de registro parcial en CPU Intel que renombran registros parciales por separado de registros completos (familias P6 y SnB).

xor etiquetará el registro como teniendo las partes superiores puestas a cero , así xor eax, eax / inc al / inc eax evita la penalización habitual de registro parcial que tienen las CPU pre-IvB. Incluso sin xor, IvB solo necesita un uop de fusión cuando se modifican los 8bits altos (AH) y luego se lee todo el registro, y Haswell incluso lo elimina.

De la guía microarch de Agner Fog, pg 98 (sección Pentium M, referenciada por secciones posteriores incluyendo SnB):

El procesador reconoce el XOR de un registro consigo mismo como configuración a cero. Una etiqueta especial en el registro recuerda que la parte alta del registro es cero por lo que EAX = AL. Esta etiqueta es recordada incluso en un bucle:

    ; Example    7.9. Partial register problem avoided in loop
    xor    eax, eax
    mov    ecx, 100
LL:
    mov    al, [esi]
    mov    [edi], eax    ; No extra uop
    inc    esi
    add    edi, 4
    dec    ecx
    jnz    LL

(de pg82): El procesador recuerda que los 24 bits superiores de EAX son cero siempre y cuando no obtienes una interrupción, mala predicción u otro evento serializado.

Pg82 de esa guía también confirma que mov reg, 0 es no reconocido como un modismo de cero, al menos en los primeros diseños P6 como PIII o PM. Me sorprendería mucho si gastaran transistores en detectarlo en CPU posteriores.


xor establece banderas, lo que significa que tienes que tener cuidado al probar las condiciones. Desde setcc desafortunadamente, solo está disponible con un destino de 8 bits , por lo general debe tener cuidado para evitar penalizaciones de registro parcial.

Habría sido bueno si x86-64 reutilizara uno de los opcodes eliminados (como AAM) para un bit setcc r/m de 16/32/64, con el predicado codificado en el campo de 3 bits del registro fuente del campo r/m (de la manera en que otras instrucciones de un solo operando los usan como bits de opcode). Pero no hizo eso, y eso no ayudaría para x86-32 de todos modos.

Idealmente, deberías usar xor / set flags / setcc / read full register:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

Esto tiene un rendimiento óptimo en todas las CPU (sin paradas, fusión de uops o dependencias falsas).

Las cosas son más complicadas cuando no quieres xor antes de una instrucción de configuración de bandera . por ejemplo, desea ramificar en una condición y luego establecer CC en otra condición desde las mismas banderas. por ejemplo,cmp/jle, sete, y o bien no tiene un registro de repuesto, o desea mantener el xor fuera de la ruta de código no tomada por completo.

No hay expresiones de puesta a cero reconocidas que no afecten a los indicadores, por lo que la mejor opción depende de la microarquitectura de destino. En Core2, insertar un uop de fusión podría causar un bloqueo de 2 o 3 ciclos. Parece ser más barato en SnB, pero no pasé mucho tiempo tratando de medir. Usando mov reg, 0 / setcc tendría una penalización significativa en CPU Intel más antiguas, y aún así ser algo peor con información nueva.

Utilizando setcc / movzx r32, r8 es probablemente la mejor alternativa para las familias Intel P6 y SnB, si no puede xor-cero antes de la instrucción de configuración de bandera. Eso debería ser mejor que repetir la prueba después de una reducción a cero de xor. (Ni siquiera considere sahf / lahf o pushf / popf). IvB puede eliminar movzx r32, r8 (es decir, manejarlo con cambio de nombre de registro sin unidad de ejecución o latencia, como xor-cero). Haswell y posteriores solo eliminan las instrucciones regulares mov , así que movzx toma una unidad de ejecución y tiene latencia distinta de cero, haciendo prueba/setcc/movzx peor que xor / test / setcc, pero al menos tan bueno como test/mov r,0/setcc (y mucho mejor en CPU más antiguas).

Utilizando setcc / movzx sin cero primero es malo en AMD / P4 / Silvermont, porque no rastrean deps por separado para sub-registros. Habría un dep falso en el valor antiguo del registro. Usando mov reg, 0/setcc para la reducción a cero / la ruptura de dependencias es probablemente la mejor alternativa cuando xor/test/setcc no es una opción.

Por supuesto, si no necesita que la salida de setcc sea más ancha que 8 bits, no necesita poner a cero nada. Sin embargo, tenga cuidado con las dependencias falsas en CPU que no sean P6 / SnB si elige un registro que recientemente formó parte de una larga cadena de dependencias. (Y tenga cuidado de causar una pérdida parcial de registro o uop adicional si llama a una función que podría guardar/restaurar el registro del que está utilizando parte.)


and con una inmediata zero no es un caso especial como independiente del valor antiguo en cualquier CPU que tenga conocimiento, por lo que no rompe las cadenas de dependencias. No tiene ventajas sobre xor, y muchas desventajas.

Véase http://agner.org/optimize / para la documentación de microarquitectura, incluyendo qué modismos de puesta a cero se reconocen como ruptura de dependencias (por ejemplo, sub same,same está en algunas pero no en todas las CPU, mientras que xor same,same se reconoce en todas.) mov rompe la cadena de dependencia del valor antiguo del registro (independientemente del valor de origen, cero o no, porque así es como funciona mov). xor solo rompe cadenas de dependencias en el caso especial donde src y dest son el mismo registro, por lo que mov se deja fuera de la lista de especialmente separadores de dependencias reconocidos. (También, porque no se reconoce como un idioma de cero, con los otros beneficios que conlleva.)

Curiosamente, el diseño P6 más antiguo (PPro) no reconoció -cero como un disyuntor de dependencias, solo como un modismo de cero con el propósito de evitar paradas de registro parcial, por lo que en algunos casos valió la pena usar ambos. (Véase el Ejemplo de Agner Fog 6.17. en su microarch pdf. Afirma esto también se aplica a P2, P3, e incluso (temprano?) PM, pero soy escéptico de eso. Un comentario en la entrada del blog enlazada dice que solo PPro tuvo este descuido. Parece muy poco probable que existieran varias generaciones de la familia P6 sin reconocer xor-cero como un interruptor dep.)


Si realmente hace su código más agradable o guarda instrucciones, entonces seguro, cero con mov para evitar tocar las banderas, siempre y cuando no introduzca un problema de rendimiento que no sea el tamaño del código. Sin embargo, evitar clobbering flags es la única razón sensata para no usar xor.

 168
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:41