assembly x86 micro-optimization

assembly - ¿El uso de xor reg, reg da ventaja sobre mov reg, 0?



x86 micro-optimization (6)

Como otros han notado, la respuesta es "¿a quién le importa?". ¿Estás escribiendo un compilador?

Y en una segunda nota, su evaluación comparativa probablemente no funcionará, ya que tiene una sucursal allí que probablemente tome todo el tiempo de todos modos. (a menos que su compilador desenrolle el loop por usted)

Otra razón por la que no puede comparar una única instrucción en un bucle es que todo su código se almacenará en caché (a diferencia del código real). Así que has tomado gran parte de la diferencia de tamaño entre mov eax, 0 y xor eax, eax fuera de la imagen al tenerlo en L1-caché todo el tiempo.

Mi suposición es que cualquier diferencia de rendimiento mensurable en el mundo real se debería a la diferencia de tamaño que consume la memoria caché, y no al tiempo de ejecución de las dos opciones.

Hay dos formas bien conocidas de establecer un registro de enteros a cero en x86.

Ya sea

mov reg, 0

o

xor reg, reg

Existe la opinión de que la segunda variante es mejor ya que el valor 0 no está almacenado en el código y eso ahorra varios bytes de código máquina producido. Esto es definitivamente bueno, se usa menos caché de instrucciones y esto a veces permite una ejecución más rápida del código. Muchos compiladores producen dicho código.

Sin embargo, existe formalmente una dependencia entre instrucciones entre la instrucción xor y cualquier instrucción anterior que cambie el mismo registro. Como hay una dependencia, la última instrucción debe esperar hasta que la anterior finalice y esto podría reducir la carga de las unidades de procesamiento y perjudicar el rendimiento.

add reg, 17 ;do something else with reg here xor reg, reg

Es obvio que el resultado de xor será exactamente el mismo independientemente del valor de registro inicial. ¿Pero el procesador puede reconocer esto?

Intenté la siguiente prueba en VC ++ 7:

const int Count = 10 * 1000 * 1000 * 1000; int _tmain(int argc, _TCHAR* argv[]) { int i; DWORD start = GetTickCount(); for( i = 0; i < Count ; i++ ) { __asm { mov eax, 10 xor eax, eax }; } DWORD diff = GetTickCount() - start; start = GetTickCount(); for( i = 0; i < Count ; i++ ) { __asm { mov eax, 10 mov eax, 0 }; } diff = GetTickCount() - start; return 0; }

Con las optimizaciones de ambos bucles toman exactamente el mismo tiempo. ¿Esto prueba razonablemente que el procesador reconoce que no hay dependencia de xor reg, reg instruction en la instrucción anterior de mov eax, 0 ? ¿Cuál podría ser una prueba mejor para verificar esto?


Creo que en las arquitecturas anteriores las instrucciones mov eax, 0 solían tomar un poco más de tiempo que xor eax, eax también ... no puedo recordar exactamente por qué. A menos que tenga muchos más mov , sin embargo, me imagino que no es probable que cause fallas en la memoria caché debido a ese literal almacenado en el código.

También tenga en cuenta que, desde la memoria, el estado de las banderas no es idéntico entre estos métodos, pero puedo estar recordando mal esto.


Dejé de poder arreglar mis propios autos después de que vendí mi camioneta HR de 1966. Estoy en una situación similar con las CPU modernas :-)

Realmente dependerá del microcódigo o circuito subyacente. Es muy posible que la CPU pueda reconocer "XOR Rn,Rn" y simplemente poner a cero todos los bits sin preocuparse por el contenido. Pero, por supuesto, puede hacer lo mismo con un "MOV Rn, 0" . Un buen compilador elegirá la mejor variante para la plataforma de destino de todos modos, por lo que esto suele ser solo un problema si está codificando en ensamblador.

Si la CPU es lo suficientemente inteligente, su dependencia XOR desaparece, ya que sabe que el valor es irrelevante y lo establecerá en cero de todos modos (una vez más, esto depende de la CPU real utilizada).

Sin embargo, hace mucho que no me interesan unos pocos bytes o unos pocos ciclos de reloj en mi código, esto parece una micro-optimización enloquecida.


En las CPU modernas, se prefiere el patrón XOR. Es más pequeño y más rápido.

Más pequeño realmente importa porque en muchas cargas de trabajo reales uno de los principales factores que limitan el rendimiento es i-caché falla. Esto no se capturaría en un micro-benchmark que compara las dos opciones, pero en el mundo real hará que el código se ejecute un poco más rápido.

Y, ignorando los errores de i-cache reducidos, XOR en cualquier CPU en los últimos años es la misma velocidad o más rápido que MOV. ¿Qué podría ser más rápido que ejecutar una instrucción MOV? ¡No está ejecutando ninguna instrucción en absoluto! En los procesadores Intel recientes, la lógica de envío / cambio de nombre reconoce el patrón XOR, ''se da cuenta'' de que el resultado será cero y simplemente señala el registro en un registro cero físico. Luego descarta la instrucción porque no hay necesidad de ejecutarla.

El resultado neto es que el patrón XOR utiliza cero recursos de ejecución y puede, en las CPU recientes de Intel, ''ejecutar'' cuatro instrucciones por ciclo. MOV supera las tres instrucciones por ciclo.

Para detalles, vea esta publicación de blog que escribí:

https://randomascii.wordpress.com/2012/12/29/the-surprising-subtleties-of-zeroing-a-register/

La mayoría de los programadores no deben preocuparse por esto, pero los escritores de compiladores tienen que preocuparse, y es bueno entender el código que se está generando, ¡y es genial!



x86 tiene instrucciones de longitud variable. MOV EAX, 0 requiere uno o dos bytes más en el espacio de código que XOR EAX, EAX.