performance assembly optimization x86 micro-optimization

performance - ¿Cuál es la mejor manera de establecer un registro a cero en el ensamblado x86: xor, mov o y?



assembly optimization (1)

Todas las siguientes instrucciones hacen lo mismo: establezca %eax en cero. ¿Qué forma es óptima (requiere menos ciclos de máquina)?

xorl %eax, %eax mov $0, %eax andl $0, %eax


TL; Resumen DR : 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. Intel y AMD lo recomiendan oficialmente. En el modo de 64 bits, todavía use xor r32, r32 , porque escribir un registro de 32 bits pone a cero los 32 superiores . xor r64, r64 es un desperdicio de un byte, porque necesita un prefijo REX.

Peor aún que eso, Silvermont solo reconoce xor r32,r32 como desgarrador, no un tamaño de operando de 64 bits. Por lo tanto, incluso cuando todavía se requiere un prefijo REX porque está poniendo a cero r8..r15, use xor r10d,r10d , no xor r10,r10 .

Ejemplos:

xor eax, eax ; RAX = 0 xor r10d, r10d ; R10 = 0 xor edx, edx ; RDX = 0 ; small code-size alternative: cdq ; zero RDX if EAX is already zero ; SUB-OPTIMAL xor rax,rax ; waste of a REX prefix, and extra slow on Silvermont mov eax, 0 ; doesn''t touch FLAGS, but not faster and takes more bytes

Poner a cero un registro vectorial generalmente se realiza mejor con pxor xmm, xmm . Eso es típicamente lo que hace gcc (incluso antes de usarlo con las 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 entero y FP generalmente no es relevante, porque la ejecución fuera de orden generalmente puede ocultarlo al comienzo de una nueva cadena de dependencia).

En las microarquitecturas de la familia SnB, ninguno de los sabores de xor-zeroing necesita un puerto de ejecución. En AMD y Intel anterior a Nehalem P6 / Core2, xorps y pxor se manejan de la misma manera (como instrucciones de enteros vectoriales).

El uso de la versión AVX de una instrucción vectorial de 128b también vpxor xmm, xmm, xmm cero la parte superior del registro, por lo que vpxor xmm, xmm, xmm es una buena opción para poner a cero YMM (AVX1 / AVX2) o ZMM (AVX512), o cualquier extensión de vector futura. vpxor ymm, ymm, ymm no necesita bytes adicionales para codificar y se ejecuta de la misma manera. La reducción a cero AVX512 ZMM requeriría bytes adicionales (para el prefijo EVEX), por lo que debería preferirse la reducción a cero XMM o YMM.

Algunas CPU reconocen sub same,same que un idioma de xor cero como xor , pero todas las CPU que reconocen cualquier idioma de xor cero reconocen xor . Simplemente use xor para que no tenga que preocuparse sobre qué CPU reconoce qué idioma de puesta a cero.

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

  • tamaño de código más pequeño que mov reg,0 . (Todas las CPU)
  • evita penalizaciones por registro parcial para código posterior. (Familia Intel P6 y familia SnB).
  • no usa 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 que le presten instrucciones cercanas si es necesario. (Familia Intel SnB).
  • no utiliza entradas en el archivo de registro físico . (Intel SnB-family (y P4) al menos, posiblemente también AMD ya que usan un diseño PRF similar en lugar de mantener el estado de registro en el ROB como las microarquitecturas de la familia Intel P6).

