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

Respuestas:

222

TL; Resumen de DR : xor same, samees 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. Es recomendado oficialmente por Intel y AMD, y lo que hacen los compiladores. En modo de 64 bits, siga utilizándolo xor r32, r32, porque escribir un registro de 32 bits pone ceros a los 32 superiores . xor r64, r64es una pérdida de un byte, porque necesita un prefijo REX.

Incluso peor que eso, Silvermont solo reconoce xor r32,r32como de ruptura de depósito, no de tamaño de operando de 64 bits. Por lo tanto, incluso cuando se requiera un prefijo REX porque está poniendo a cero r8..r15, use xor r10d,r10d, notxor r10,r10 .

Ejemplos de GP-integer:

xor   eax, eax       ; RAX = 0.  Including AL=0 etc.
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
xor   r10,r10       ; bad on Silvermont (not dep breaking), same as r10d everywhere else because a REX prefix is still needed for r10d or r10.
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes
 and   eax, 0        ; false dependency.  (Microbenchmark experiments might want this)
 sub   eax, eax      ; same as xor on most but not all CPUs; bad on Silvermont for example.

xor   al, al        ; false dep on some CPUs, not a zeroing idiom.  Use xor eax,eax
mov   al, 0         ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified

Por lo general, es mejor poner a cero un registro vectorial pxor xmm, xmm. Eso es típicamente lo que hace gcc (incluso antes de usarlo con instrucciones FP).

xorps xmm, xmmpuede tener sentido. Es un byte más corto que pxor, pero xorpsnecesita el puerto de ejecución 5 en Intel Nehalem, mientras que pxorpuede 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 ocultarla al comienzo de una nueva cadena de dependencia).

En las microarquitecturas de la familia SnB, ninguna versión de xor-zeroing necesita siquiera un puerto de ejecución. En AMD y pre-Nehalem P6 / Core2 Intel, xorpsy pxorse manejan de la misma manera (como instrucciones de vectores enteros).

El uso de la versión AVX de una instrucción vectorial de 128b también pone a cero la parte superior del registro, por lo que vpxor xmm, xmm, xmmes una buena opción para poner a cero YMM (AVX1 / AVX2) o ZMM (AVX512), o cualquier extensión de vector futura. vpxor ymm, ymm, ymmSin embargo, no se necesitan bytes adicionales para codificar y funciona igual en Intel, pero más lento en AMD antes de Zen2 (2 uops). La puesta a cero de AVX512 ZMM requeriría bytes adicionales (para el prefijo EVEX), por lo que debería preferirse la puesta a cero de XMM o YMM.

Ejemplos de XMM / YMM / ZMM

    # Good:
 xorps   xmm0, xmm0         ; smallest code size (for non-AVX)
 pxor    xmm0, xmm0         ; costs an extra byte, runs on any port on Nehalem.
 xorps   xmm15, xmm15       ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX.  Code-size is the only penalty.

   # Good with AVX:
 vpxor xmm0, xmm0, xmm0    ; zeros X/Y/ZMM0
 vpxor xmm15, xmm0, xmm0   ; zeros X/Y/ZMM15, still only 2-byte VEX prefix

#sub-optimal AVX
 vpxor xmm15, xmm15, xmm15  ; 3-byte VEX prefix because of high source reg
 vpxor ymm0, ymm0, ymm0     ; decodes to 2 uops on AMD before Zen2


    # Good with AVX512
 vpxor  xmm15,  xmm0, xmm0     ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
 vpxord xmm30, xmm30, xmm30    ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD.  May be worth using only high regs to avoid needing vzeroupper in short functions.
    # Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
 vpxord zmm30, zmm30, zmm30    ; Without AVX512VL you have to use a 512-bit instruction.

# sub-optimal with AVX512 (even without AVX512VL)
 vpxord  zmm0, zmm0, zmm0      ; EVEX prefix (4 bytes), and a 512-bit uop.  Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.

Consulte ¿La puesta a cero de vxorps en AMD Jaguar / Bulldozer / Zen es más rápida con registros xmm que ymm? y
¿Cuál es la forma más eficaz de borrar uno o varios registros ZMM en Knights Landing?

Semi-relacionado: La forma más rápida de establecer el valor __m256 en todos los bits UNO y
Establecer todos los bits en el registro de la CPU en 1 de manera eficiente también cubre los registros de k0..7máscara AVX512 . SSE / AVX vpcmpeqdestá rompiendo las depuraciones en muchos (aunque todavía necesita un uop para escribir los 1), pero AVX512 vpternlogdpara los registros de ZMM ni siquiera es una depuradora. Dentro de un bucle, considere copiar de otro registro en lugar de volver a crearlos con un uop ALU, especialmente con AVX512.

Pero poner a cero es barato: xor-poner a cero un registro xmm dentro de un bucle suele ser tan bueno como copiar, excepto en algunas CPU AMD (Bulldozer y Zen) que tienen eliminación de mov para registros vectoriales pero aún necesitan un uop de ALU para escribir ceros para xor -poner a cero.


