2011-02-05 14 views
17

Estoy trabajando a través del de Kip Irvine "Assembly Language for x86 Processors, sexta edición" y estoy realmente disfrutando.¿Por qué el código debe estar alineado con los límites de direcciones pares en x86?

acabo de leer sobre la tecla de acceso NOP en el párrafo siguiente:

"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 

Luego, el libro afirma:

"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í es porque para los procesadores x86 a los que se refiere el libro (32 bits), el tamaño de palabra de la CPU tiene 32 bits y, por lo tanto, puede extraer las instrucciones con el NOP y procesarlas de una vez. Si este es el caso, supongo que un procesador de 64 bits con un tamaño de palabra de un 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 y corregir la alineación con NOP para optimizarlo, o el compilador (MASM, en mi caso) hará esto por mí, como parece sugerir el texto?

Gracias,

de Scott

+7

Todo lo que desea saber sobre la arquitectura de los procesadores modernos está en http://www.agner.org/optimize/. La alineación requerida para las instrucciones es independiente del tamaño de la palabra, y tiene 16 bytes para los procesadores Intel modernos.No quiero arruinar tu diversión, pero no debes confiar en un libro que hace afirmaciones generales sobre el rendimiento de los "procesadores x86". Cada modelo individual tiene diferentes características. –

+0

¡Gracias por tu comentario! No has arruinado mi diversión. ¡La alegría está en el aprendizaje y acabo de aprender algo más de ti! Verificará el sitio web también. –

+0

Este libro se ve horriblemente desactualizado. 16bit x86 es realmente antiguo, TBH. No veo el valor de enseñar esto incluso con fines educativos. Tal vez como un ejemplo de contador, como _no_diseñar un lenguaje de procesador/ensamblador. – hirschhornsalz

Respuesta

17

código que se ejecuta en la palabra (para 8086) o DWORD (80386 y posteriores) límites ejecuta más rápido debido a que el procesador va a buscar palabras enteras (D). Entonces, si sus instrucciones no están alineadas, entonces hay un bloqueo al cargar.

Sin embargo, no puede dword-alinear cada instrucción. Bueno, supongo que podrías, pero estarías desperdiciando espacio y el procesador tendría que ejecutar las instrucciones NOP, lo que mataría cualquier beneficio de rendimiento de alinear las instrucciones.

En la práctica, alinear código en límites dword (o lo que sea) solo ayuda cuando la instrucción es el objetivo de una instrucción de bifurcación y los compiladores alinearán típicamente la primera instrucción de una función, pero no alinearán los destinos de bifurcación que puedan también ser alcanzado por el otoño. Por ejemplo:

MyFunction: 
    cmp ax, bx 
    jnz NotEqual 
    ; ... some code here 
NotEqual: 
    ; ... more stuff here 

un compilador que genera el código normalmente se alineará MyFunction porque es un objeto de bifurcación (alcanzado por call), pero no va a alinear el NotEqual porque hacerlo sería insertar NOP instrucciones que tendría para ser ejecutado cuando se cae. Eso aumenta el tamaño del código y hace que el caso de caída sea más lento.

Sugeriría que si recién está aprendiendo el lenguaje ensamblador, que no se preocupe por cosas como esta que con mayor frecuencia le darán ganancias de rendimiento marginales. Solo escribe tu código para que las cosas funcionen. Después de que funcionen, puedes crear un perfil y, si crees que es necesario después de consultar los datos del perfil, alinea tus funciones.

El ensamblador normalmente no lo hará automáticamente.

+0

¡Gracias por su respuesta! Sí, estoy de acuerdo: me limitaré a lo básico por ahora, pero no pude evitar pensar en la optimización. Cosas fascinantes! –

+0

en general, una respuesta excelente. La creación de perfiles en el ensamblaje no siempre es relevante porque, si tiene que recurrir a ella, probablemente haya perfilado algún código C o C++ y, en primer lugar, haya encontrado las cosas que deben abordarse para llegar al ensamblaje. Lo que puede (y debería) hacer para verificar un código que está lejos de ser completo pero cuyo nivel básico de desempeño necesita ser cuantificado es medir el tiempo del código en cuestión usando la instrucción rdtsc (ReaD Time Stamp Counter) antes y después y calcular la diferencia . Esto solo está disponible desde Pentium MMX en adelante y en modo de 32 bits. –

+1

@Scott Davies: No está mal pensar en la optimización cuando se está programando en ensamblaje. Lo más probable es que lo esté haciendo, ya que es posible que desee una optimización. Pero tenga en cuenta que estos consejos de optimización proporcionados en este libro fueron ciertos hace unos 25 años, pero ahora están obsoletos o incluso son incorrectos. Realmente no desea rellenar sus instrucciones con nops para que permanezcan en una dirección pareja en un procesador moderno, incluso si se ejecutara en el modo de 16 bits. Si quieres leer algunas cosas fascinantes, que de hecho tienen algún uso, realmente recomiendo los manuales de optimización en agner.org – hirschhornsalz