Un tamaño de código de 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 mejor captura de instrucciones y potencialmente decodificación de 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 más tarde tienen 4 puertos de ejecución que pueden manejar instrucciones enteras de ALU, incluidos mov r32, imm32 , por lo que con una toma de decisiones perfecta por parte del planificador (lo 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 en otra pregunta sobre la reducción a cero de registros para obtener más detalles.

article 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 uno uop en el dominio fusionado. Las CPU Intel modernas pueden emitir y retirar 4 uops de dominio fusionado por reloj. De ahí proviene el límite de 4 ceros por reloj. La mayor complejidad del hardware de cambio de nombre del registro es solo una de las razones para limitar el ancho del diseño a 4. (Bruce ha escrito algunas publicaciones de blog muy excelentes, como su serie sobre cuestiones de matemáticas FP y x87 / SSE / redondeo , lo que hago altamente recomendado).

En las CPU de la familia AMD Bulldozer , mov immediate ejecuta mov immediate en los mismos puertos de ejecución de enteros EX0 / EX1 que xor . mov reg,reg también puede ejecutarse en AGU0 / 1, pero eso es solo para la copia de registros, no para la configuración inmediata. Entonces, AFAIK, en AMD, la única ventaja de 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.

Los modismos de puesta a cero reconocidos evitan las penalizaciones de registro parcial en las CPU Intel que cambian el nombre de los registros parciales por separado de los registros completos (familias P6 y SnB).

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

De la guía de microarquitectura de Agner Fog, página 98 (sección Pentium M, referenciada en secciones posteriores que incluyen SnB):

El procesador reconoce el XOR de un registro consigo mismo y lo establece en cero. Una etiqueta especial en el registro recuerda que la parte alta del registro es cero, de modo que EAX = AL. Esta etiqueta se recuerda 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 que no se produzca una interrupción, predicción errónea u otro evento de serialización.

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

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

Hubiera sido bueno si x86-64 reutilizara uno de los códigos de setcc r/m eliminados (como AAM) para un setcc setcc r/m 16/32/64 bits, con el predicado codificado en el campo de 3 bits del registro de origen de r / m campo (la forma en que otras instrucciones de un solo operando los usan como bits de código de operación). Pero no hicieron eso, y eso no ayudaría para x86-32 de todos modos.

Idealmente, debe 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 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 ramificarse en una condición y luego establecer cc en otra condición desde los mismos indicadores. por ejemplo, cmp/jle , sete , y usted no tiene un registro de reserva, o desea mantener el xor fuera de la ruta de código no tomada por completo.

No hay modismos de reducción a cero reconocidos que no afecten a las banderas, por lo que la mejor opción depende de la microarquitectura objetivo. En Core2, la inserción de una uop de fusión puede provocar 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 las CPU Intel más antiguas, y aún sería algo peor en las nuevas Intel.

Usando setcc / movzx r32, r8 es probablemente la mejor alternativa para las familias Intel P6 y SnB, si no puede hacer xor-zero antes de las instrucciones de configuración del indicador. Eso debería ser mejor que repetir la prueba después de una reducción a cero. (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-zeroing). Haswell y luego solo eliminan las instrucciones regulares de mov , por lo que movzx toma una unidad de ejecución y tiene una latencia distinta de cero, lo que hace que test / setcc / movzx peor que xor / test / setcc , pero al menos tan bueno como test / mov r,0 / setcc (y mucho mejor en CPU antiguas).

Usar setcc / movzx sin movzx a cero primero es malo en AMD / P4 / Silvermont, porque no rastrean los departamentos por separado para los sub-registros. Habría una falsa dep en el antiguo valor del registro. Usando mov reg, 0 / setcc para la reducción a cero / dependencia 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 falsas dependencias en CPU que no sean P6 / SnB si elige un registro que recientemente fue parte de una larga cadena de dependencias. (Y tenga cuidado de causar un bloqueo parcial del registro o una subida adicional si llama a una función que podría guardar / restaurar el registro del que está utilizando parte).

and con un cero inmediato no está en mayúsculas especiales como independiente del valor anterior en cualquier CPU que conozca, por lo que no rompe las cadenas de dependencia. No tiene ventajas sobre xor , y muchas desventajas.

Consulte http://agner.org/optimize/ para ver la documentación de microarchivos, incluido qué modismos de reducción a cero se reconocen como ruptura de dependencia (por ejemplo, sub same,same está en algunas CPU pero no todas, mientras que xor same,same se reconoce en todos). mov rompe la cadena de dependencia del valor anterior del registro (independientemente del valor de origen, cero o no, porque así es como funciona mov ). xor solo rompe las cadenas de dependencia en el caso especial donde src y dest son el mismo registro, razón por la cual mov se queda fuera de la lista de interruptores de dependencia especialmente reconocidos. (Además, porque no se reconoce como un idioma de reducción a cero, con los otros beneficios que conlleva).

Curiosamente, el diseño P6 más antiguo (PPro a través de Pentium III) no reconoció el xor cero como un interruptor de dependencia, solo como un idioma de puesta a cero con el fin de evitar puestos de registro parcial, por lo que en algunos casos valió la pena usar ambos . (Vea el ejemplo 6.17 de Agner Fog. En su microarchivo pdf. Dice que esto también se aplica a P2, P3 e incluso PM (temprano). Un comentario en la publicación del blog vinculado dice que solo PPro tuvo esta supervisión, pero yo '' probé en Katmai PIII y @Fanael probó en un Pentium M, y ambos descubrimos que no rompió la dependencia de una cadena imul unida a la latencia).

Si realmente hace que su código sea más agradable o guarde las instrucciones, entonces asegúrese de cero con mov para evitar tocar las banderas, siempre que no presente un problema de rendimiento que no sea el tamaño del código. Sin embargo, evitar las banderas de golpeteo es la única razón sensata para no usar xor .