¿Qué tiene de especial poner a cero modismos como xor en varios uarches?

Algunas CPU reconocen sub same,samecomo un lenguaje de puesta a cero como xor, pero todas las CPU que reconocen cualquier lenguaje de puesta a cero reconocenxor . Solo utilícelo xorpara no tener que preocuparse por qué CPU reconoce qué idioma de puesta a cero.

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

  • tamaño de código más pequeño que mov reg,0. (Todas las CPU)
  • evita penalizaciones de registro parcial para código posterior. (Familia Intel P6 y familia SnB).
  • no utiliza una unidad de ejecución, lo que ahorra energía y libera 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 las instrucciones cercanas puedan pedir prestado si es necesario. (Familia Intel SnB).
  • no utiliza entradas en el archivo de registro físico . (Familia Intel SnB (y P4) al menos, posiblemente AMD también, 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 densidad de código más alta conduce a menos pérdidas de caché de instrucciones, y una mejor captura 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 versiones posteriores tienen 4 puertos de ejecución que pueden manejar instrucciones ALU enteras, incluyendo mov r32, imm32, por lo que con una toma de decisiones perfecta por parte del programador (lo que no siempre sucede en la práctica), HSW aún podría sostener 4 uops por reloj incluso cuando todos necesitan ALU puertos de ejecución.

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

La publicación del blog de Bruce Dawson que Michael Petch vinculó (en un comentario sobre la pregunta) señala que xorse maneja en la etapa de registro y cambio de nombre 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 modernas de Intel pueden emitir y retirar 4 uops de dominio fusionado por reloj. De ahí viene el límite de 4 ceros por reloj. La mayor complejidad del hardware de cambio de nombre de 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 matemáticas FP y problemas de redondeo x87 / SSE , que yo hago altamente recomendado).


En las CPU de la familia AMD Bulldozer , se mov immediateejecuta en los mismos puertos de ejecución de enteros EX0 / EX1 que xor. mov reg,regtambién se puede ejecutar en AGU0 / 1, pero eso es solo para copia de registro, no para configuración desde inmediatos. Así que yo sepa, en la única ventaja de AMD a xorlo largo moves 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 penalizaciones por registros parciales en las CPU Intel que cambian el nombre de los registros parciales por separado de los registros completos (familias P6 y SnB).

xorse etiquetar el registro como teniendo las partes superior a cero , de modo xor eax, eax/ inc al/ inc eaxevita la pena de-registro parcial usual que pre-IVB CPUs tiene. Incluso sin xor, IvB solo necesita un uop de fusión cuando AHse modifican los 8 bits ( ) altos y luego se lee todo el registro, y Haswell incluso lo elimina.

De la guía de microarquía de Agner Fog, pág. 98 (sección Pentium M, a la que se hace referencia en secciones posteriores, incluido SnB):

El procesador reconoce el XOR de un registro consigo mismo al establecerlo 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 guía que también confirma que mov reg, 0se no se reconoce como un lenguaje de puesta a cero, al menos en P6 principios de diseños como PIII o PM. Me sorprendería mucho si gastaran transistores en detectarlo en CPU posteriores.


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

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

Idealmente, debería 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, fusiones o falsas dependencias).

Las cosas son más complicadas cuando no desea xor antes de una instrucción de colocación de banderas . por ejemplo, quiere bifurcarse en una condición y luego establecer cc en otra condición desde los mismos indicadores. por ejemplo cmp/jle, setey que o bien no tienen un registro de repuesto, o si desea mantener el xorfuera de la ruta de código no-tomado por completo.

No existen modismos de puesta a cero reconocidos que no afecten a las banderas, por lo que la mejor opción depende de la microarquitectura de destino. En Core2, la inserción de un uop combinado puede provocar un bloqueo de 2 o 3 ciclos. Parece ser más barato en SnB, pero no pasé mucho tiempo tratando de medir. El uso de mov reg, 0/ setcctendría una penalización significativa en las CPU Intel más antiguas y aún sería algo peor en las Intel más nuevas.