4

Porque el procesador (16 bits) puede recuperar valores de la memoria solo en direcciones pares, debido a su diseño particular: se divide en dos "bancos" de 1 byte cada uno, por lo que la mitad del bus de datos se conecta al primer banco y la otra mitad al otro banco. Ahora, supongamos que estos bancos están alineados (como en mi imagen), el procesador puede obtener 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, ya que este traiga limitación, si la CPU se ve obligado a buscar valores que se encuentran en una dirección impar (suponer 3), que tiene a buscar valores a los 2 y 3, entonces los valores a las 4 y 5 , deseche los valores 2 y 5, luego únase a 4 y 3 (está hablando de x86, que es como una pequeña distribución de memoria endia).
Es por eso que es mejor tener código (¡y datos!) En direcciones pares.

PD: En procesadores de 32 bits, el código y los datos se deben alinear en direcciones que son divisibles por 4 (ya que hay 4 bancos).

Espero que estuviera claro. :)

+0

"luego valores a 4 y 5, descarta los valores 2 y 5, luego une 4 y 3" ¿puedes elaborar esto? ¿Por favor? –

+0

@ user1218927 supongamos que desea cargar la palabra hecha de los bytes 3 y 4. La CPU carga la palabra en la dirección 2 (primer acceso a la memoria) y la palabra en la dirección 4 (segundo acceso a la memoria); los bytes almacenados en las direcciones 2 y 5 se descartan porque no son necesarios, mientras que los bytes almacenados en 3 y 4 se unen – BlackBear

1

El problema no se limita solo a las recuperaciones de instrucciones. Y es desafortunado que los programadores no se den cuenta de esto temprano y lo castiguen a menudo. La arquitectura x86 ha dejado a la gente floja. 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 significado. Por lo tanto, si realizara una lectura de 32 bits desde la dirección 0x02, 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, bloqueando la tubería si se trata de una instrucción de búsqueda. El rendimiento alcanzado es dramático y de ninguna manera una optimización desaprovechada 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 tanto como duplicar el rendimiento sin ningún otro esfuerzo. De manera similar, usar un int en lugar de un char para una variable, incluso si solo va a contar hasta 10, puede ser más rápido. Es cierto que agregar nops a los programas para alinear los destinos de las sucursales generalmente no vale la pena. Lamentablemente, x86 tiene una longitud de palabra variable, basada en bytes, y usted constantemente sufre estas ineficiencias. Si está pintado en una esquina y necesita exprimir unos cuantos 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 bit) sino también en un límite de línea de caché, y intente mantener ese bucle dentro de una o quizás dos líneas de caché. En ese sentido, un solo nop al azar en un programa puede causar cambios donde las líneas de caché tocan y se puede detectar un cambio en el rendimiento si el programa es lo suficientemente grande y tiene suficientes funciones o bucles. La misma historia, por ejemplo, si tiene un destino de sucursal en la dirección 0xFFFC, si no está en la memoria caché debe buscarse una caché, nada inesperado, pero una o dos instrucciones más adelante (cuatro bytes) se requiere otra línea de caché. Si el objetivo ha sido 0x10000, dependiendo del tamaño de su función de forma natural, es posible que haya sacado esto en una línea de caché. Si se trata de una función a la que a menudo se llama y otra función a la que se suele llamar se encuentra en una dirección lo suficientemente similar como para que estos dos se desalojen mutuamente, se ejecutará el doble de lento. Este es un lugar donde el x86 ayuda con la longitud de la instrucción variable, pero puede empacar más código en una línea de caché que en otras arquitecturas bien utilizadas.

Con x86 y recuperaciones de instrucción realmente no se puede ganar. En este punto, a menudo es inútil tratar de sintonizar a mano los programas x86 (desde una perspectiva de instrucciones). La cantidad de núcleos diferentes y sus matices, puede hacer 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 despacio, a veces menos de la mitad de la velocidad. Es mejor ser genéricamente eficiente pero tener un poco de descuido para que funcione bien en todas las computadoras todos los días. La alineación de los datos mostrará mejoras en los procesadores de las computadoras, pero la alineación de las instrucciones no es la misma.

+1

La longitud de la instrucción variable no es del todo mala. Un compilador/programador experto puede/utilizará formas de instrucciones más cortas que conducen a un código más denso que, a su vez, descargará el caché del código. Para acceder al código o a los datos de L1, L2, L3 o RAM, se puede usar un costo de aproximadamente 3, 10, 30 y 100 ciclos de reloj de pérdida. Algo encontrado en L2 istdo L1 causará 7 (10-3) ciclos extra. L3 (istdo L1 y 2) 17 (30-10-3) y RAM (istdo caches) 67 (100-30-10-3). Desde esta perspectiva, el código denso es bastante bueno. –

Cuestiones relacionadas