1 Departamento de Arquitectura y Tecnología de Computadores E.T.S. Ingeniería Informática Práctica 3. Optimización de Código Julio Ortega Lopera. Curso 2008/2009 ARQUITECTURA DE COMPUTADORES I
2 2 Arquitectura de Computadores I. Práctica 3 Bibliografía 1.http://www.intel.com/design/PentiumIII/manuals/ 2.http://developer.intel.com/design/pentium4/manuals/index.htm 3.GERBER, R.:”The Software Optimization Cookbook. High Performance Recipes for the Intel Architecture”. Intel Press, 2002. 4.GERBER, R.; et al.:”The Software Optimization Cookbook. High Performance Recipes for the IA-32 Platforms”. Intel Press, 2006. 5.FOG, A.:”How to Optimize for the Pentium family of microprocessors”, http://cr.yp.to/2005-590/fog.pdf, 2004.
3 3 Arquitectura de Computadores I. Práctica 3 Herramientas para realizar la Práctica Compilador: Intel C++ Herramienta de análisis: VTune Versiones de evaluación a partir de la página: developer.intel.com/design/index.htm (a partir de la opción “Intel Software Evaluation Center”) Se puede utilizar el compilador de C de Intel para compilar y la herramienta VTune para visualizar los distintos códigos En esta práctica se puede utilizar el compilador de C que cada uno desee. No obstante el que se ha considerado para plantear los ejercicios a realizar es el GCC para DOS/WINDOWS: www.delorie.com/djgpp
4 4 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
5 5 Arquitectura de Computadores I. Práctica 3 CUESTIONES GENERALES SOBRE OPTIMIZACIÓN (I) Explotando las características de la microarquitectura de un procesador recientemente aparecido, un programador puede aprovechar nuevas propiedades que ofrece dicho procesador antes de que las mismas se hayan incorporado en un compilador o en una biblioteca y estén disponibles en el mercado (con el coste correspondiente): Ejemplo: La instrucción CMOV se incorpora al repertorio de instrucciones x86 con el Pentium Pro, en 1995. El repertorio SSE (Streaming SIMD Extension) aparece con el Pentium III en 1999. Sin embargo, hasta la versión de 2003 del Visual C++.NET no se aprovechaba ni la instrucción CMOV, ni las instrucciones SSE para generar código eficiente
6 6 Arquitectura de Computadores I. Práctica 3 CUESTIONES GENERALES SOBRE OPTIMIZACIÓN (II) Usualmente la optimización de una aplicación se realiza al final del proceso, si queda tiempo. Esperar al final para optimizar dificulta el proceso de optimización. Es un error escribir la aplicación sin tener en cuenta la arquitectura o arquitecturas en las que se va a ejecutar. No es correcto optimizar eliminando propiedades y funciones (features) del código, en el caso de que no se satisfagan las restricciones de tiempo. La optimización debe realizarse durante el proceso de desarrollo, utilizando las características de optimización del compilador (no es adecuado desactivar estas opciones para facilitar la depuración del código). Cuando se optimiza código es importante analizar donde se encuentran los cuellos de botella. El cuello de botella más estrecho es el que al final determina las prestaciones y es el que debe evitarse en primer lugar. Se puede optimizar sin tener que acceder al nivel del lenguaje ensamblador (en algunos casos sí).
7 7 Arquitectura de Computadores I. Práctica 3 CUESTIONES GENERALES SOBRE OPTIMIZACIÓN (III) Optimizaciones desde el Lenguaje de Alto Nivel (OHLL) Optimizaciones desde el Lenguaje Ensamblador (OASM) Optimizaciones aplicables a cualquier procesador (OGP) Optimizaciones específicas para un procesador (OEP) Optimizaciones aplicables a cualquier procesador (OGP) Optimizaciones específicas para un procesador (OEP) Una Clasificación de las Optimizaciones Un Compilador puede ejecutarse utilizando diversas opciones de optimización. Por ejemplo el compilador de c gcc dispone de las opciones –O1, -O2, -O3, -Os que proporcionan códigos con distintas opciones de optimización. Es posible encontrar una descripción de las distintas alternativas de optimización en: http://gcc.gnu.org/onlinedocs/
8 8 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
9 9 IF1IF2IF3ID1ID2RATROB Emit (RS) EXRET1RET2 Varios Ciclos CaptaciónDecodificaciónRenombrado Escritura ROB Emisión Retirar (3uops/ciclo) Arquitectura de Computadores I. Práctica 3 Etapas del Cauce en la microarquitectura P6
10 10 3 x 118 Bytes IF1 IF2 IF3 ID2 RAT 16 Bytes De la cache de Instrucciones 32 Bytes ID1 D0D1D2 6 x 118 Bytes 3 x 118 Bytes Al ROB Predicción Estática de Saltos Secuenciador de Microinstrucciones Predicción Dinámica de Saltos IP Siguiente Arquitectura de Computadores I. Práctica 3 Captación y Decodificación en la microarq. P6
11 11 Arquitectura de Computadores I. Práctica 3 Optimización de la Captación en P6 (I) Los bloques ifetch (como máximo de 16 bytes, no alineados) pasan desde del buffer de instrucciones captadas (32 bytes como máximo, alineados) a los decodificadores. Los bloques ifetch comienzan en el inicio de una instrucción y tienen 16 bytes, salvo que haya una instrucción de salto para la que se tenga historia (predicción dinámica Si se produce salto, pueden pasar dos ciclos hasta que se capte la instrucción siguiente (si la instrucción cruza un límite de 16 bytes y hacen falta dos accesos): es beneficioso que haya más instrucciones en el bloque ifetch que puedan pasar a decodificarse mientras se espera la instrucción correspondiente. Saber la forma en que se van delimitando los ifetch (sobre todo después de un salto) permite determinar qué instrucciones van a ir pasando a decodificarse y se puede mejorar el rendimiento de la decodificación (como se verá) Hasta que no se ha terminado de decodificar un ifetch no se inicia el siguiente.
12 12 Arquitectura de Computadores I. Práctica 3 Optimización de la Captación en P6 (II) En la siguiente tabla se muestran las reglas que permiten conocer el alineamiento del primer bloque ifetch después de un salto. Número de Grupos de Decodificación en el bloque ifetch que contiene un salto Hay límite de 16 bytes en el bloque ifetch Límite de 16 bytes en la primera instrucción después de un salto Retardo para empezar la decodificación Alineamiento del primer bloque ifetch después de un salto 10a 16 bytes 1X1a la instrucción 1X1a 16 bytes 1XX2a la instrucción 20 2X0 2X0a 16 bytes 2XX1a la instrucción 3 -0a la instrucción 3 -X0a la instrucción 3 -X0a la instrucción 3 -XX0a la instrucción
13 13 Primer IFETCH: 1000h –1010h (sin incluir) (2 ciclos en decodificar) Segundo IFETCH:1007h –1017h (sin incluir) (1 ciclo en decodificar) Tercer IFETCH:1017h – 1022h (inclusive) (3 ciclos en decodificar) La primera iteración del bucle LL necesita 5 ciclos para decodificarse El primer bloque ifetch después del salto empezará en la instrucción LL ya que el último bloque ifetch tiene una alineación de 16 bytes y tres grupos de decodific. en 1020h (desde 1005h a 1015h) El siguiente bloque ifetch empieza en 1011h y termina antes de la 1021h y el último bloque empieza en 1021h hasta el final (y no incluye límites de 16 bytes) Se necesitan 7 ciclos para decodificar la segunda iteración y el bloque ifetch de la siguiente iteración empieza en un límite de 16 bytes que incluya a la dirección de salto (1000h) Las iteraciones impares necesitan 5 ciclos y las pares 7 ciclos para decodificarse Optimización de la Captación en P6 (III) Ejemplo
14 14 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
15 15 Arquitectura de Computadores I. Práctica 3 Optimización de la Decodificación (I) Los decodificadores pueden manejar tres instrucciones por ciclo pero sólo si se reúnen una serie de condiciones que se resumen a continuación: La primera instrucción, decodificada en D0 no puede generar más de 4 uops en un sólo ciclo de reloj, y la segunda y tercera instrucción no deben generar más de 1 uop cada una. La segunda y tercera instrucción no debe tener más de 8 bytes cada una. Las instrucciones deben estar contenidas en el mismo bloque de ifetch de 16 bytes. movl_MEM1, %ebx1 uop (D0) incl%ebx1 uop (D1) addl_MEM2, %eax2 uops (D0) addl%eax, _MEM34 uops (D0) movl_MEM1, %ebx1 uop (D1) incl%ebx1 uop (D2) addl_MEM2, %eax2 uops (D0) Se gana un ciclo en la decodificación (3 2) Microarquitectura P6
16 16 Arquitectura de Computadores I. Práctica 3 Optimización de la Decodificación (II) Los prefijos que tienen ciertas instrucciones también pueden ocasionar pérdidas de ciclos en los decodificadores. Prefijo de tamaño de operando cuando se tiene un operando de 16 bits en un entorno de 32 o viceversa (Operand-size override prefix, 66h). Prefijo de tamaño de dirección (Address-size override prefix, 67h). Un prefijo produce un ciclo de penalización (si se utiliza más de un prefijo, habría una penalización de un ciclo por prefijo) si: El prefijo de tamaño de operando se utiliza con un operando inmediato El prefijo de ajuste de dirección se utiliza con una dirección que incluye un offset. movw $0x77, _mem movl %edx,_mem(%ax) movl $0x77,%eax movw %ax,_mem addw _mem,%ax movl %edx,(%ax) Prefijo 66h en el modo de 32 bits (almacena un dato de 16 bits en memoria) Se ahorra un ciclo Prefijo 67h (almacena un dato en memoria utilizando un offset) Microarquitectura P6
17 17 Arquitectura de Computadores I. Práctica 3 Optimización de la Decodificación (III) En la microarquitectura NetBurst el decodificador puede generar de 1 a 4 uops por instrucción y ciclo. Las instrucciones que necesitan más de 4 uops se envían a una memoria ROM de microcódigo. En este caso, la instrucción puede tardar más de un ciclo en decodificarse. Las instrucciones que tienen más de un prefijo necesitan un ciclo de decodificación por prefijo. El tiempo de decodificación no es importante en el caso de bucles que quepan en la cache de traza. Si las uoperaciones correspondientes a una instrucción no están en la cache de traza, éstas pasan a las etapas de ejecución desde el decodificador y, en este caso, la velocidad de decodificación sí es relevante (las instrucciones pasan al decodificador desde la cache L2). Las trazas de código que tardan en decodificarse más que en ejecutarse son las que suelen incluirse en la cache de traza (de forma automática). Microarquitectura NetBurst
18 18 Arquitectura de Computadores I. Práctica 3 Cache de Traza en NetBurst (I) En el Pentium 4 las instrucciones se introducen en una cache de traza tras haber sido decodificadas en uoperaciones (en lugar de almacenarse las instrucciones en una cache L1 para instrucciones, en la cache de traza se almacenan las trazas de las uoperaciones consecutivas correspondientes). Existe una cache L2 para código y datos de 256 KB como mínimo, con un bus de acceso de 256 bits. La cache de traza está organizada en 2048 líneas con espacio para 6 uops cada una y con correspondencia asociativa por conjuntos de 4 vías. En el espacio disponible para cada uop hay 16 bits de datos: si una uop necesita más bits para los datos, ocupa más espacio (espacio de varias uops). Hay que tener en cuenta que, ninguna uop depende de más de dos operandos de entrada. Por ejemplo: CMOVcc puede tener más de dos operandos y se divide en dos uops MOV [ESI+EDI], AX también da lugar a dos uops MOV EAX,[MEM1] necesita dos espacios de la cache (la dirección puede tener más de 16 bits)
19 19 Arquitectura de Computadores I. Práctica 3 Cache de Traza en NetBurst (II) Las uops pueden extraerse de la cache a una velocidad que corresponde a 1 línea de cache cada dos ciclos: esto supone unas 3 uops por ciclo (aproximadamente). Es conveniente que ninguna uop que ocupe dos espacios cruce los límites de 6 espacios que correspoden al final/comienzo de una línea. Por ejemplo, se podrían reordenar de forma que haya un número par (considerando al 0 como número par) de uops que ocupan un espacio entre cada dos instrucciones de doble espacio. MOV EAX, [MEM1] (1 uop, 2 esp.) ADD EAX, 1(1 uop, 1 esp.) MOV EBX,[MEM2](1 uop, 2 esp.) MOV [MEM3],EAX(1 uop, 2 esp.) ADD EBX,1(1 uop, 1 esp.) MOV EAX, [MEM1] (1 uop, 2 esp.) MOV EBX,[MEM2](1 uop, 2 esp.) ADD EAX, 1(1 uop, 1 esp.) ADD EBX,1(1 uop, 1 esp.) MOV [MEM3],EAX(1 uop, 2 esp.)
20 20 Arquitectura de Computadores I. Práctica 3 Cache de Traza en NetBurst (III) Las trazas están organizadas de forma que las uops que dan lugar a saltos están seguidas por las uops que suelen seguirlas al ejecutarse en vez de las que están a continuación de ellas en el código (también pueden ser éstas las que las siguen): la misma secuencia de uops puede repetirse en la cache de traza si se salta a ella desde diferentes sitios. Cuando hay saltos, el rendimiento de la cache suele ser menor que 3 uops por ciclo: si la primera uop de una línea de cache es una uop que da lugar a un salto a una uop distinta de la que le sigue, las otras 5 uop de la línea que se han leído junto con la uop de salto no sirven para nada. Es bastante difícil planificar la posición de las instrucciones de salto para que siempre estén al final de una línea de la cache de traza.
21 21 Arquitectura de Computadores I. Práctica 3 Cache de Traza en NetBurst (IV) Líneas generales para mejorar las prestaciones de la cache de traza: 1.Utilizar instrucciones que generen pocas uops 2.Utilizar datos inmediatos entre -2 15 y 2 15, si es posible 3.Evitar direccionamientos directos con direcciones de 32 bits 4.Evitar un número impar de uops que necesitan un solo espacio entre uops de dos espacios 5.Intentar sustituir las instrucciones de salto condicional por instrucciones de movimiento condicional (si eso no implica costos excesivos por otro lado)
22 22 Arquitectura de Computadores I. Práctica 3 Optimización del Renombrado (I) Para la optimización de la etapa de renombrado en el RAT, lo ideal es que, en un ciclo, no se introduzcan en el RAT grupos de uoperaciones que lean más de dos registros del banco de registros. Se pueden seguir ciertas recomendaciones: Mantener las uoperaciones que leen el mismo registro lo más cerca posible para que sea más probable que entren a la vez en el RAT. Mantener las uoperaciones que leen de registros diferentes lo más lejos posible para que no entren a la vez en el RAT. Provocar renombrados de registros para evitar los ciclos perdidos en el acceso a los registros (si no se introducen muchas uoperaciones).
23 23 MOVEAX, EBX SUBECX, EAX INCEBX MOV[EAX], EDX ADDESI, EBX ADDESI, ECX Todas las instrucciones generan una uop. Las tres primeras pasan al RAT. Utilizan tres registros, pero como EAX se escribe antes, se le asignará una entrada en el ROB (se renombra) y se lee desde allí. Sólo se leen EBX y ECX. En la segunda tanda de tres uops se necesitan más de dos registros, pero como se ha escrito antes sobre EBX, y ECX sólo ESI y EDX deben leerse desde el fichero de registros Ninguna uop usa más de dos operandos (todas las instrucciones que usan más de dos registros dan lugar a dos o más uops) Lo ideal es que no pasen a la vez por el RAT uops que lean más de dos registros del fichero de registros: es difícil predecir qué uops pasan por el RAT en cada ciclo (un salto mal predicho descarta las instrucciones de la cola entre la unidad de decodificación y el ROB y habría que tener en cuenta el orden en que se generan las uops). Si la lectura de un operando se hace desde el ROB (en lugar de utilizarse el Banco de Registros) no existen limitaciones para el número de lecturas (la limitación en el número de lecturas viene del número de puertos de lectura del banco de registros). Arquitectura de Computadores I. Práctica 3 Optimización del Renombrado (II)
24 24 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
25 25 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: Unidades de ejecución Se podrían realizar cuatro sumas por ciclo pero hay que tener en cuenta que la cache de traza sólo emite tres uops por ciclo: esta velocidad de ejecución se puede conseguir si las instrucciones han estado en cola durante un cierto periodo de tiempo previo. Si todos los puertos estuviesen generando resultados a su máxima velocidad se podrían terminar 6 uops por ciclo (pero hay que tener en cuenta que las instrucciones se retiran a un ritmo de 3 uops por ciclo, como en la microarquitectura P6. La mayoría de las subunidades están segmentadas (la fp-div no está segmentada, y puede tardar entre 23 y 43 ciclos de reloj) PuertoUnidadOperaciones (Subunidades)Velocidad (resultados/ciclo) p0ALU0add, sub, mov, lógicas, saltos, store entero2 p0MOVmov y store fp, fxch1 p1ALU1add, sub, mov2 p1INTmisc1 p1FPfp add, mul, div, misc1/2 p1MMXmmx alu, shift, misc1/2 p2LOADTodas las cargas1 p3STOREalmacenamientos con direcciones1 Microarquitectura NetBurst
26 26 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: Registros parciales Se puede producir un 'atasco' en el cauce debido al uso de un registro parcial si se lee un registro de mayor tamaño después de una escritura en un registro parcial Para evitar ese tipo de atascos se tendría que borrar el registro mayor con XOR o SUB antes de escribir sobre el registro parcial. Borrar el registro mayor con MOV no evita el atasco. mov eax, 0 mov ax, mem16 add ecx, eax xor eax, eax mov ax, mem16 add ecx, eax mov ah,cl mov eax,ecx mov al,dlshl eax,8 mov mem, eaxand edx,0xff Sin atascoCon atasco
27 27 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: Bit de Estado Una escritura en alguno (no en todos) de los flags de estado del registro EFLAGS precede a una lectura tanto de los flags modificados como no modificados (no hay problema si se leen sólo los modificados o sólo los no modificados). sahf// almacena el registro ah en el registro de flags excepto OF jg label// jg lee el flag OF junto con los SF y ZF sahf// no hay atasco instrucciones que no leen los flags jg label Instrucciones como INC y DEC que no actualizan todos los flags de estado (no actualizan el flag de acarreo, CF) pueden provocar atascos: En estos casos es mejor utilizar ADD o SUB (Ojo en el control de bucles!!)
28 28 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: Desenrollado de Bucles Utilizar el desenrollado de bucles para romper secuencias de instrucciones dependientes intercalando otras instrucciones. float dot-product(float *a, float *b) { int i; float tmp=0.0; for (i=0; i
29 29 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: Unidades Funcionales de la Microarquitectura Es importante tener en cuenta las unidades funcionales de que dispone la microarquitectura para utilizar las instrucciones de la forma más eficaz. Así: - La división es una operación muy costosa y por lo tanto habría que evitarla (por ejemplo, utilizando desplazamientos) o reducir su número. for (i=0;i
30 30 Arquitectura de Computadores I. Práctica 3 Optimización de la Ejecución: No utilizar código ambiguo No utilizar código ambiguo ya que si los compiladores no pueden resolver los punteros, tampoco pueden realizar ciertas optimizaciones, asignar variables durante la compilación, realizar cargas de memoria mientras que un almacenamiento está en marcha. Para evitar esto: Utilizar variables locales en lugar de punteros, utilizar variables globales si no se pueden utilizar las locales, y poner las instrucciones de almacenamiento después o bastante antes de las de carga de memoria. No obstante, si no se utilizan punteros el código es más dependiente de la máquina, y a veces las ventajas de no utilizarlos no compensa.int j; void mars (int *v) {void mars(int v) {j=7.0; *v=15;v=15;j/=7;.................} En el código no optimizado, el compilador no puede asumir que *v no apunt a j. Sin embargo en el código optimizado, el compilador puede hacer directamente j=1.0.
31 31 Arquitectura de Computadores I. Práctica 3 Utilizar siempre la precisión más baja posible: -Precisión simple (32 bits) es más rápida que doble (64 bits) o doble-extendida (80 bits) y consume menos memoria. -Las instrucciones FDIV y FSQRT tienen una latencia mucho más alta en doble precisión. La mayoría de los lenguajes de programación redondean al valor más cercano para las operaciones en coma flotante, y truncan -'chop'- (al número menor si es positivo y al mayor si es negativo) para la conversión a entero. Ejemplo: cada vez que la instrucción FISTP (convierte el valor de ST(0) a un número con signo, almacena el resultado en el registro destino y hace un pop de la pila de registros) convierte un número en coma flotante a entero, el compilador utiliza dos veces la instrucción FLDCW para cambiar el modo de redondeo. La instrucción FLDCW tiene un costo muy elevado puesto que interrumpe el procesamiento paralelo de instrucciones para sincronizar su ejecución. Para evitar el uso de FLDCW se puede: -No cambiar al modo de redondeo 'chop' si no se necesita para los resultados -Poner fuera de los bucles la instrucción de conversión de modo de redondeo. -Reemplazar el algoritmo de conversión por uno más eficaz diseñado específicamente Optimización de la Ejecución: Operaciones en Coma Flotante
32 32 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
33 33 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Alineación de Datos (II) En el Pentium Pro, Pentium II y Pentium III, el acceso a los datos no alineados supone un costo adicional de entre 6 y 12 ciclos cuando se cruza el límite de la línea de cache. Los operandos menores de 16 bytes que no crucen una línea de cache no dan lugar a penalización. Es posible controlar, desde un programa escrito en un lenguaje de alto nivel como C, la alineación de los datos que utiliza dicho programa y con ello evitar la penalización que pueda producirse por una falta de alineación. BOUND=32 si se quiere que esté alineado con líneas de cache de 32 bytes N : Tamaño del vector BOUND : 0x20 = 00000000000000000000000000100000 BOUND – 1 : 0x1F = 00000000000000000000000000011111 ~(BOUND-1): 0xFFE0 = 11111111111111111111111111100000
34 34 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Alineación de Datos (II) Cuando el código accede a un array de forma no secuencial es conveniente ajustar y alinearla estructura de forma que ocupen el mínimo número de líneas de cache. Ejemplo: En el código del ejemplo se accede a la primera y a la última estructura de un array de 5 estructuras de 28 bytes. Cuando se accede a una estructura se producen dos faltas de acceso a cache en lugar de una (en el caso de estas dos estructuras, tal y como se consideran situadas en el ejemplo) Código originalCódigo Optimizado struct {struct{int a[7]; } s[5];int pad;..........} s[5]; for (i=0;i
35 35 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Colisiones en Cache Puesto que la cache es asociativa por conjuntos de cuatro vías en el Pentium II y Pentium III y las líneas son de 32 bytes, los bits 5 a 11 de la dirección física de memoria (0 es el bit menos significativo) indican el conjunto en el que se introduce la línea correspondiente. Para conocer si dos líneas se almacenan en cache en el mismo conjunto, se toman dos direcciones, una de cada línea y se hacen igual a 0 los 5 bits menos significativos de ambas. Si la diferencia de las nuevas direcciones es múltiplo de 4096, las líneas a las que pertenecen esas direcciones van al mismo conjunto. El ejemplo siguiente ilustra un procedimiento para asegurar que dos zonas de datos se asignan a distintos conjuntos (y no colisionen en cache): int *tempA, *tempB;..................... pA= (int *) malloc (sizeof(int)*N + 31); tempA = (int *)(((int)pA+31)&~(31)); tempB = (int *)((((int)pA+31)&~(31))+4096+32); Los punteros tempA y tempB están apuntando a zonas de memoria que empiezan en posiciones que son múltiplos de 32 y que no se asignarían al mismo conjunto de cache.
36 36 Arquitectura de Computadores I. Práctica 3 La forma en que se declaren los arrays determina la forma en que se almacenan en memoria. Interesa declararlos según se vaya a realizar el acceso. Ejemplos: Formas óptimas de declaración de variables según el tipo de acceso a los datosstruct { int a[500];int a; int b[500];int b; } s;} s[500];................... for (i=0; i
37 37 Arquitectura de Computadores I. Práctica 3 Intercambiar los bucles para cambiar la forma de acceder a los datos según los almacena el compilador, y para aprovechar la localidad Ejemplo: Código Original for (j=0; j
38 38 Arquitectura de Computadores I. Práctica 3 Los 'atascos' (stalls) por acceso a la memoria (load adelanta a store, especulativo) se producen cuando: Hay una carga (load) 'larga' que sigue a un almacenamiento (store) 'pequeño' alineados en la misma dirección o en rangos de direcciones solapadas. mov word ptr [ebp],0x10 mov ecx, dword ptr [ebp] Una carga (load) 'pequeña' sigue a un almacenamiento (store) 'largo' en direcciones diferentes aunque solapadas (si están alineadas en la misma dirección no hay problemas). mov dword ptr [ebp-1], eax mov ecx,word ptr [ebp] Datos del mismo tamaño se almacenan y luego se cargan desde direcciones solapadas que no están alineadas. mov dword ptr [ebp-1], eax mov eax, dword ptr [ebp] Para evitarlos: Utilizar datos del mismo tamaño y direcciones alineadas y poner los loads tan lejos como sea posible de los stores a la misma área de memoria Optimización del Acceso a Memoria y Cache: Mejora del rendimiento del acceso especulativo
39 39 for (i=0;i
40 40 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Pre-captación (I) Pentium 4: Cache L1 de datos con líneas de 64 bytes (128 líneas * 64 bytes/línea = 8 KBytes), asociativa por conjuntos de 4 vías. Cache L2 unificada, con líneas de 128 bytes (2K líneas * 128 bytes = 256 Kbytes), asociativa por conjuntos de 8 vías. El procesador, mediante las correspondientes instrucciones de prefetch, carga zonas de memoria en cache antes de que se soliciten (cuando hay ancho de banda disponible). Hay cuatro tipos de instrucciones de prefetch: Instrucción ensamb. Segundo parámetro en la función C++ _mm_prefetch Descripción prefetchnta_MM_HINT_NTA Prefetch en buffer no temporal (dato para una lectura) prefetcht0_MM_HINT_T0Prefetch en todas las caches útiles prefetcht1_MM_HINT_T1Prefetch en L2 y L3 pero no en L1 prefetcht2_MM_HINT_T2Prefetch sólo en L3
41 41 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Pre-captación (II) El aspecto crucial al realizar precaptación es la anticipación con la que se pre-captan los datos. En muchos casos es necesario aplicar una estrategia de prueba y error. Además, la anticipación óptima puede cambiar según las características del computador (menos portabilidad en el código). Una instrucción de prefetch carga una línea entera de cache (en el Pentium 4 sólo sería necesario captar 1 byte de cada 64 byte) For (i=0; i
42 42 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Write-Combining En algunos casos las prestaciones y la funcionalidad de ciertas aplicaciones se ven afectadas si se utiliza la cache en las escrituras (escrituras en registros de control, adaptadores de tarjetas, u otros buffers situados en diversos dispositivos hardaware). Por eso, el sistema operativo y los drivers de dispositivos pueden definir zonas de memoria como no cacheables: los datos escriben inmediatamente y en el orden especificado en memoria, evitando la cache. Estas escrituras tienen prestaciones muy malas Mediante la técnica de Escrituras Combinadas (Write Combining, WC) se utilizan buffers internos disponibles en el procesador para almacenar secuencias de escrituras de datos contiguos antes de iniciar una transferencia de memoria para almacenarlos. Esta técnica es útil cuando las prestaciones del acceso a memoria es más importante que la inmediatez y el orden de las escrituras, y el procesador no necesita acceder a los datos de nuevo. El sistema operativo y los drivers de dispositivo deben definir las zonas de memoria correspondientes como zonas Write Combining (para buffers de tramas en tarjetas gráficas, buffers en discos duros,..). La instrucción SFENCE indica que se escriban los buffers en memoria (cuando se necesitan los datos)
43 43 Arquitectura de Computadores I. Práctica 3 Optimización del Acceso a Memoria y Cache: Write-Combining (II) Además, mediante el uso del Write Combining, las aplicaciones pueden evitar el uso de la cache para los datos que sólo tienen que escribirse, obteniendo mejores prestaciones. Para utilizar Write Combining existen una serie de instrucciones de almacenamiento específicas denominadas streaming store instructions: cuando se ejecuta una de estas instrucciones, si los datos no están en cache se utilizan los buffers de escritura y se evita el uso de la cache. Se pueden escribir datos de 32, 46, y 128 bits //ensamblador movntdq mem, xmm0 //intrinsics _mm_stream_si128(mem,a) // C++ biblioteca de clases store_nta(mem,a) movntiDoble palabra entera (32 bits) movntqQuadword entera (64 bits) movntdqQwadword doble entera (128 bits) movntpdDos valores de coma flotante y doble prec (128 bits) movntpsCuatro valores coma flotante y simple prec. (128 b) maskmovqBytes seleccionados de una quadword (64 bits) maskmovdqu Bytes de una doble quadword (128 bits)
44 44 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
45 45 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS if (t1==0 && t2==0 && t3==0) if ((t1 | t2 | t3)==0) Cada una de las condiciones separadas por && se evalúa mediante una instrucción de salto distinta. Si las variables pueden ser 1 ó 0 con la misma probabilidad, la posibilidad de predecir esas instrucciones de salto no es muy elevada. Si se utiliza un único salto, la probabilidad de 1 es de 0.125 y la de 0 de 0.875 y la posibilidad de hacer una buena predicción aumenta. // if ((t1 | t2 | t3)==0) { t4=1}; //t1 -> edi t2 -> ebx t3 -> ebp t4 -> eax mov ecx, 1 or edi, ebx or edi, ebp cmove eax, ecx Mediante la instrucción de movimiento condicional se pueden evitar los daltos
46 46 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS: MEJORA DE LA PREDICCIÓN (I) Predicción Dinámica (36 bits) Dada una secuencia de saltos/no saltos para una instrucción de salto, si toda subsecuencia de cuatro bits (indicando cada bit si se produce salto o no) va seguida del mismo bit, la secuencia es predecible (tras el correspondiente número de ciclos de aprendizaje) Cuando no hay historia para una instrucción de salto, esto es, la primera vez que se ejecuta, se utiliza el siguiente procedimiento de predicción estática: Si la dirección de salto no es relativa al contador de programa IP: Predice 'Saltar' si el salto es un 'return', y 'No Saltar' en caso contrario. Si la dirección de salto es relativa a IP: Predice 'Saltar' si el salto es hacia atrás (situación análoga a los bucles), y 'No Saltar' si el salto es hacia delante. Secuencia predecible: 1000100010001000 Secuencia no predecible: 000001000001 Microarquitectura P6
47 47 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS: MEJORA DE LA PREDICCIÓN (II) En algunos casos, los bucles se pueden organizar para que den lugar a secuencias predecibles por el esquema de predicción dinámica: Si un bucle se ejecuta 20 veces no se predice correctamente la última iteración. Para evitar esto se puede utilizar dos bucles anidados de 4 y 5 iteraciones respectivamente, o desenrollar el bucle por cuatro, para que sólo haya 5 iteraciones. Si un salto que no tiene ninguna historia almacenada en el BTB se utiliza la predicción estática, que predice los saltos hacia delante como no tomados. Se pueden mejorar las prestaciones del procedimiento si se situa el código más frecuente después del salto condicional hacia delante. Código Ensamblador Original comp a, 5 je L1 Código Infrecuente jmpL2 L1: Código Frecuente L2: Código Ensamblador Mejorado comp a, 5 jne L1 Código Frecuente jmpL2 L1: Código Infrecuente L2:
48 48 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS: MEJORA DE LA PREDICCIÓN (III) No se dispone de información precisa del procedimiento de predicción de saltos que utiliza el Pentium 4, si bien parece que tiene un esquema basado en bits de historia global y predictores locales como el de la figura. En el Pentium 4, parece ser que se utilizan 16 bits de historia global de saltos, pero la función de indexación no se conoce con detalle. Dirección de la instrucción de Salto Historia Global de Saltos 010111……….10 Función de indexación Tabla de patrones de historia global Tomado/no tomado Coincidencia/ No-coincidencia XOR Predicción
49 49 Arquitectura de Computadores I. Práctica 3 Se puede reducir el número de saltos de un programa reorganizando las alternativas en las sentencias switch, en el caso de que alguna opción se ejecute mucho más que las otras (más del 50% de las veces, por ejemplo). Ciertos compiladores que utilizan información de perfiles de ejecución del programa son capaces de realizar esta reorganización (Se recomienda utilizarla si la sentencia switch se implementa como una búsqueda binaria en lugar de una tabla de salto). OPTIMIZACIÓN DE SALTOS: REDUCCIÓN DE SALTOS (I) Código original switch (i) { case 16: Bloque16 break; case 22: Bloque22 break; case 33: Bloque33 break; } Código Optimizado if (i==33) { Bloque33 } else switch (i) { case 16: Bloque16 break; case 22: Bloque22 break; }
50 50 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS: REDUCCIÓN DE SALTOS (II) CMOVcc hace la transferencia de información si se cumple la condición indicada en cc test ecx,ecx jne 1hcmoveq eax, ebx mov eax,ebx 1h: FCMOVcc es similar a CMOVcc pero utiliza operandos en coma flotante
51 51 Arquitectura de Computadores I. Práctica 3 OPTIMIZACIÓN DE SALTOS: REDUCCIÓN DE SALTOS (III) La instrucción SETcc es otro ejemplo de instrucción con predicado que puede permitir reducir el número de instrucciones de salto. ebx= (A
52 52 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
53 53 OPTIMIZACIÓN DE BUCLES (I) En un programa, a menudo, la mayor parte del tiempo se pasa en uno de los bucles del mismo. La forma más directa de mejorar la velocidad consiste en optimizar cuidadosamente el bucle que más tiempo consuma utilizando el lenguaje ensamblador. En los ejemplos que se considerarán a continuación, se asume que los datos están en la cache de nivel 1. Si la velocidad está limitada por los fallos de cache habría que concentrarse primero en distribuir los datos para disminuir los fallos. Como ejemplo se tomará un procedimiento sencillo en C: void Cambiosigno (int *A, int *B, int N) { int i; for (i=0; i
54 54 OPTIMIZACIÓN DE BUCLES (II) Este código puede mejorarse más, tratando de evitar las instrucciones que generan muchas uops, como por ejemplo LOOP, LODS, y STOSD. A continuación se considerarán distintas mejoras del código (zona representada dentro del cuadro de línea discontinua) teniendo en cuenta la microarquitectura del procesador y utilizando el desenrollado de bucles..file"cambiosign.c" gcc2_compiled.: ___gnu_compiled_c:.text.p2align 2.globl _changesign _changesign: pushl %ebp movl %esp,%ebp subl $16,%esp pushl %esi pushl %edi movl 16(%ebp),%ecx jecxz L2 movl 8(%ebp),%esi movl 12(%ebp),%edi.p2align 4,,7 cld L1: lodsl negl %eax stosl loop L1 L2: leal -24(%ebp),%esp popl %edi popl %esi movl %ebp,%esp popl %ebp ret
55 55 OPTIMIZACIÓN DE BUCLES (III) A continuación se mostrarán algunas mejoras de este código considerando distintos aspectos de la microarquitectura del procesador: Decodificación de instrucciones. En la versión mejorada del programa ensamblador hay una instrucción que genera 2 uops (MOV [EDI],EAX) y debe ir al decodificador D0. En cada iteración del bucle existen tres grupos de decodificación (se podrían decodificar en tres ciclos). Límites de los bloques de 16 bytes de instrucciones que se captan. Una forma de mejorar la velocidad es conseguir que las instrucciones del bucle estén alineadas en el menor número posible de estos bloques Atascos producidos por las lecturas de registros (posibles riesgos de tipo RAW) Análisis de las uops que van a cada puerto de ejecución. Hay que evitar las colisiones en la medida de lo posible. Retirada de instrucciones del ROB. En cada ciclo se pueden retirar como mucho 3 uops. Arquitectura de Computadores I. Práctica 3
56 56 OPTIMIZACIÓN DE BUCLES (IV) pushl %esi pushl %edi movl 16(%ebp),%ecx jecxz L2 movl 8(%ebp),%esi movl 12(%ebp),%edi.p2align 4,,7 ; alineado a 16 bytes L1: movl (%esi),%eax ; 2 p2rESIwEAX addl $4,%esi ; 3 p01rwESIwF negl %eax ; 2 p01rwEAXwF movl %eax,(%edi) ; 2 p4rEAX, p3rEDI addl $4,%edi ; 3 p01rwEDIwF decl %ecx ; 1 p01rwECXwF jnz L1 ; 2 p1rF L2: leal -24(%ebp),%esp popl %edi popl %esi Hay tres grupos de decodificación en el bucle (3 ciclos de reloj) Puesto que el bucle entero tiene 15 bytes, se puede introducir completamente en un bloque ifetch de 16 bytes si se alinea el comienzo del bucle a 16 bytes. No hay atascos de registros (renombrado): cuando se hace una lectura de registro, la escritura en el mismo se ha hecho antes. En cuanto a la ejecución, para los puertos p0 o p1, hay 4 uops; para p1, 1 uop; y 1 uop para cada unos de los puertos p2, p3, y p4. Suponiendo una distribución óptima de las operaciones esto supone unos 3 ciclos. El número de ciclos necesario para retirar las uops es igual al menor entero mayor o igual al número de uops dividido por tres. Cada iteración del Bucle se podría terminar en tres ciclos de reloj.
57 57 OPTIMIZACIÓN DE BUCLES (V) pushl %esi pushl %edi movl 8(%ebp),%esi movl 12(%ebp),%edi movl 16(%ebp),%ecx leal (%esi,%ecx,4),%esi leal (%edi,%ecx,4),%ebx negl %ecx jz L2.p2align 4,,7 L1: movl (%esi,%ecx,4),%eax ; 3 p2rESIrECXwEAX negl %eax ; 2 p01rwEAXwF movl %eax,(%edi,%ecx,4) ; 3 p4rEAX, p3rEDIrECX incl %ecx ; 1 p01rECXwF jnz L1 ; 2 p1rF L2: leal -24(%ebp),%esp popl %edi popl %esi Se han reducido 6 uops utilizando el mismo registro como contador e índice. Los punteros base apuntan al final de los arrays para que se pueda contar descendentemente. Hay dos grupos de decodificación: se puede hacer en 2 ciclos Alineando el bucle en los bloques de 16, como el número de bytes es 11, sólo se necesitan 2 ciclos para captar las instrucciones. Hay 2 uops para p0 o p1, y 1 uop para p1, p2, p3, y p4. Se puede ejecutar en unos2 ciclos Se pueden retirar las uops en dos ciclos. Se podría terminar cada iteración del Bucle en dos ciclos Arquitectura de Computadores I. Práctica 3
58 58 OPTIMIZACIÓN DE BUCLES (VI). DESENROLLADO El desenrollado de bucles permite reducir el overhead debido a los saltos y el número de saltos. Por ejemplo, en el código de la página Optimización de Bucles (IV) el overhead de cada bucle es de 4 uops (actualizar punteros, contador e instrucción de salto) frente a 4 uops para hacer los cálculos en si. Si se hiciera un desenrollado como indica el código que se muestra (dividiendo el número de iteraciones por la mitad) se pasaría de un 50% de overhead a un 33%. Existe un código que puede tener un mejor comportamiento que éste en cuanto a que puede reducir el número de ciclos de decodificación y el número de ciclos necesario para retirar las instrucciones del ROB.p2align 4,,7 L1: movl (%esi),%eax ;2 p2rESIwEAX negl %eax ;2 p01rwEAXwF movl %eax,(%edi) ;2 p4rEAX,p3rEDI movl 4(%esi),%eax;3 p2rESIwEAX negl %eax ;2 p01rwEAXwF movl %eax,4(%edi);3 p4rEAX,p3rEDI addl $8,%esi ;3 p01rwESIwF addl $8,%edi ;3 p01rwEDIwF decl %ecx ;1 p01rwECXwF jnz L1 ;2 p1rF Arquitectura de Computadores I. Práctica 3
59 59 OPTIMIZACIÓN DE BUCLES (VII). DESENROLLADO Teniendo en cuenta la distribución de instrucciones, el primer grupo de 16 bytes incluiría hasta la instrucción negl %ebx, y se podrían decodificar en dos ciclos. El resto de instrucciones del bucle se podrían decodificar en otros dos ciclos. Este código se puede decodificar en 4 ciclos en lugar de los 5 que necesitaba el anterior. Este código también permite mejorar el rendimiento del ROB. El coste ha sido la necesidad de introducir un registro extra (el registro EBX).p2align 4,,7 L1: movl (%esi),%eax ;2 p2rESIwEAX movl 4(%esi),%ebx;3 p2rESIwEBX negl %eax ;2 p01rwEAXwF movl %eax,(%edi) ;2 p4rEAX,p3rEDI addl $8,%esi ;3 p01rwESIwF negl %ebx ;2 p01rwEBXwF movl %ebx,4(%edi);3 p4rEBX,p3rEDI addl $8,%edi ;3 p01rwEDIwF decl %ecx ;1 p01rwECXwF jnz L1 ;2 p1rF Arquitectura de Computadores I. Práctica 3
60 60 Arquitectura de Computadores I. Práctica 3 Índice 1.Cuestiones generales sobre optimización 2.Optimización de la Captación de Instrucciones 3.Optimización de la Decodificación y el Renombrado de Registros 4.Optimización de la Ejecución 5.Optimización del Acceso a Memoria Principal y Cache 6.Optimización de Saltos 7.Optimización de Bucles 8.Realización de los Ejercicios Prácticos
61 61 Arquitectura de Computadores I. Práctica 3 Realización de la Práctica Se utiliza como compilador de C el GCC para DOS/WINDOWS: www.delorie.com/djgpp Como es sabido, el proceso de compilación tiene cuatro etapas: preprocesamiento, compilación propiamente dicha, ensamblado, y enlazado (link). Con las opciones: -cSe compila o se ensamblan los ficheros fuente utilizados pero no se enlazan. Es decir, se generan los ficheros objeto.o. -SEl proceso de compilación para después de la etapa de compilación propiamente dicha. Por lo tanto, se obtiene el código en ensamblador.s
62 62 Arquitectura de Computadores I. Práctica 3 test_bench.c Función a optimizar suma_prod() Ejemplo de programa de prueba en C
63 63 test_bench.csuma_prod.c Transformaciones Alto Nivel Transformaciones Ensamblador gcc –c test_bench.o gcc –S suma_prod.s gcc –c suma_prod.o test_bench gcc –o test_bench ficheros.o Esquema de Trabajo en los Ejercicios Prácticos Arquitectura de Computadores I. Práctica 3
64 64 Arquitectura de Computadores I. Práctica 3 Opciones de Optimización de GCC (I) -O1: Con esta opción, el compilador modifica las siguientes alternativas de compilación -fthread-jumps (comprueba si hay un salto a otra posición donde se encuentra otro salto y si se conoce la condición de salto se redirecciona el primero convenientemente). -fdefer-pop (evita hacer pop de los argumentos de llamada a una función cuando se retorna de la llamada a la función) -fdelayed-branch (en máquinas segmentadas con ‘delays slots’, intenta reordenar las instrucciones para eliminar ciclos desperdiciados). -fomit-frame-pointer (en máquinas que permiten la depuración sin utilizar punteros de pila, indica que no se almacene el puntero de pila en un registro en el caso de funciones que no lo necesiten. Con esto se ahorran las correspondientes instrucciones de almacenamiento y recuperación del puntero). -O2: Con esta opción se activan todas las alternativas de optimización menos el desenrollado y la opción –finline-functions (integra las funciones sencillas en donde están sus llamadas). -O3: Se activan todas las alternativas de optimización. -Os: Se activan todas las alternativas de –O2 que no suelen incrementar el tamaño del código
65 65 Arquitectura de Computadores I. Práctica 3 Opciones de Optimización de GCC (II) Otros ejemplos (además de los indicados anteriormente): -fforce-mem: fuerza a los operandos en memoria a copiarse en los registros antes de hacer operaciones aritméticas con ellos. Esta alternativa se activa con –O2. -fforce-addr: fuerza que las direcciones de memoria constantes se copien en registros antes de hacer operaciones aritméticas sobre ellas. Se activa con –O2. -ffast-math: se permite al gcc que viole algunas reglas y/o especificaciones ANSI o IEEE para conseguir más velocidad. Por ejemplo se asume que los argumentos de la función sqrt son no-negativos y que ningún valor en coma flotante es NaN. Descripción de las opciones de optimización de gcc (II) Manual de gcc: http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc.pdf
66 66 Arquitectura de Computadores I. Práctica 3 Ejemplo de Códigos en Ensamblador generados con la opción –S (para suma_prod())
67 67 Arquitectura de Computadores I. Práctica 3 Para consultar………… Repertorio de Instrucciones. Listado de instrucciones y número de microoperaciones. Información precisa sobre la microarquitectura: http://www.intel.com/design/PentiumIII/manuals/ http://developer.intel.com/design/pentium4/manuals/index.htm Compilador de C (gcc para DOS/WINDOWS). http://www.delorie.com/djgpp/ Ensamblador (formato AT&T), directivas, arquitectura, convenciones de llamada: http://docencia.ac.upc.edu/FIB/PROSO/index_files/Annex-%20Asm.pdf http://www.delorie.com/djgpp/doc/ug/asm/about-386.html http://en.wikibooks.org/wiki/Reverse_Engineering/Assemblers#.28x86.29_AT.26T_Syntax_Ass emblers http://www.delorie.com/djgpp/doc/ug/asm/calling.html Página de Compilación para Pentium http://www.iti.cs.tu-bs.de/soft/www.goof.com/pcg/