Usar setcc/ movzx r32, r8es probablemente la mejor alternativa para las familias Intel P6 y SnB, si no puede xor-zero antes de la instrucción de configuración de banderas. Eso debería ser mejor que repetir la prueba después de un xor-zeroing. (Ni siquiera consideres sahf/ lahfo 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 versiones posteriores solo eliminan las movinstrucciones regulares , por lo que movzxtoma una unidad de ejecución y tiene una latencia distinta de cero, lo que hace que la prueba / setcc/ sea movzxpeor que xor/ prueba / setcc, pero al menos tan buena como la prueba / mov r,0/ setcc(y mucho mejor en las CPU más antiguas).

Usar setcc/ movzxsin poner a cero primero es malo en AMD / P4 / Silvermont, porque no rastrean los departamentos por separado para los subregistros. Habría un depósito falso sobre el valor anterior del registro. Usar mov reg, 0/ setccpara poner a cero / romper dependencias es probablemente la mejor alternativa cuando xor/ test / setccno es una opción.

Por supuesto, si no necesita que setccla salida 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 fue parte de una cadena de dependencia larga. (Y tenga cuidado de causar un bloqueo parcial del registro o un uop adicional si llama a una función que podría guardar / restaurar el registro del que está usando parte).


andcon un cero inmediato no tiene una carcasa especial como independiente del valor anterior en cualquier CPU que conozca, por lo que no rompe las cadenas de dependencia. No tiene ventajas xory muchas desventajas.

Es útil solo para escribir microbenchmarks cuando desea una dependencia como parte de una prueba de latencia, pero desea crear un valor conocido reduciendo a cero y agregando.


Ver http://agner.org/optimize/ para más detalles microarch , incluyendo el que los modismos de puesta a cero se reconocen como romper la dependencia (por ejemplo, sub same,samees en algunas pero no todas las CPU, mientras que xor same,samese reconoce en absoluto.) movHace romper la cadena de dependencia en el valor de edad del registro (independientemente del valor de la fuente, cero o no, porque así es como movfunciona). xorsolo rompe las cadenas de dependencia en el caso especial donde src y dest son el mismo registro, razón por la cual movse deja fuera de la lista de interruptores de dependencia especialmente reconocidos. (Además, porque no se reconoce como un modismo de reducción a cero, con los otros beneficios que conlleva).

Curiosamente, el diseño más antiguo de P6 (PPro a Pentium III) no reconoció xor-zeroing como un interruptor de dependencia, solo como un lenguaje de puesta a cero con el propósito de evitar paradas de registro parcial , por lo que en algunos casos valió la pena usar ambos mov y luego xor-poner a cero en ese orden para romper el dep y luego a cero de nuevo + establecer el bit de etiqueta interno que los bits altos son cero para EAX = AX = AL.

Véase el ejemplo 6.17 de Agner Fog. en su pdf de microarch. Él dice que esto también se aplica a P2, P3 e incluso (¿temprano?) PM. Un comentario en la publicación del blog vinculada 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 rompía una dependencia para una latencia. -cadena enlazada imul. Esto confirma los resultados de Agner Fog, desafortunadamente.


TL: DR:

Si realmente hace que su código sea más agradable o guarda instrucciones, entonces claro, cero con movpara evitar tocar las banderas, siempre y cuando no introduzca un problema de rendimiento que no sea el tamaño del código. Evitar las banderas que golpean es la única razón sensata para no usar xor, pero a veces puede xor-zero antes de lo que establece las banderas si tiene un registro de repuesto.

mov-zero antes de setcces mejor para la latencia que movzx reg32, reg8después (excepto en Intel cuando puede elegir diferentes registros), pero peor tamaño de código.

Peter Cordes
fuente
7
La mayoría de las instrucciones aritméticas OP R, S son forzadas por una CPU fuera de orden a esperar a que el contenido del registro R sea llenado por instrucciones previas con el registro R como objetivo; esta es una dependencia de datos. El punto clave es que los chips Intel / AMD tienen hardware especial para romper las dependencias de espera de datos en el registro R cuando se encuentra XOR R, R, y no necesariamente lo hace para otras instrucciones de puesta a cero de registros. Esto significa que la instrucción XOR se puede programar para su ejecución inmediata, y es por eso que Intel / AMD recomienda usarla.
Ira Baxter
3
@IraBaxter: Sí, y solo para evitar confusiones (porque he visto este concepto erróneo sobre SO), mov reg, srctambién rompe las cadenas de depuración para las CPU OO (independientemente de que src sea imm32 [mem], u otro registro). Esta ruptura de dependencias no se menciona en los manuales de optimización porque no es un caso especial que solo ocurre cuando src y dest son el mismo registro. Es siempre sucede por las instrucciones que no dependen de su dest. (excepto por la implementación de Intel de popcnt/lzcnt/tzcnttener un depósito falso en el destino)
Peter Cordes
2
@Zboson: La "latencia" de una instrucción sin dependencias solo importa si había una burbuja en la tubería. Es bueno para la eliminación de mov, pero para las instrucciones de puesta a cero, el beneficio de latencia cero solo entra en juego después de algo como un error de predicción de rama o I $ miss, donde la ejecución está esperando las instrucciones decodificadas, en lugar de que los datos estén listos. Pero sí, mov-elimination no es movgratis, solo latencia cero. La parte de "no tomar un puerto de ejecución" generalmente no es importante. El rendimiento del dominio fusionado puede ser fácilmente el cuello de botella, especialmente. con cargas o almacenes en la mezcla.
Peter Cordes
2
Según Agner, KNL no reconoce la independencia de los registros de 64 bits. Entonces xor r64, r64no solo desperdicia un byte. Como dices xor r32, r32es la mejor opción especialmente con KNL. Consulte la sección 15.7 "Casos especiales de independencia" en este manual de microarca si desea leer más.
Bosón Z
3
Ah, ¿dónde está el viejo MIPS, con su "registro cero" cuando lo necesita?
hayalci