assembly - pelicula - ¿Por qué debería alinearse el código a los límites de direcciones pares en x86?
assembly pelicula (3)
Debido a que el procesador (de 16 bits) puede obtener valores de la memoria solo en direcciones pares, debido a su diseño particular: está dividido en dos "bancos" de 1 byte cada uno, por lo que la mitad del bus de datos está conectado al primer banco y al otra mitad al otro banco. Ahora, supongamos que estos bancos están alineados (como en mi foto), el procesador puede recuperar valores que están en la misma "fila".
bank 1 bank 2
+--------+--------+
| 8 bit | 8 bit |
+--------+--------+
| | |
+--------+--------+
| 4 | 5 | <-- the CPU can fetch only values on the same "row"
+--------+--------+
| 2 | 3 |
+--------+--------+
| 0 | 1 |
+--------+--------+
/ / / /
| | | |
| | | |
data bus (to uP)
Ahora, debido a esta limitación de búsqueda, si la CPU se ve obligada a buscar valores que se encuentran en una dirección impar (suponga 3), tiene que buscar valores en 2 y 3, luego valores en 4 y 5, desechar los valores 2 y 5 luego únase a 4 y 3 (estamos hablando de x86, que como un pequeño diseño de memoria endian).
Es por eso que es mejor tener código (y datos) en direcciones pares.
PD: en los procesadores de 32 bits, el código y los datos deben alinearse en direcciones que son divisibles por 4 (ya que hay 4 bancos).
Espero que haya quedado claro. :)
Estoy trabajando en el "Lenguaje de ensamblaje para procesadores x86 de Kip Irvine , sexta edición" y realmente lo estoy disfrutando.
Acabo de leer sobre la mnemotécnica NOP en el siguiente párrafo:
"It [NOP] is sometimes used by compilers and assemblers to align code to
even-address boundaries."
El ejemplo dado es:
00000000 66 8B C3 mov ax, bx
00000003 90 nop
00000004 8B D1 mov edx, ecx
El libro dice entonces:
"x86 processors are designed to load code and data more quickly from even
doubleword addresses."
Mi pregunta es: ¿Es la razón por la que esto es así? Porque para los procesadores x86 a los que se refiere el libro (32 bits), el tamaño de palabra de la CPU es de 32 bits y, por lo tanto, puede extraer las instrucciones con el NOP y procesarlos en uno. ir? Si este es el caso, supongo que un procesador de 64 bits con un tamaño de palabra de una quadword haría esto con un hipotético 5 bytes de código más un nop?
Por último, después de escribir mi código, ¿debo revisar la alineación con los NOP para optimizarlo o el compilador (MASM, en mi caso), hará esto por mí, como parece implicar el texto?
Gracias,
Scott
El código que se ejecuta en los límites de palabra (para 8086) o DWORD (80386 y posteriores) se ejecuta más rápido porque el procesador obtiene palabras completas (D). Así que si sus instrucciones no están alineadas, entonces hay una parada al cargar.
Sin embargo, no puedes alinear cada instrucción. Bueno, supongo que podrías, pero entonces estarías desperdiciando espacio y el procesador tendría que ejecutar las instrucciones NOP, lo que eliminaría cualquier beneficio de rendimiento de alinear las instrucciones.
En la práctica, alinear el código en los límites de dword (o lo que sea) solo ayuda cuando la instrucción es el objetivo de una instrucción de bifurcación, y los compiladores generalmente alinearán la primera instrucción de una función, pero no alinearán los objetivos de bifurcación que también pueden alcanzar caer a través. Por ejemplo:
MyFunction:
cmp ax, bx
jnz NotEqual
; ... some code here
NotEqual:
; ... more stuff here
Un compilador que genera este código generalmente alineará MyFunction
porque es un objetivo de bifurcación (alcanzado por la call
), pero no alineará el NotEqual
porque al hacerlo insertaría instrucciones NOP
que tendrían que ejecutarse cuando no se NotEqual
. Eso aumenta el tamaño del código y hace que el caso de caída directa sea más lento.
Yo sugeriría que si solo estás aprendiendo lenguaje ensamblador, no te preocupes por cosas como esta que con más frecuencia te darán ganancias de rendimiento marginal. Solo escribe tu código para que las cosas funcionen. Después de que funcionen, puede crear un perfil y, si cree que es necesario, después de mirar los datos del perfil, alinear sus funciones.
El ensamblador normalmente no lo hará por ti automáticamente.
El problema no se limita solo a la obtención de instrucciones. Y es desafortunado que los programadores no sean conscientes de esto tan pronto y lo castiguen a menudo. La arquitectura x86 ha hecho a la gente perezosa. Esto dificulta la transición a otras arquitecturas.
Tiene todo que ver con la naturaleza del bus de datos. Cuando tiene, por ejemplo, un bus de datos de 32 bits de ancho, una lectura de la memoria se alinea en ese límite. En este caso, los dos bits de dirección inferiores normalmente se ignoran, ya que no tienen ningún significado. Entonces, si tuviera que realizar una lectura de 32 bits desde la dirección 0x02, ya sea parte de una búsqueda de instrucciones o una lectura de la memoria. Luego se requieren dos ciclos de memoria, una lectura de la dirección 0x00 para obtener dos de los bytes y una lectura de 0x04 para obtener los otros dos bytes. Tomando el doble de tiempo, deteniendo la tubería si se trata de una búsqueda de instrucciones. El impacto en el rendimiento es espectacular y de ninguna manera una optimización desperdiciada para lecturas de datos. Los programas que alinean sus datos en límites naturales y ajustan estructuras y otros elementos en múltiplos enteros de estos tamaños, pueden ver hasta el doble de rendimiento sin ningún otro esfuerzo. De manera similar, utilizar un int en lugar de un carácter para una variable, incluso si solo va a contar hasta 10, puede ser más rápido. Es cierto que, por lo general, no vale la pena agregar nops a los programas para alinear los destinos de las sucursales. Desafortunadamente, x86 es una longitud de palabra variable, basada en bytes, y constantemente sufres estas ineficiencias. Si está pintado en una esquina y necesita sacar algunos relojes más de un bucle, no solo debe alinearse en un límite que coincida con el tamaño del bus (en estos días 32 o 64 bits) sino también en un límite de línea de caché, y trate de mantener ese bucle dentro de una o tal vez dos líneas de caché. En esa nota, un solo nop aleatorio en un programa puede causar cambios donde las líneas de caché impactan y se puede detectar un cambio en el rendimiento si el programa es lo suficientemente grande y tiene suficientes funciones o ciclos. La misma historia, digamos, por ejemplo, que tiene un destino de bifurcación en la dirección 0xFFFC, si no está en la memoria caché se debe buscar una línea de caché, nada inesperado, pero una o dos instrucciones más tarde (cuatro bytes) se requiere otra línea de memoria caché. Si el objetivo hubiera sido 0x10000, dependiendo del tamaño de su función, naturalmente, podría haberlo logrado en una línea de caché. Si esta es una función a menudo llamada y otra función a menudo se encuentra en una dirección lo suficientemente similar para que estos dos se desalojen entre sí, se ejecutará el doble de lento. Este es un lugar donde el x86 ayuda, aunque con una longitud de instrucción variable, puede empaquetar más código en una línea de caché que en otras arquitecturas bien usadas.
Con x86 y las instrucciones no se puede ganar. En este punto, a menudo es inútil intentar sintonizar manualmente los programas x86 (desde una perspectiva de instrucción). Con la cantidad de núcleos diferentes y sus matices, puede obtener ganancias en un procesador en una computadora un día, pero ese mismo código hará que otros procesadores x86 en otras computadoras funcionen más lentamente, a veces menos de la mitad de la velocidad. Es mejor ser genéricamente eficiente pero tener un poco de descuido para que funcione correctamente en todas las computadoras todos los días. La alineación de datos mostrará mejoras en los procesadores de las computadoras, pero la alineación de instrucciones no.