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
.