Sistemas Computacionales Ejercicios resueltos y planteados Mario Medina C. Depto. Ing. Eléctrica Facultad de Ingeniería Universidad de Concepción 2014 ii Prefacio Esta es una colección de ejercicios de sistemas computacionales que espero sea de utilidad a aquellos alumnos empeñados en desarrollar las habilidades y competencias asociadas a esta materia. Muchos de ellos aparecen en los textos enumerados en la bibliografía de este documento; otros han sido creados por el autor para ser usados en tareas y exámenes. Es mi opinión que la única forma de aprender es haciendo. Se espera que los ejercicios planteados sean desarrollados por Uds., los alumnos. Por ello, en la mayoría de éstos, sólo se indica la solución final. Estoy siempre dispuesto a responder consultas sobre estos ejercicios, ya sea via correo electrónico o en persona. Asimismo, rogaría me hicieran llegar cualquier corrección o comentario a los ejercicios de este libro. Asi que, buena suerte, y provecho! Mario Medina C. mariomedina@udec.cl Índice general 1 Representación digital de información 1 2 Ensamblador Intel x86 y lenguaje C 11 3 Procesador Y86 35 4 Pipelining 45 5 Optimización de código 54 6 Sistemas de memoria 55 7 Planificación de procesos 67 iii Capítulo Representación digital de información 1.1 Suponga que un compilador C para un procesador de 32 bits representa los números negativos utilizando complemento a 2, y que utiliza desplazamientos aritméticos para los desplazamientos a la derecha. Suponga además que x e y son números enteros con signo arbitrarios, y que ux y uy son números enteros sin signo arbitrarios, tal que ux = (unsigned) x y que uy = (unsigned) y. Entonces, para cada una de las siguientes expresiones en lenguaje C, indique si la expresión siempre es verdadera. En caso contrario, indique posibles valores de x y/o y que la hagan falsa. a) ((x + y) << 4) + y - x == 17*y + 15*x b) ~x + ~y == ~(x + y) c) (int) (ux - uy) == -(y - x) d) ((x >> 1) << 1) <= x Solución a) Verdadera. 17*y equivale a y << 4 + y, y 15*x equivale a x << 4 − x. b) Falsa. Contraejemplo: ∼ x+ ∼ y = (−x − 1) + (−y − 1) = −(x + y) − 2 =∼ (x + y) + 1. c) Verdadera d) Verdadera 1 1 Capítulo 1: Representación digital de información 2 1.2 Suponga que se dispone de un procesador de 32 bits, que usa aritmética de complemento a 2 para la representación de números con signo. Se utilizan desplazamientos lógicos para los enteros sin signo, y desplazamientos aritméticos para los enteros con signo. Suponga además que se sabe que x e y son enteros con signo, y que ux y uy son enteros sin signo. Entonces, para cada una de las siguientes expresiones, indique si son verdaderas para todos los valores posibles de x e y. En caso que sean falsas, indique los valores de x y/o y para los cuales las expresiones no se cumplen. a) (x >= 0) || ((2*x) < 0) b) (x & 7) != 7 || (x << 30 < 0) c) (x * x) >= 0 d) x < 0 || -x <= 0 e) x > 0 || -x >= 0 f ) x*y == ux*uy g) ~x*y +ux*uy == -y Solución a) Falsa. Sea x = −232 . Entonces, 2*x = 0. b) Verdadera. Si (x & 7) != 7 es falsa, entonces el bit x2 = 1. Al desplazar x 30 bits a la izquierda, el bit x2 pasa a ser el bit de signo. c) Falsa. Si x = 0xFFFF, lo que equivale a 65535, entonces se tiene que x*x = 0xFFFE0001, es decir, −131071. d) Verdadera. e) Falsa. Sea x = −232 . Entonces, x y -x son negativas. f ) Verdadera. g) Verdadera. 1.3 Suponga que las variables x, f y d son de tipos int, float y double, respectivamente. Suponga además que tanto f como d son diferentes de ∞, −∞ y N aN . Para cada una de las siguientes expresiones, indique si son verdaderas para todos los valores posibles de las variables involucradas. En caso que sean falsas, indique los valores de dichas variables para los cuales las expresiones no se cumplen. 3 Capítulo 1: Representación digital de información a) x == (int)(float) x b) x == (int)(double) x c) f == (float)(double) f d) d == (float) d e) f == -(-f) f ) 2/3 == 2/3.0 g) (d >= 0.0) || ((d*2) < 0.0) h) (d + f) - d == f Solución a) Falso. Contraejemplo: x = 232 . b) Verdadero. c) Verdadero. d) Falso. Contraejemplo: x = 1040 . e) Verdadero. f ) Falso. g) Verdadero. h) Falso. Contrajemplo: d = +∞ y f = 1. 1.4 Sea un procesador de 32 bits que utiliza complemento a 2. Entonces, para las expresiones siguientes, rellene la siguiente tabla con el tipo del resultado (signed ó unsigned) y si la expresión es F ó V . Expresión −2147483647 − 1 −2147483647 − 1 −2147483647 − 1U −2147483647 − 1 −2147483647 − 1U == < < < < Tipo 2147483648U 2147483647 2147483647 −2147483647 −2147483647 Solución La siguiente tabla muestra la solución. FóV 4 Capítulo 1: Representación digital de información Expresión −2147483647 − 1 −2147483647 − 1 −2147483647 − 1U −2147483647 − 1 −2147483647 − 1U == < < < < 2147483648U 2147483647 2147483647 −2147483647 −2147483647 Tipo FóV unsigned signed unsigned signed unsigned true true false true true 1.5 En una máquina de 32 bits, sean x, y y z enteros de valor arbitrario. Entonces, dado el siguiente código, unsigned ux double dx = double dy = double dz = = (unsigned int) x; (double) x; (double) y; (double) z; indique cuáles de las siguientes expresiones siempre son verdaderas. En caso contrario, debe indicar un contraejemplo para recibir el puntaje asociado. a) (x >= 0) || (x < ux) b) dx + dy == (double) (y + x) c) dx + dy + dz == dz + dy + dx d) dx*dy*dz == dz*dy*dx Solución a) Verdadero b) Falso. Contraejemplo: x, y = 231 − 1. c) Falso. Contraejemplo: x = 3.14, y = DBL_MAX y z = −DBL_MAX. √ √ d) Falso. Contraejemplo: x = 0.01, y = DBL_MAX y z = DBL_MIN. donde DBL_MIN y DBL_MAX son los valores mínimos y máximos representables en un número de punto flotante de doble precisión. 1.6 Considere la siguiente representación de punto flotante de 16 bits basada en el estándar IEEE-754: El bit más significativo es el bit de signo Los 7 bits siguientes son el exponente, que se almacena como e+63 5 Capítulo 1: Representación digital de información Los últimos 8 bits son la mantisa, que se codifica con la técnica del 1 implícito. Siguiendo reglas de representación similares a IEEE-754, indique las representaciones hexadecimales de los siguientes números: a) −0 b) Menor número positivo mayor que 1 c) Mayor número positivo denormalizado d) −∞ e) NotANumber Solución La siguiente tabla muestra la solución del problema. Número −0 Menor número positivo mayor que 1 Mayor número positivo denormalizado −∞ NotANumber Signo Mantisa Exponente 1 0 0 1 0 0x00 0x01 0xFF 0x00 0x01 0x00 0x01 0x00 0x7F 0x7F 1.7 Considere la siguiente representación de punto flotante de 8 bits basada en el estándar IEEE-754: El bit más significativo es el bit de signo Los 3 bits siguientes son el exponente E, que se almacena como e+3 Los últimos 4 bits son la mantisa M. El valor del número se codifica como V = (−1)s × 1.M × 2E . Siguiendo reglas de representación similares a IEEE-754, indique las representaciones binarias de los siguientes números: a) −0 b) El mayor número positivo normalizado c) El menor número negativo denormalizado 6 Capítulo 1: Representación digital de información d) ∞ e) 1 f ) NotANumber Además, indique qué valores son representados por las siguientes secuencias de bits: a) 0 100 0101 b) 1 010 0110 c) 0 111 1110 d) 1 000 0000 Solución Número −0 Mayor número positivo normalizado Menor número negativo denormalizado ∞ 1 NotANumber Signo Exponente Mantisa 1 0 1 0 0 0 000 110 000 111 011 111 0000 1111 0001 0000 0000 0001 a) 0 100 0101 = 2.625 b) 1 010 0110 = 0.6875 c) 0 111 1110 = NotANumber d) 1 000 1111 = −0.1171875 1.8 La función clasifica_float() recibe como argumento un número de precisión simple de formato IEEE-754, y debe clasificarlo. Para ello, complete el código siguiente. void clasifica_float(float f) { // unsigned u tiene el mismo patron de bits que f unsigned u = *(unsigned *) &f; // Identifica las partes de f int signo = u >> 31; // Bit de signo Capítulo 1: Representación digital de información int exp = ________________________; int frac = _______________________; // Expresiones siguientes dependen de signo, exp y frac if (___________________________) printf("Cero positivo o negativo\n"); else if (___________________________) printf("Numero denormalizado\n"); else if (___________________________) printf("Infinito positivo o negativo\n"); else if (___________________________) printf("Not A Number\n"); else if (___________________________) printf("Mayor que -1.0 y menor que 1.0\n"); else if (___________________________) printf("Menor o igual a -1.0\n"); else printf("Mayor o igual a 1.0\n"); } Solución El siguiente código implementa la función pedida. void clasifica_float(float f) { // Unsigned u tiene el mismo patron de bits que float f unsigned u = *(unsigned *) &f; //* int int int Identifica las partes de f signo = u >> 31; exp = (u >> 23) & 0xFF; frac = u & 0x7FFFFF; // Expresiones siguientes dependen de signo, exp y frac if (exp == 0x00 && frac == 0x00) printf("Cero positivo o negativo\n"); else if (exp == 0x00 && frac != 0x00) printf("Numero denormalizado\n"); else if (exp == 0xFF && frac == 0x00) printf("Infinito positivo o negativo\n"); else if (exp == 0xFF && frac != 0x00) printf("Not A Number\n"); else if (exp < 0x7F) printf("Mayor que -1.0 y menor que 1.0\n"); else if (exp >= 0x7F && signo == 0x01) printf("Menor o igual a -1.0\n"); 7 Capítulo 1: Representación digital de información 8 else printf("Mayor o igual a 1.0\n"); } 1.9 Escriba una función en C llamada bool isLittleEndian(void) tal que retorne true si es compilada y ejecutada en un procesador de arquitectura Little Endian, y que retorne false si es compilada y ejecutada en un procesador de arquitectura Big Endian. Solución El siguiente código muestra una posible solución. bool isLittleEndian(void) { int x = 0x00000001; char *p = &x; return (*p == 1); } 1.10 Suponga que existen dos formatos de representación de números de punto flotante de 9 bits similares al estándar IEEE. Éstos son: Formato A : 1 bit de signo s 5 bits de exponente e, el que se almacena como e + 15 3 bits de mantisa m Formato B : 1 bit de signo s 4 bits de exponente e, el que se almacena como e + 7 4 bits de mantisa m a) Para ambos formatos, indique el patrón de bits correspondiente a 1) 2) 3) 4) 5) el mayor número positivo normalizado el menor número positivo normalizado el menor número positivo denormalizado uno infinito positivo b) La siguiente tabla muestra 5 patrones de bit en formato A. Para cada uno, indique: 9 Capítulo 1: Representación digital de información 1) Su valor, ya sea como entero, potencia o expresado como fracción 2) El patrón equivalente en formato B 3) El valor del patrón en formato B Es posible que en algún caso, Ud. no pueda representar exactamente el valor pedido en formato B. En ese caso, aproxime siempre su resultado hacia +∞. Formato A Bits Valor s e m 1 01111 001 0 10110 011 1 00111 010 0 00000 111 1 11100 000 0 10111 100 Formato B Bits Valor s e m Solución a) La siguiente tabla muestra los patrones de bits solicitados y los valores asociados. Mayor número pos. normalizado Menor número pos. normalizado Menor número pos. denormalizado Uno Infinito positivo Mayor número pos. normalizado Menor número pos. normalizado Menor número pos. denormalizado Uno Infinito positivo Formato A Valor 0 11110 111 0 00001 000 0 00000 001 0 01111 000 0 11111 000 (2 − 2−3 )215 2−14 2−17 +1.0 +∞ Formato B Valor 0 1110 1111 0 0001 0000 0 0000 0001 0 0111 0000 0 1111 0000 (2 − 2−4 )27 2−6 2−10 +1.0 +∞ 10 Capítulo 1: Representación digital de información b) La siguiente tabla muestra los valores y patrones de bits solicitados. En la tercera línea, es necesario convertir un número normalizado en formato A a un número denormalizado en formato B. Asimismo, las líneas 4 y 5 requieren aproximar el resultado en formato B hacia +∞. Formato A Bits Valor 1 01111 001 0 10110 011 1 00111 010 0 00000 111 1 11100 000 0 10111 100 − 98 176 5 − 1024 7 × 2−17 -8192 384 Formato B Bits Valor 1 0111 0010 0 1110 0110 1 0000 0101 0 0000 0001 1 1110 1111 0 1111 0000 − 98 176 5 − 1024 2−10 −248 +∞ Capítulo Ensamblador Intel x86 y lenguaje C 2.1 La función void decode1(int *xp, int *yp, int *zp) es compilada al siguiente código ensamblador. movl movl movl movl movl movl movl movl movl 8(%ebp), %edi 12(%ebp), %ebx 16(%ebp), %esi (%edi), %eax (%ebx), %edx (%esi), %ecx %eax, (%ebx) %edx, (%esi) %ecx, (%edi) Los parámetros xp, yp y zp están almacenados a 8, 12 y 16 bytes de la dirección de memoria apuntada por %ebp. Escriba el código C equivalente al código ensamblador mostrado. Solución El siguiente código muestra una posible solución. void decode1(int *xp, int *yp, int *zp) { int tx = *xp; int ty = *yp; int tz = *zp; 11 2 Capítulo 2: Ensamblador Intel x86 y lenguaje C 12 *yp = tx; *zp = ty; *xp = tz; } 2.2 El lenguaje ensamblador Intel x86 posee la instrucción leal s, d, que almacena en la dirección destino d la dirección efectiva calculada para la dirección fuente s. Esta instrucción puede ser utilizada para realizar cálculos de la forma a << k + b, donde k puede tomar valores 1, 2 o 3. Indique todos los posibles múltiplos del registro %eax que pueden almacenarse en el registro %edx utilizando esta instrucción, mostrando el formato de la instrucción leal s, d para cada uno. Solución Los múltiplos del registro %eax que se pueden calcular con la instrucción leal s, d se muestran en la siguiente tabla. Instrucción leal leal leal leal leal leal leal (%eax), %edx (%eax, %eax), %edx (%eax, %eax, 2), %edx (,%eax, 4), %edx (%eax, %eax, 4), %edx (,%eax, 8), %edx (%eax, %eax, 8), %edx Acción %edx ← %eax %edx ← 2%eax %edx ← 3%eax %edx ← 4%eax %edx ← 5%eax %edx ← 8%eax %edx ← 9%eax 2.3 Sea el siguiente código ensamblador X86: pushl %ebp movl %esp, %ebp movl 8(%ebp), %edx movl 12(%ebp), %eax movl %ebp, %esp movl (%edx), %edx addl %edx, (%eax) movl %edx, %eax popl %ebp ret Rellene el siguiente fragmento de código C para que sea equivalente al código ensamblador anterior. Capítulo 2: Ensamblador Intel x86 y lenguaje C 13 int fun(int *ap, int *bp) { ______ = ______; ______ = ______; return ______; } Solución El siguiente código C es equivalente al código ensamblador X86 mostrado. int fun(int *ap, int *bp) { int temp = *ap; *bp += *ap; return temp; } 2.4 El siguiente código C int dw_loop(int x, int y, int n) { do { x += n; y *= n; n--; } while ((n > 0) & (y < n)); return x; } es traducido al siguiente código ensamblador, donde las variables x, y y n están a 8, 12 y 16 bytes de (%ebp), respectivamente. movl 8(%ebp), %esi movl 12(%ebp), %ebx movl 16(%ebp), %ecx .L6: imull %ecx, %ebx addl %ecx, %esi decl %ecx testl %ecx, %ecx setg %al cmpl %ecx, %ebx setl %dl andl %edx, %eax Capítulo 2: Ensamblador Intel x86 y lenguaje C 14 testb $1, %al jne .L6 a) Genere una tabla de uso de registros b) Comente el código ensamblador c) Identifique la condición y el cuerpo del ciclo do-while() y las líneas correspondientes del código ensamblador. Solución a) La tabla de registros es Registro Variable Valor Inicial %esi %ebx %ecx x y n x y n b) El código ensamblador comentado es movl 8(%ebp), %esi movl 12(%ebp), %ebx movl 16(%ebp), %ecx .L6: imull %ecx, %ebx addl %ecx, %esi decl %ecx testl %ecx, %ecx setg %al cmpl %ecx, %ebx setl %dl andl %edx, %eax testb $1, %al jne .L6 // // // // // // // // // // // // // Almacena x en %esi Almacena y en %ebx Almacena n en %ecx y *= n x += n n-Test n n > 0 Compara y y n y < n (n > 0) & (y < n) Test LSB If != 0, ir a lazo El compilador reconoce que las condiciones (n > 0) y (y < n) se evalúan sólo a 0 o 1, y que entonces sólo es necesario examinar el bit menos significativo del resultado para saber el valor de la condición compuesta. c) El cuerpo del ciclo está en las líneas 4 a 6 del código C y en las líneas 6 a 8 del código ensamblador. La condición a evaluar está en la línea 7 del código C y en las líneas 9 a 14 del código ensamblador. Capítulo 2: Ensamblador Intel x86 y lenguaje C 15 2.5 En el siguiente código C, se han omitido los operadores de comparación y los tipos de datos de los cast. char ctest(int a, int b, { char t1 = a char t2 = b char t3 = ( ) c char t4 = ( ) a char t5 = c char t6 = a int c) __ __ ( __ ( __ ( __ __ b; ) a; ) a; ) c; b; 0; return t1 + t2 + t3 + t4 + t5 + t6; } A partir de ese código, el compilador C genera el siguiente código ensamblador: movl 8(%ebp), %ecx movl 12(%ebp), %esi cmpl %esi, %ecx setl %al cmpl %ecx, %esi setb -1(%ebp) cmpw %cx, 16(%ebp) setge -2(%ebp) movb %cl,%dl cmpb 16(%ebp),%dl setne %bl cmpl %esi, 16(%ebp) setg -3(%ebp) testl %ecx, %ecx setg %dl addb -1(%ebp),%al addb -2(%ebp),%al addb %bl, %al addb -3(%ebp),%al addb %dl, %al movsbl %al, %eax Suponga que las variables a, b y c están a 8, 12 y 16 bytes de (%ebp), respectivamente. Indique las partes faltantes del código C. Solución Un posible código C equivalente es: Capítulo 2: Ensamblador Intel x86 y lenguaje C char ctest(int a, int b, int c) { char t1 = a < char t2 = b < (unsigned) char t3 = (short) c >= (short) char t4 = (char) a != (char) char t5 = c > char t6 = a > 16 b; a; a; c; b; 0; return t1 + t2 + t3 + t4 + t5 + t6; } 2.6 Sea el siguiente programa en C: unsigned int c, v; for (c = 0; v; c++) { v &= (v - 1); } a) Identifique qué hace este programa. b) Escriba código en ensamblador Intel X86 de 32 bits equivalente. Suponga que el registro %ecx contiene la variable c, y que el registro %ebx contiene la variable v. Su código debe retornar el resultado en el registro %eax. Solución a) El programa cuenta el número de bits en 1 del argumento v. b) A continuación, se muestra un programa en ensamblador que realiza la misma función. xorl %ecx, %ecx cmpl $0, %ebx je fin lazo: movl %ebx, decl %eax incl %ecx andl %eax, %ebx jnz lazo movl %ecx, %eax # Hace 0 el contador c # Si v = 0, ir a fin %eax # Calcula v - 1 # Incrementa el contador # Calcula v = v&(v - 1) # Si no es 0, repetir # Retorna c en %eax Capítulo 2: Ensamblador Intel x86 y lenguaje C 17 2.7 Considere el siguiente código ensamblador: lazo: pushl %ebp movl %esp, %ebp movl 8(%ebp), %ecx movl 12(%ebp), %edx xorl %eax, %eax cmpl %edx, %ecx jle Fin Otro: decl %ecx incl %edx incl %eax cmpl %edx, %ecx jg Otro Fin: incl %eax leave ret Se sabe que el código anterior corresponde a un código C de la forma: int lazo(int x, int y) { int result; for(________; ________; result++) { ________; ________; } ________; return result; } Reescriba el código C anterior, rellenando las líneas indicadas. Utilice sólo las variables x, y y result. No utilice los nombres de los registros! Solución Para analizar el código ensamblador, comenzaremos por comentarlo: lazo: pushl %ebp movl %esp, %ebp movl 8(%ebp), %ecx movl 12(%ebp), %edx xorl %eax, %eax cmpl %edx, %ecx # # # # Lee x en %ecx Lee y en %edx result en %eax es igual a 0 Compara x con y (x - y) Capítulo 2: Ensamblador Intel x86 y lenguaje C jle Fin # Salta si x es <= a y Otro: decl %ecx incl %edx incl %eax cmpl %edx, %ecx jg Otro # # # # # Fin: # Incrementa result incl %eax leave ret 18 Decrementa x Incrementa y Incrementa result Compara x con y (x - y) Repite si x > y Dado que la convención de paso de parámetros de C dice que éstos se pasan a través de la pila de derecha a izquierda, es claro que x está en la posición -8(%ebp) y que y está en la posición -12(%ebp). Entonces, se puede ver que el código C equivalente es: int lazo(int x, int y) { int result; for(result = 0; x > y; result++) { x--; y++; } result++; return result; } 2.8 Considere el siguiente código C, donde M y N son constantes definidas con #define en alguna parte del código. int mat1[M][N]; int mat2[N][M]; int sumaElementos(int i, int j) { return mat1[i][j] + mat2[i][j]; } Ese código C genera el siguiente código en lenguaje de ensamblador. Suponga que el argumento i está en 8(%ebp), y que el argumento j está en 12(%ebp). Suponga además que mat1 y mat2 corresponden a las direcciones iniciales en memoria de las matrices correspondientes. Capítulo 2: Ensamblador Intel x86 y lenguaje C sumaElementos: 19 pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax movl 12(%ebp), %ecx sall $2, %ecx leal 0(, %eax, 8), %edx subl %eax, %edx leal (%eax, %eax, 4), %eax movl mat2(%ecx, %eax, 4), %eax addl mat1(%ecx, %edx, 4), %eax leave ret Cuáles son los valores de M y N ? Solución A continuación, se muestra el código ensamblador comentado. 1 2 3 4 5 6 7 8 9 10 11 12 pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax movl 12(%ebp), %ecx sall $2, %ecx leal 0(, %eax, 8), %edx subl %eax, %edx leal (%eax, %eax, 4), %eax movl mat2(%ecx, %eax, 4), %eax addl mat1(%ecx, %edx, 4), %eax leave ret // // // // // // // // // // Almacena %ebp en la pila Actualiza puntero base %eax <- i %ecx <- j %ecx <- 4j %edx <- 8i %edx <- 8i - i = 7i %eax <- 5*%eax = 5i %eax <- M[mat2 + 4j + 20i] %eax <- %eax*M[mat1 + 4j + 28i] En las líneas 9 y 10, se accede a las posiciones mat1 + 28i + 4j y mat2 + 20i + 4j. Dado que cada elemento de la matriz es un entero de 32 bits, se deduce entonces que M = 5 y N = 7. 2.9 Considere el siguiente código C, donde M y N son constantes definidas con #define en alguna parte del código. int mat1[M][N]; int mat2[N][M]; int copiaElemento(int i, int j) { return mat1[i][j] = mat2[j][i]; } Capítulo 2: Ensamblador Intel x86 y lenguaje C 20 Ese código C genera el siguiente código en lenguaje de ensamblador. Recuerde que el lenguaje C almacena las matrices por filas. Suponga que el argumento i está en 8( %ebp), y que el argumento j está en 12( %ebp). Suponga además que mat1 y mat2 corresponden a las direcciones iniciales en memoria de las matrices correspondientes. copiaElemento: pushl %ebp movl %esp, %ebp pushl %ebx movl 8(%ebp), %ecx movl 12(%ebp), %ebx leal (%ecx,%ecx,8), %edx sall $2, %edx movl %ebx, %eax sall $4, %eax subl %ebx, %eax sall $2, %eax movl mat2(%eax, %ecx, 4), %eax movl %eax, mat1(%edx, %ebx, 4) popl %ebx leave ret Cuáles son los valores de M y N ? Solución A continuación, se muestra el código ensamblador X86 comentado. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pushl %ebp pushl %ebx movl 8(%ebp), %ecx movl 12(%ebp), %ebx leal (%ecx,%ecx,8), %edx sall $2, %edx movl %ebx, %eax sall $4, %eax subl %ebx, %eax sall $2, %eax movl mat2(%eax, %ecx, 4), %eax movl %eax, mat1(%edx, %ebx, 4) popl %ebx leave ret // // // // // // // // // // // // // // Forma marco de activacion Almacena %ebx en la pila %ecx <- i %ebx <- j %edx <- 9i %edx <- 4*9i %eax <- j %eax <- 16*j %eax <- 15*j %eax <- 4*15j %eax <- M[mat2 + 4*(15*j + i)] M[mat1 + 4*(9*i + j)] <- %eax Recupera %ebx de la pila Elimina marco de activacion Capítulo 2: Ensamblador Intel x86 y lenguaje C 21 De estudiar los códigos ensamblador y en lenguaje C, se ve que en las líneas 11 y 12 se accede a las posiciones mat2 + 4(15*j + i) y mat1 + 4*(9*i + j). Dado que cada elemento de la matriz es un entero de 32 bits, se tiene que M = 15 y N = 9. 2.10 El producto punto de vectores es una operación de reducción que opera sobre dos vectores de la forma que se indica: a • b = a1 b1 + a2 b2 + . . . + an−1 bn−1 + an bn La función int productoPunto(int A[], int B[], int n) en lenguaje C retorna el producto punto de dos vectores A y B de largo n. int productoPunto(int A[], int B[], int n) { int i; int suma = 0; for (i = 0; i < n; i++) suma += A[i]*B[i]; return suma; } Escriba una función en lenguaje ensamblador equivalente. Solución Primero, debemos transformar el código C a una forma más apropiada para su implementación en lenguaje ensamblador. Es decir, transformar el ciclo for a un ciclo while, y transformar los accesos a vector vía índice a notación de punteros. int productoPunto(int *A, int *B, int n) { int i = 0; int suma = 0; while (i < n) { suma = suma + *(A + 4*i) * *(B + 4*i); i++; } return suma; } Luego, traducimos este código C a ensamblador, como se muestra a continuación. Capítulo 2: Ensamblador Intel x86 y lenguaje C productoPunto: pushl %ebp movl %esp, %ebp pushl %esi pushl %edi xorl %edx, %edx xorl %eax, %eax movl 8(%ebp), %esi movl 12(%ebp), %edi Lazo: cmpl %edx, 16(%ebp) je Fin movl (%esi, %edx, 4), %ecx imull (%edi, %edx, 4), %ecx addl %ecx, %eax incl %edx jmp Lazo Fin: popl %edi popl %esi leave ret 22 // Prepara la pila // // // // // // Almacena registro %esi Almacena registro %edi i <- 0 suma <- 0 %esi <- A %edi <- B // // // // // // // Compara n con i Si son iguales, ir a Fin %ecx <- *(A + 4*i) %ecx <- *(B + 4*i)*%ecx suma <- %ecx i <- i + 1 Repetir // Recupera registro %edi // Recupera registro %esi // Recupera valores de pila 2.11 El Máximo Común Divisor entre dos números puede calcularse recursivamente como se muestra a continuación: int mcd(int m, int n) { if (n == 0) return m; if (m == 0) return n; mcd(n, m%n); } Escriba ahora una función equivalente en ensamblador. Solución Se muestra una posible solución. Como sólo hace uso del registro %eax que es Caller Save y que no retiene valores importantes entre invocaciones recursivas, no es necesario salvarlo en la pila. mcd: pushl %ebp movl %esp, %ebp // Prepara la pila Capítulo 2: Ensamblador Intel x86 y lenguaje C cmpl $0, 12(%ebp) jne SIGUE1 movl 8(%ebp), %eax jmp FIN SIGUE1: cmpl $0, 8(%ebp) jne SIGUE2 movl 12(%ebp), %eax jmp FIN SIGUE2: movl 12(%ebp), %eax ctld idivl 8(%ebp) pushl %edx movl 12(%ebp), %eax pushl %eax call mcd FIN: leave ret 23 // Compara n con 0 // Si n == 0, retornar m // Compara m con 0 // Si m == 0, retornar n // %eax <- n // Extiende m a 64 bits // Calcula m/n // m%n se agrega a la pila // %eax <- n // Agrega n a la pila 2.12 Sea la matriz A de dimensiones 3 ∗ 3, como se muestra: a b c A = d e f g h i El determinante |A| de dicha matriz está dado por |A| = aei − af h − bdi + bf g + cdh − ceg. Escriba una función en ensamblador Intel x86 det3x3(int A[][3]) que reciba como argumento la dirección inicial de la matriz en 8(%ebp) y retorne el determinante de dicha matriz en el registro %eax. Suponga además que el rango de los elementos de A es tal que le basta con usar la operación de multiplicación imull S, D. Solución Se muestra un posible código ensamblador para la función pedida. det3x3: pushl %ebp movl %esp, %ebp pushl %ebx movl 8(%ebp), %ebx movl 16(%ebx), %ecx imull 32(%ebx), %ecx // Prepara la pila // // // // Almacena registro %ebx en pila Lee direccion de matriz A Lee valor e en %ecx Calcula ei Capítulo 2: Ensamblador Intel x86 y lenguaje C movl 20(%ebx), %edx imull 28(%ebx), %edx subl %edx, %ecx imull (%ebx), %ecx movl %ecx, %eax // // // // // Lee valor f en %edx Calcula fh Calcula ei - fh Calcula a(ei - fh) Acumula resultado en %eax movl 20(%ebx), %ecx imull 24(%ebx), %ecx movl 12(%ebx), %edx imull 32(%ebx), %edx subl %edx, %ecx imull 4(%ebx), %ecx addl %ecx, %eax // // // // // // // Lee valor f en %ecx Calcula fg Lee valor d en %edx Calcula di Calcula fg - di Calcula b(fg - di) Acumula resultado en %eax movl 12(%ebx), %ecx imull 28(%ebx), %ecx movl 16(%ebx), %edx imull 24(%ebx), %edx subl %edx, %ecx imull 8(%ebx), %ecx addl %ecx, %eax // // // // // // // Lee valor d en %ecx Calcula dh Lee valor e en %edx Calcula eg Calcula dh - eg Calcula c(dh - eg) Acumula resultado en %eax popl %ebx leave ret // Recupera valor de %ebx 24 2.13 Suponga que los 4 bytes de un entero int de 32 bits se numeran desde el menos significativo (0) al más significativo (3). a) Escriba una función en C unsigned int reemplazaByte(unsigned int x, unsigned int i, unsigned char b) que reemplace el iésimo byte del entero x por el byte b dado. Por ejemplo, escribir reemplazaByte(0x12345678, 2, 0xAB) deberá retornar el entero 0x12AB5678, mientras que reemplazaByte(0x12345678, 0, 0xAB) deberá retornar el entero 0x123456AB. b) Escriba ahora una función equivalente en lenguaje ensamblador x86. Recuerde que la convención de paso de argumentos de C establece que éstos se almacenan en la pila de derecha a izquierda. No olvide respetar las convenciones Caller-Save y Callee-Save. Solución a) El siguiente código C implementa la función pedida. Se utiliza una máscara para anular los bits a reemplazar, y luego se suma el nuevo byte, desplazado i bytes a la izquierda. Capítulo 2: Ensamblador Intel x86 y lenguaje C 25 unsigned int reemplazaBytes(unsigned int x, unsigned int i, unsigned char b) { unsigned int mascara = 0xFF << (i*8); return (x & ~mascara) | (b << (i*8)); } b) El siguiente código ensamblador implementa la función pedida. Para acceder al byte a modificar, se utiliza la variable i como un desplazamiento dentro de la copia del entero x que está en la pila. Luego, esta versión modificada de x se lee al registro %eax para ser usado como valor de retorno. Como los únicos registros utilizados son %eax y %ecx, y éstos son Caller-Save, no es necesario almacenarlos en la pila. pushl %ebp movl %esp, %ebp movb 16(%ebp), %al movl 12(%ebp), %ecx movb %al, 8(%ebp, %ecx) movl 8(%ebp), %eax leave ret # Crea marco de activacion # # # # # Lee byte b a en %al Lee i b -> i-esimo byte de x Lee valor modificado como valor de retorno 2.14 Se desea obtener la traspuesta de una matriz cuadrada M de dimensiones N × N . Para ello, Ud. debe realizar las siguientes tareas: a) Primero, escriba una función void swap(int *a, int *b) en C que intercambie los contenidos de dos punteros a enteros. b) Luego, escriba esta función en lenguaje de ensamblador x86. Recuerde que la convención de paso de argumentos de C establece que éstos se almacenan en la pila de derecha a izquierda. No olvide respetar las convenciones Caller-Save y Callee-Save. c) Luego, escriba una función en C llamada void traspuesta(int *M, int N) que recibe dos argumentos. El primero es un puntero a un vector de N 2 enteros, utilizado para simular el acceso a una matriz de N × N enteros, tal como se vió en clases. El segundo argumento es la dimensión de la matriz. Su función debe utilizar la función swap() escrita anteriormente. d) Implemente ahora esta función en lenguaje ensamblador x86. Capítulo 2: Ensamblador Intel x86 y lenguaje C 26 Solución a) El siguiente código C implementa la función deseada. No es la implementación más eficiente, pero refleja la implementación en lenguaje ensamblador que se muestra a continuación. void swap(int *a, int *b) { int aTemp = *a; int bTemp = *b; *a = bTemp; *b = aTemp; } b) El siguiente código ensamblador implementa la función swap()} del punto anterior. Utiliza el registro %ebx, el cual es Callee-Save. Por ello, el valor de este registro se almacena en la pila al comienzo de la función y se recupera al final. pushl %ebp movl %esp, %ebp pushl %ebx movl 8(%ebp), %eax movl 12(%ebp), %edx movl (%eax), %ecx movl (%edx), %ebx movl %ecx, (%edx) movl %ebx, (%eax) popl %ebx leave ret # Crea marco de activacion # # # # # # # # Almacena registro %ebx Lee a al registro %eax Lee b al registro %edx Lee *a al registro %ecx Lee *b al registro %ebx Escribe *a a puntero b Escribe *b a puntero a Recupera registro %ebx c) A continuación, se muestra una implementación en C de la función traspuesta() solicitada. void traspuesta(int *M, int N) { int i, j; for (i = 0; i < N; i++) { for (j = 0; j < i; j++) { swap(&M[i*N + j], &M[j*N + i]); } } } d) El siguiente código muestra una posible implementación en lenguaje ensamblador de esta función. Capítulo 2: Ensamblador Intel x86 y lenguaje C pushl %ebp movl %esp, %ebp xorl %eax, %eax lazo1: xorl %ecx, %ecx lazo2: movl 12(%ebp), %edx mull %ecx, %edx addl %eax, %edx sall $2, %edx addl 8(%ebp), %edx pushl %edx movl 12(%ebp), %edx mull %eax, %edx addl %ecx, %edx sall $2, %edx addl 8(%ebp), %edx pushl %edx call swap addl $8, %esp incl %ecx cmpl %ecx, %eax jl lazo2 incl %eax cmpl %eax, 12(%ebp) jl lazo1 leave ret 27 # Crea marco de activacion # # # # # # # # # # # # # # # # # # # # # Hace i igual a 0 Hace j igual a 0 Lee N Multiplica j*N Calcula j*N + i Multiplica 4*(j*N + i) Calcula M + 4*(j*N + i) Empila M + 4*(j*N + i) Lee N Multiplica i*N Calcula i*N + j Multiplica 4*(i*N + j) Calcula M + 4*i*N + j) Empila M + 4*(i*N + j) Invoca a funcion swap() Recupera posicion Incrementa j Compara j con i (i - j) Si j < i, ir a lazo2 Incrementa i Compara i con N (N - i) 2.15 Escriba una función unsigned int unosConsecutivos(unsigned int x) en lenguaje ensamblador que retorne el largo de la máxima secuencia de 1s consecutivos presentes en el argumento x. Es decir, si el argumento es 0xDBFF1F1C, la función debe retornar 10. No olvide respetar las convenciones Caller-Save y Callee-Save en su código. Sugerencia: piense en cómo escribiría esta función en C antes de comenzar a programar en ensamblador. Solución El siguiente código muestra una posible implementación de la función deseada en lenguaje de programación C. unsigned int UnosConsecutivos(unsigned int x) { int maxContador = 0; // Largo de maxima secuencia int contador = 0; // Contador de unos while (x != 0) { if ((signed int) x < 0) { Capítulo 2: Ensamblador Intel x86 y lenguaje C 28 contador++; if (maxContador < contador) maxContador = contador; } else contador = 0; x = x << 1; } return maxContador; } A su vez, el siguiente código muestra una implementación de la función anterior en lenguaje de ensamblador x86. unosconsecutivos: pushl %ebp movl %esp, %ebp movl 8(%ebp), %ecx xorl %eax, %eax xorl %edx, %edx # # # # # Almacena puntero base en la pila Redefine el puntero base Lee el argumento x a %ecx Hace 0 %eax (maxContador) Hace 0 %edx (contador) lazo: cmpl $0, %ecx # Compara x con 0 jz fin # si x es cero, va a fin jns pos # Salta si x es positivo incl %edx # Si es negativo, contador++ cmpl %edx, %eax # Compara maxContador y contador jge shift # Sigue si maxContador >= contador movl %edx, %eax # Actualiza valor de maxContador jmp shift shift: sall $1, %ecx # Desplaza x un bit a la izquierda jmp lazo pos: xorl %edx, %edx # Limpia el contador jmp shift # Si i es 0, ir al fin fin: movl %ebp, %esp # Recupera valores de %ebp y %esp popl %ebp ret 2.16 Sea la siguiente función switcher en lenguaje C, que utiliza un comando switch. int switcher(int a, int b, int c) { int result; switch(a) { case ________________: /* Caso A */ Capítulo 2: Ensamblador Intel x86 y lenguaje C c = ________________; case ________________: /* Caso result = ________________; break; case ________________: /* Caso case ________________: /* Caso result = ________________; break; case ________________: /* Caso result = ________________; break; default: result = ________________; 29 B */ C */ D */ E */ } return result; } Esta función es traducida en lenguaje ensamblador como se muestra a continuación: # Codigo de funcion switcher() # Suponga a en %ebp + 8, b en %ebp + 12, c en %ebp + 16 movl 8(%ebp), %eax cmpl $7, %eax ja .L2 jmp *.L7(, %eax, 4) .L2: movl 12(%ebp), %eax jmp .L8 .L5: movl $4, %eax jmp .L8 .L6: movl 12(%ebp), %eax xorl $15, %eax movl %eax, 16(%ebp) .L3: movl %16(%ebp), %eax addl $112, %eax jmp .L8 .L4: movl 16(%ebp), %eax addl 12(%ebp), %eax sall $2, %eax .L8: # Tabla de saltos .L7: .long .L3 .long .L2 .long .L4 .long .L2 .long .L5 .long .L6 Capítulo 2: Ensamblador Intel x86 y lenguaje C 30 .long .L2 .long .L4 Complete entonces la función switcher mostrada. Solución El siguiente código implementa la función pedida. int switcher(int a, int b, int c) { int result; switch(a) { case 5: /* Caso A */ c = b^15; case 0: /* Caso B */ result = c + 112; break; case 2: /* Caso C */ case 7: /* Caso D */ result = 4*(b + c); break; case 4: /* Caso E */ result = 4; break; default: result = b; } return result; } 2.17 Uno de los problemas intrínsecos de la representación de enteros en un número finito de bits es la ocurrencia de rebalses. a) Escriba, entonces, una función en C llamada sePuedenSumar(int x, int y) que retorne 1 si los enteros con signo de 32 bits x e y se pueden sumar sin posibilidad de rebalse, y 0 en caso contrario. b) Escriba ahora código en ensamblador Intel X86 de 32 bits equivalente. Suponga que el registro %ecx contiene la variable x, y que el registro %ebx contiene la variable y. Su código debe retornar el resultado 0 ó 1 en el registro %eax. Solución Para determinar si dos números enteros se pueden sumar sin que ocurra un error de representación, una técnica sencilla es realizar la suma Capítulo 2: Ensamblador Intel x86 y lenguaje C 31 y ver el signo del resultado: si la suma de dos números positivos da un resultado negativo, entonces ocurrió un rebalse. Asimismo, si la suma de dos números negativos da un resultado positivo, entonces ocurrió un rebalse. La suma de un número positivo y un número negativo no puede dar un rebalse. a) El siguiente código C implementa lo solicitado. bool sePuedenSumar(int x, int y) { int suma = x + y; int rebalse_neg = x < 0 && y < 0 && suma >= 0; int rebalse_pos = x >= 0 && y >= 0 && suma < 0; return !rebalse_pos && rebalse_neg; } b) El siguiente código Intel x86 implementa lo solicitado. movl $1, %eax movl %ecx, %esi movl %ebx, %edi xorl %esi, %edi js fin addl %ebx, %esi js sum_neg testl %ecx, %ecx js fin_no jmp fin sum_neg:testl %ecx, %ecx jns fin_no jmp fin fin_no: xorl %eax, %eax fin: # # # # # # # # # # # # # # # Hace 1 el registro de resultado %eax Copia x en %esi Copia y en %edi Calcula x XOR y Si < 0, x e y tienen distinto signo Calcula suma = x + y Salta si la suma es negativa Si x < 0 e y < 0 y la suma es positiva, no se pueden sumar Si x < 0 e y < 0, no se pueden sumar Si x >= 0 e y >= 0, y suma es negativa entonces no se pueden sumar En caso contrario, se pueden sumar Hace 0 el registro resultado %eax Fin del codigo solicitado 2.18 Escriba una función void invierteRistra(char *cp, unsigned int n) en lenguaje ensamblador que reciba como argumentos un puntero a una ristra de C y el número de caracteres de ésta, y que invierta esta ristra in situ, es decir, en las mismas direcciones de memoria. Suponga que los argumentos a la función están almacenados en 8( %ebp) y 12( %ebp), respectivamente. No olvide respetar las convenciones CallerSave y Callee-Save en su código. Sugerencia: piense en cómo escribiría esta función en C antes de comenzar a programar en ensamblador. Solución El siguiente código de ensamblador X86 muestra una posible solución. Capítulo 2: Ensamblador Intel x86 y lenguaje C 32 .type invierteRistra, @function invierteRistra: pushl %ebp # Almacena puntero base en la pila movl %esp, %ebp # Redefine el puntero base movl 8(%ebp), %eax # Lee direccion de ristra en %eax movl 12(%ebp), %edx # Lee el largo de la ristra en %edx addl %eax, %edx # Calcula posicion del ultimo subl $1, %edx # caracter de la ristra lazo1: cmpl %eax, %edx # Compara puntero con el final js finLazo1 # de la ristra y salta si es menor movb (%eax), %ch # Intercambia caracteres de ristra movb (%edx), %cl # utilizando registros intermedios movb %ch, (%edx) movb %cl, (%eax) incl %eax # Incrementa puntero a ristra en 1 decl %edx # Decrementa fin de ristra en 1 jmp lazo1 finLazo1: leave ret 2.19 Complete el siguiente código C en base al código ensamblador generado. int funcion(int x, int n) { int result = x; switch(n) { // Aqui falta el codigo que Ud. debe escribir } return result; } El código ensamblador Intel x86 generado se muestra a continuación, donde la columna de la izquierda indica la dirección en memoria de cada instrucción. Asimismo, la tabla de saltos está almacenada a partir de la dirección de memoria 2000. 1000: 1001: 1003: 1006: 1009: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax movl 12(%ebp), %edx subl $50, %edx Capítulo 2: Ensamblador Intel x86 y lenguaje C 33 1012: cmpl $5, %edx 1015: ja 1040 1017: jmp *2000(, %edx, 4) 1024: shll $2, %eax 1027: jmp 1043 1029: sarl $2, %eax 1032: jmp 1043 1034: leal (%eax, %eax, 2), %eax 1037: imull %eax, %eax 1040: addl $10, %eax 1043: popl %ebp 1044: ret . . . . . . . . 2000: 1024 2004: 1040 2008: 1024 2012: 1029 2016: 1034 2020: 1037 Solución El siguiente código muestra una implementación que cumple con lo solicitado. int funcion(int x, int n) { int result = x; switch(n) { case 50: case 52: result <<= 2; break; case 53: result >>= 2; break; case 54: result *= 3; case 55: result *= result; default: result += 10; } return result; } Capítulo 2: Ensamblador Intel x86 y lenguaje C 34 2.20 La función 91 es una función recursiva creada por John McCarthy que tiene la particularidad que se evalúa a 91 para todos los enteros n ≤ 100, y es igual a n − 10 para n ≥ 101. Su implementación en C es como sigue: int f91(int n) { if (n > 100) return n - 10; else return f91(f91(n + 11)); } Escriba esta función recursiva en lenguaje ensamblador X86. Suponga, como de costumbre, que esta función recibe un argumento entero en la pila, y que retorna su valor en el registro %eax. Solución El siguiente código muestra una posible implementación. .type f91, @function f91: pushl %ebp # movl %esp, %ebp # movl 8(%ebp), %eax # cmpl $100, %eax # jg fin_f91 # addl $11, %eax # pushl %eax # call f91 # popl %ebx # pushl %eax # call f91 # popl %ebx # jmp fin2 fin_f91: subl $10, %eax # fin2: movl %ebp, %esp # popl %ebp ret Almacena puntero base en la pila Redefine el puntero base Lee el argumento al registro %eax Compara argumento con 100 Si es 1, terminar En otro caso, incrementar %eax en 10 Almacena en la pila Llama a f91 Extrae argumento de la pila Almacena nuevo argumento en la pila Llama a f91 de nuevo Extrae argumento de la pila Si n es > 100, retorna n - 10 Recupera valores de %ebp y %esp Capítulo Procesador Y86 3.1 El algoritmo recursivo de Dijkstra para calcular el máximo común divisor puede escribirse como: m, if m = n mcd(m − n, n), if (m > n) mcd(m, n) = mcd(m, n − m), en otro caso Recuerde que el máximo común divisor entre dos números enteros positivos n y m es el mayor entero d que divide exactamente tanto n como m, es decir, m mod d = n mod d = 0. Escriba una función en int mcd(int m, int n) que implemente este algoritmo, y luego una versión en lenguaje ensamblador Y86. El siguiente código muestra un esqueleto que le puede servir como comienzo. Ejecute su código utilizando el simulador del procesador secuencial para calcular mcd(18, 14), mostrando la salida del simulador secuencial estándar en modo texto. # Ejecucion comienza en la direccion 0x00 .pos 0 init: irmovl Pila, %esp # define puntero a la pila irmovl Pila, %ebp # define puntero base jmp Main # ejecuta programa principal Main: irmovl $14, %eax pushl %eax irmovl $18, %eax pushl %eax # # # # %eax n es %eax m es 35 vale 14 14 vale 18 18 3 36 Capítulo 3: Procesador Y86 call mcd halt # llama a mcd(18, 14) # int mcd(int n, int m) # INSERTE SU CODIGO AQUI .pos 0x100 Pila: # pila del programa comienza aqui 3.2 Sea la función recursiva SumaRec(int *dato, int cuenta), que suma cuenta enteros a partir de la posición dada dato, cuyo código C se muestra a continuación. Escriba esta función en lenguaje ensamblador Y86, y luego utilice esta función para calcular la suma de un vector de 8 elementos. Ejecute su código utilizando el simulador del procesador secuencial. int SumaRec(int *dato, int cuenta) { if (cuenta == 1) { return *dato; } return *dato + SumaRec(dato++, cuenta--); } El siguiente código muestra un esqueleto que le puede servir como comienzo. # Ejecucion comienza en la direccion 0x00 .pos 0 init: irmovl Pila, %esp # define puntero a la pila irmovl Pila, %ebp # define puntero base jmp Main # ejecuta programa principal # vector de elementos .align 4 vector: .long 0x56ad .long 0x1786 .long 0x3018 .long 0x0F41 .long 0x000c .long 0x2918 .long 0x1004 .long 0x0F0F Main: irmovl $8, %eax # %eax vale 8 37 Capítulo 3: Procesador Y86 pushl %eax irmovl vector, %edx pushl %edx call SumaRec halt # # # # cuenta es 8 lee direccion de vector dato es dir. vector llama a SumaRec(vector, 8) # int SumaRec(int *dato, int cuenta) # INSERTE SU CODIGO AQUI .pos 0x150 Pila: # pila del programa comienza aqui 3.3 En los programas de ejemplo del procesador X86, se presenta la instrucción leave, y se indica que es equivalente a las instrucciones Y86 rrmovl %ebp, %esp y popl %ebp. Agregue esta instrucción al conjunto de instrucciones del procesador Y86, usando la siguiente codificación. 0 D 0 1 2 3 4 5 Use la tabla para popl como guía. Solución La tabla de acciones para esta instrucción se muestra a continuación. Etapa Acciones Fetch iCode : iFun ← M1 [P C] valP ← P C + 1 Decode valA ← R[ %ebp] valB ← R[ %ebp] Execute valE ← valB + 4 Memory valM ← M4 [valA] Write Back R[ %esp] ← valE R[ %ebp] ← valM PC Update P C ← valP 3.4 En los programas de ejemplo del procesador Y86, hay ocasiones en que es necesario sumar un valor constante a un registro. Esto requiere usar 38 Capítulo 3: Procesador Y86 una instrucción irmovl para almacenar la constante en un registro, y luego una instrucción addl para sumar su valor al registro destino. Agregue entonces una instrucción iaddl con el siguiente formato: 0 C 0 1 8 rB 2 3 4 5 V Esta instrucción suma la constante V al registro rB. Describa las computaciones necesarias para implementar la instrucción. Use la tabla para irmovl y OP como guía. Solución La tabla de acciones para esta instrucción se muestra a continuación. Etapa Acciones Fetch iCode : iFun ← M1 [P C] rA : rB ← M1 [P C + 1] valC ← M4 [P C + 2] valP ← P C + 6 Decode valB ← R[rB] Execute valE ← valB + valC Memory Write Back R[rB] ← valE PC Update P C ← valP 3.5 La función recursiva de Takeuchi fue inventada por Ikeo Takeuchi en 1978 para medir el desempeño de compiladores LISP, mientras trabajaba en los laboratorios de la Nippon Telephone and Telegraph Co. Esta función está definida como sigue: T (x, y, z) = if x ≤ y then y else T (T (x − 1, y, z), T (y − 1, z, x), T (z − 1, x, y)) Una posible implementación en lenguaje C es: Capítulo 3: Procesador Y86 39 int takeuchi(int x, int y, int z) { if (x <= y) return y; else return takeuchi(takeuchi(x - 1, y, z), takeuchi(y - 1, z, x), takeuchi(z - 1, x, y)); } La invocación de la función takeuchi() con argumentos (3, 2, 1) resulta en 9 invocaciones sucesivas a la función, con diferentes argumentos, y un resultado final de 3. Escriba una función en lenguaje ensamblador Y86 que implemente esta función. Recuerde que la convención de paso de parámetros de C dice que éstos se almacenan en la pila de derecha a izquierda. No olvide cumplir con las convenciones Caller-Save y Callee-Save. El siguiente esqueleto de código le puede servir de comienzo. # Ejecucion en la direccion 0x00 .pos 0 init: irmovl Pila, %esp # define puntero a la pila irmovl Pila, %ebp # define puntero base jmp Main # ejecuta programa principal Main: irmovl $1, %eax # %eax vale 1 pushl %eax # 3er argumento z es 1 irmovl $2, %eax # %eax vale 2 pushl %eax # 2do argumento y es 2 irmovl $2, %eax # %eax vale 3 pushl %eax # 1er argumento x es 3 call takeuchi # llama a takeuchi(3, 2, 1) halt # int takeuchi (int x, int y, int z) # INSERTE SU CODIGO AQUI .pos 0x200 Pila: # pila del programa comienza aqui Simule la ejecución de su función para los argumentos (3, 2, 1) usando el simulador secuencial ssim-std.exe. Calcule el número de ciclos que demora la ejecución y la cantidad de memoria utilizada, tanto por el programa como por la pila. 40 Capítulo 3: Procesador Y86 Simule ahora la ejecución de su función usando el simulador segmentado psim.exe. Indique el CPI obtenido, identificando las instrucciones que causan pérdida de ciclos en el pipeline. Solución El siguiente código muestra una posible implementación de la función de Takeuchi en lenguaje ensamblador Y86. # Ejecucion comienza en la direccion 0x00 .pos 0 init: irmovl Pila, %esp # define puntero a la pila irmovl Pila, %ebp # define puntero base jmp Main # ejecuta programa principal Main: irmovl $1, %eax # %eax vale 1 pushl %eax # 3er argumento z es 1 irmovl $2, %eax # % eax vale 2 pushl %eax # 2do argumento y es 2 irmovl $3, %eax # %eax vale 3 pushl %eax # 3er argumento z es 3 call takeuchi # llama a takeuchi con argumentos (3, 2, 1) halt # int takeuchi(int x, int y, int z) takeuchi: pushl %ebp # almacena puntero base rrmovl %esp, %ebp # crea marco de activación mrmovl 8(%ebp), %ecx # lee x mrmovl 12(%ebp), %eax # lee y subl %ecx, %eax # resta y - x jl Lazo mrmovl 12(%ebp), %eax # lee y rrmovl %ebp, %esp # recupera puntero a la pila popl %ebp # recupera puntero base ret Lazo: mrmovl 16(%ebp), %edx irmovl $-1, %eax addl %eax, %edx mrmovl 12(%ebp), %eax pushl %eax pushl %ecx pushl %edx call takeuchi irmovl $12, %edx addl %edx, %esp pushl %eax # # # # # # # lee z almacena -1 en %eax calcula z - 1 lee y 3er argumento es y 2do argumento es x 1er argumento es z - 1 # almacena 12 en %edx # ajusta el puntero a la pila en 12 # almacena resultado en la pila 41 Capítulo 3: Procesador Y86 mrmovl 8(%ebp), %ecx mrmovl 12(%ebp), %eax irmovl $-1, %edx addl %edx, %eax mrmovl 16(%ebp), %edx pushl %ecx pushl %edx pushl %eax call takeuchi irmovl $12, %edx addl %edx, %esp pushl %eax # # # # # # # # lee x lee y almacena - 1 en %edx calcula y - 1 lee z 3er argumento es x 2do argumento es z 1er argumento es y - 1 mrmovl 8(%ebp), %ecx irmovl $-1, %edx addl %edx, %ecx mrmovl 16(%ebp), %edx mrmovl 12(%ebp), %eax pushl %edx pushl %eax pushl %ecx call takeuchi irmovl $12, %edx addl %edx, %esp pushl %eax # # # # # # # # call takeuchi irmovl $12, %edx addl %edx, %esp pushl %eax # # # # rrmovl %ebp, %esp popl %ebp ret # recupera puntero a la pila # recupera puntero base # almacena 12 en %edx # ajusta el puntero a la pila # almacena resultado en la pila lee x almacena -1 en %edx calcula x - 1 lee z lee y 3er argumento es z 2do argumento es y 1er argumento es x - 1 # almacena 12 en %edx # ajusta el puntero a la pila # almacena resultado en la pila llama a takeuchi de nuevo almacena 12 en %edx ajusta el puntero a la pila almacena resultado en la pila .pos 0x0200 Pila: # Pila del programa comienza aqui Algunos comentarios sobre esta solución: El programa anterior cumple con las convenciones Caller-Save y Callee-Save, y con la convención de paso de parámetros de C a través de la pila, de derecha a izquierda. Se escribió el programa intentando minimizar el uso de memoria en la pila, para así facilitar la ejecución recursiva de la función. Capítulo 3: Procesador Y86 42 El programa ocupa 236 bytes de memoria, y su ejecución ocupa 68 bytes en la pila. La función takeuchi() misma ocupa 189 bytes de memoria. La simulación del programa anterior usando el simulador secuencial ssim-std.exe calcula la función takeuchi(3, 2, 1) en 177 ciclos de reloj. La simulación del programa anterior usando el simulador segmentado psim.exe calcula la función takeuchi(3, 2, 1) en 229 ciclos de reloj, dando un CPI de 1.29 La función takeuchi() es invocada 9 veces. Cada invocación involucra una instrucción de retorno ret. Las penalidad asociada a dichas instrucciones es de 3 ciclos. Entonces, en total estas instrucciones generan 27 ciclos de penalidad. Asimismo, hay 9 instrucciones de salto condicional jl. La penalidad en caso de predicción errónea de salto es de 2 ciclos. Estudiando el comportamiento del algoritmo, puede verse que este salto es predicho erróneamente 7 veces, por lo que hay 14 ciclos de penalidad asociados a ese salto. Los ciclos de penalidad restantes se deben a peligros de lectura y uso. Por ejemplo, es necesario leer el argumento y de la función desde memoria usando mrmovl 12(%ebp), %eax, y escribirlo nuevamente a la pila usando pushl %eax antes de invocar por primera vez a la función takeuchi() después del rótulo Lazo. 3.6 El procesador Intel 80386 posee una instrucción bsr S, D conocida como Bit Scan Reverse, que determina el índice del primer bit en 1 del registro S, revisando dicho registro de izquierda a derecha, y retorna dicho índice en el registro D. Esta instrucción no existe en el procesador Y86, pero puede ser implementada como una función en lenguaje ensamblador. a) Escriba, una función bsr en lenguaje ensamblador Y86 que opere sobre el registro %eax y retorne en el registro %edx el índice del primer bit en 1 del registro %eax. Es decir, si %eax = 0x432FA708, al terminar su función el registro %edx debe contener el número 30, ya que ése es el mayor bit de %eax que tiene valor 1. b) Cuál es el tamaño de su función en bytes? c) Suponga que su función se ejecuta en el procesador secuencial estudiado en clases. Cuántos ciclos de reloj demorará su ejecución para %eax = 0x12345678? 43 Capítulo 3: Procesador Y86 d) Suponga ahora que su función se ejecuta en el procesador con pipelining y forwarding visto en clases. Recuerde que las penalidades asociadas a este procesador son: Peligro Penalidad Lectura y uso Predicción errada Retorno de función 1 2 3 Cuántos ciclos de reloj demorará ahora la ejecución de su función en este procesador segmentado, para %eax = 0x12345678? Solución a) Una posible implementación de la operación bsr S, D se muestra a continuación. bsr: Lazo: Fin: pushl %ebp rrmovl %esp, %ebp pushl %esi irmovl $1, %esi irmovl $31, %edx xorl %ecx, %ecx subl %ecx, %eax jl Fin addl %eax, %eax subl %esi, %edx je Fin jmp Lazo popl %esi rrmovl %ebp, %esp popl %ebp ret # crea marco de activacion # # # # # # # # # almacena copia de registro %esi inicializa %esi a 1 inicializa %edx a 31 inicializa %ecx a 0 resta %eax - 0 si resultado es negativo, ir a Fin si resultado es positivo, duplicar %eax decrementar %edx en 1 si resultado es 0, ir a Fin # recupera valor de registro %esi # recupera marco de activacion # retorno de funcion b) Es sabido que las instrucciones irmovl ocupan 6 bytes de memoria, las instrucciones ret ocupan 1 byte, que las instrucciones de salto ocupan 5 bytes y que el resto de las instrucciones ocupa 2 bytes de memoria. Puede calcularse, entonces, que el código mostrado tiene un tamaño de 48 bytes. c) La ejecución de la función mostrada en el procesador secuencial demorará tantos ciclos como instrucciones se ejecuten. En el caso que el registro %eax toma el valor 0x12345678, se ejecutan 30 instrucciones y por lo tanto demora 30 ciclos. 44 Capítulo 3: Procesador Y86 d) La ejecución de la función mostrada en el procesador segmentado con pipelining y forwarding para el caso el caso en que el registro %eax toma el valor 0x12345678 demora 45 ciclos: 30 ciclos correspondientes a la ejecución del código sin penalidad, mas 12 ciclos de penalidad causados por los 6 saltos condicionales predichos erróneamente, mas 3 ciclos de penalidad del retorno de función. 3.7 El procesador Intel Y86 no posee una instrucción de rotación de registros, pero ésta puede ser implementada como una función en lenguaje ensamblador. Escriba una función rotate que reciba como argumento un entero p entre 0 y 31 y que retorne el resultado de rotar el registro %eax p bits a la izquierda. si inicialmente se tiene que %eax = 0x12345678, su valor después de rotarlo 7 bits a la izquierda es %eax = 0x1A2B3C09. Cuál es el tamaño de su función en bytes? Solución Se muestra a continuación una implementación en 53 bytes de la función solicitada. rotate: pushl %ebp rrmovl %esp, %ebp mrmovl 8(%ebp), %edx irmovl $1, %ecx lazo: andl %edx, %edx je fin subl %ecx, %edx andl %eax, %eax jl neg addl %eax, %eax jmp lazo neg: addl %eax, %eax addl %ecx, %eax jmp lazo fin: rrmovl %ebp, %esp popl %ebp ret # crea marco de activacion # lee p # inicializa %ecx a 1 # si p es 0, terminar # # # # # # # decrementa p hace esto para pasar por la ALU salta si %eax es negativo desplaza %eax a la izquierda 1 bit vuelve a lazo desplaza %eax a la izquierda 1 bit Agrega bit 1 a la derecha de %eax # recupera marco de activacion .pos 0x100 Pila: # Define la pila del proceso Capítulo Pipelining 4.1 Se desea ejecutar un cierto código usando el procesador con pipelining visto en clases. Se ha medido la siguiente tabla de frecuencias para el código a ejecutar: El 20 % de las instrucciones corresponden a instrucciones mrmovl y popl. De éstas, el 30 % genera peligros de lectura y uso. El 15 % de las instrucciones son instrucciones de salto condicional, en las cuales el 50 % de las veces se ejecuta el salto y en el 50 % restante no se ejecuta el salto. El 5 % de las instrucciones corresponden a instrucciones de retorno ret. a) Calcule el CPI asociado a la ejecución de este código en el procesador. b) Se ha decidido modificar el circuito de predicción de saltos de manera que ahora el porcentaje de saltos predichos correctamente es 80 %. Pero, a un costo de alargar el ciclo de reloj del procesador en 10 %. Vale la pena realizar esta modificación? Solución a) En el primer caso, es necesario calcular la penalidad asociada a cada tipo de peligro. Tabla 4.1 muestra este cálculo. El CPI es entonces CP I = 1 + 0.06 + 0.15 + 0.15 = 1.36. 45 4 46 Capítulo 4: Pipelining Tabla 4.1: Penalidad por tipo de instrucción Penalidad Frec. Instr. Frec. Condición Burbujas Producto Lectura/uso Predicción errada Retorno 0.2 0.15 0.05 0.3 0.5 1.0 1 2 3 0.06 0.15 0.15 b) En el segundo caso, el costo de las predicciones erradas es ahora 0.15 × 0.2 × 2 = 0.06, por lo que el CPI es ahora 1.27. Pero, el procesador usa un reloj 10 % mas lento. Entonces, el CPI efectivo comparado con el procesador anterior es 1.27 ∗ 1.1 = 1.397. Por lo tanto, no vale la pena la modificación. 4.2 La figura 4.1 muestra las etapas de la lógica combinacional de un procesador secuencial. Se ha determinado que ésta puede ser dividida en 6 etapas llamadas A a F, donde cada etapa tiene las latencias indicadas. Se desea crear nuevas versiones de este diseño insertando registros de pipeline entre bloques. Suponga que un registro de pipeline tiene un retardo de 20 ps. Figura 4.1: Procesador segmentado de 6 etapas a) Cuál es la latencia y el throughput del pipeline actual? b) Se desea convertir el sistema anterior a un pipeline de 2 etapas. Dónde insertaría Ud. el registro de pipeline? Cuál será la latencia y y el throughput del nuevo pipeline? Capítulo 4: Pipelining 47 c) Se desea ahora convertir el sistema anterior a un pipeline de 3 etapas. Dónde insertaría Ud. los registros de pipeline? Cuál será la latencia y y el throughput de este nuevo pipeline? d) Se desea ahora convertir el sistema anterior a un pipeline de 4 etapas. Dónde insertaría Ud. los registros de pipeline? Cuál será la latencia y y el throughput de este nuevo pipeline? e) Se desea ahora convertir el sistema anterior a un pipeline de desempeño máximo. Cuántos registros de pipeline insertaría Ud. y adónde? Cuál será la latencia y y el throughput de este nuevo pipeline? Solución a) La latencia actual del pipeline es de 320 ps, lo que da un throughput de 3201 ps = 3.125 ∗ 109 instrucciones/s. b) Se debe insertar un registro de pipeline entre las etapas C y D, dividiendo así el pipeline en la etapa A-B-C-registro, de duración 190 ps, y la etapa D-E-F-registro de duración 150 ps. La nueva latencia de este pipeline es 380 ps, y el throughput es 1901 ps = 5.263 ∗ 109 instrucciones por segundo. c) Se debe insertar un registro de pipeline entre las etapas B y C, y otro registro de pipeline entre las etapas D y E, dividiendo así el pipeline en la etapa A-B-registro, de duración 130 ps, la etapa CD-registro, de duración 130 ps y la etapa E-F-registro de duración 100 ps. La nueva latencia de este pipeline es 390 ps, y el throughput es 1301 ps = 7.693 ∗ 109 instrucciones por segundo. d) Se debe insertar un registro de pipeline entre las etapas A y B, otro registro de pipeline entre las etapas C y D y otro registro de pipeline entre las etapas D y E. Esto divide el pipeline en la etapa A-registro, de duración 100 ps, la etapa B-C-registro, de duración 110 ps, la etapa D-registro de duración 70 ps y la etapa E-F-registro de duración 100 ps. La nueva latencia de este pipeline es 440 ps, y el throughput es 1101 ps = 9.09 ∗ 109 instrucciones por segundo. e) Finalmente, se insertan registros de pipeline entre las etapas A y B, B y C, C y D y D y E. Esto divide el pipeline en 5 etapas, la más lenta de las cuales demora 100 ps, por lo que la nueva latencia de este pipeline es 500 ps, y el throughput es 1001 ps = 1010 instrucciones por segundo. Capítulo 4: Pipelining 48 4.3 Un procesador tiene un pipeline de 5 etapas, de largo 20, 40, 30, 10, y 20 ps. Se necesita correr un código que ejecuta 24 veces un lazo de 135 instrucciones en este procesador. Cuánto se demorará este código? Solución Supondremos que el retardo de los registros está incluido en el tiempo de las etapas. Entonces, el procesador iniciará la ejecución de una instrucción cada 40 ps. El código requiere la ejecución de 24 ∗ 135 instrucciones, lo que requerirá 3240 ciclos, demorando entonces 3240 ∗ 40 = 129.6 ns. 4.4 Se desea ejecutar un cierto código usando el procesador con pipelining visto en clases. Al ejecutar un conjunto dado de aplicaciones, se ha medido la siguiente tabla de frecuencias para el código a ejecutar: El 25 % de las instrucciones corresponden a instrucciones de lectura de datos desde memoria. De éstas, el 25 % genera peligros de lectura y uso. El 12 % de las intrucciones son instrucciones de salto condicional, en las cuales el 70 % de las veces se ejecuta el salto y en el 30 % restante no se ejecuta el salto. El 2.5 % de las instrucciones corresponden a instrucciones de retorno ret. a) Calcule el CPI asociado a la ejecución de este código en el procesador. b) Se ha decidido modificar el circuito de predicción de saltos de manera que ahora el procesador supone que los saltos condicionales hacia atrás siempre se toman, y los saltos condicionales hacia adelante nunca se toman. El examen de las aplicaciones ejecutadas muestra que el 70 % de los saltos condicionales son hacia atrás, y de éstos el 80 % se toman. Esta modificación alarga el ciclo de reloj del procesador en un 8 %. Vale la pena realizar esta modificación? Solución a) Para calcular el CPI, es necesario calcular las penalidades incurridas por tres tipos de instrucciones. En el caso de instrucciones de lectura y uso, se sabe que sólo en un cuarto de las instrucciones de lectura y uso se incurre en una penalidad de 1 ci- Capítulo 4: Pipelining 49 clo. Entonces, la penalidad total asociada a estas instrucciones es 0.25 × 0.25 × 1 = 0.0625 ciclos. Luego, es necesario calcular el costo de una predicción de salto errada. El procesador siempre predice que el salto se toma. Si éste efectivamente es tomado, la penalidad es 0. En cambio, si el salto no se toma, la penalidad es 2 ciclos. Entonces, como la predicción de salto falla en 3 de cada 10 saltos, la penalidad total asociada a estas instrucciones es 0.12 × 0.3 × 2 = 0.072 ciclos. Finalmente, las instrucciones de retorno siempre sufren una penalidad de 3 ciclos. La penalidad total asociada a estas instrucciones es 0.025 × 3 = 0.075 ciclos. Por lo tanto, el CPI total es CP I = 1 + 0.0625 + 0.072 + 0.075 = 1.2095 b) Para analizar esta alternativa, es necesario modificar el cálculo del costo de una predicción de salto errada. El procesador predice que los saltos condicionales hacia atrás siempre se toman, y que los saltos condicionales hacia adelante nunca se toman. De la respuesta anterior, sabemos que los saltos no son tomados en un 30 % de los casos. También sabemos que el 70 % de los saltos condicionales son hacia atrás, y de éstos el 80 % se toman. Esto significa que el 56 % de los saltos son hacia atrás y se toman, por lo que la predicción está correcta y la penalidad asociada es 0, y que el 14 % de los saltos son hacia atrás y no se toman, con penalidad 2 ciclos. Además, esto nos dice que el 16 % de los saltos son hacia adelante y no se toman, y que el 14 % de los saltos son hacia adelante y se toman. Los primeros serán predichos correctamente, por lo que tienen una penalidad de 0 ciclos y los segundos tendrán una penalidad de 2 ciclos. Por lo tanto, el costo de la predicción de saltos es 0.12 × (0.56 × 0 + 0.14 × 2 + 0.14 × 2 + 0.16 × 0) = 0.0672. Entonces, el nuevo CPI será CP I = 1 + 0.0625 + 0.0672 + 0.075 = 1.2047. Ahora, esta modificación alarga el ciclo de reloj del procesador en un 8 %, por lo que el nuevo CPI será realmente 1.08 ∗ 1.2047 = 1.301, el cual, al ser mayor que la alternativa a), no hace rentable realizar este cambio. 4.5 Sea la siguiente función en lenguaje ensamblador X86, que determina si una ristra es palíndromo, es decir, si se lee de la misma manera de derecha a izquierda que de izquierda a derecha. Esta función recibe como argumentos la dirección en memoria de la ristra a examinar y su longi- 50 Capítulo 4: Pipelining tud en caracteres. La primera columna del código indica el número de la instrucción. .type palindromo, @function palindromo: 1. pushl %ebp # 2. movl %esp, %ebp # 3. pushl %ebx # 4. movl 8(%ebp), %eax # 5. movl 12(%ebp), %edx # 6. subl $1, %edx # 7. movl $0, %ebx # 8. lazo: movb (%eax, %ebx), %ch 9. movb (%eax, %edx), %cl 10. cmpb %cl, %ch # 11. jne finNo # 12. incl %ebx # 13. decl %edx # 14. cmpl %ebx, %edx # 15. js finSi # 16. jmp lazo 17. finSi: movl $1, %eax 18. jmp fin 19. finNo: movl $0, %eax 20. fin: popl %ebx # 21. leave 22. ret Almacena puntero base en la pila Redefine el puntero base Salva en la pila el registro %ebx Lee direccion de la ristra en %eax Lee el largo de la ristra en %edx Calcula offset del ultimo caracter Calcula offser del primer caracter Compara los caracteres Si son distintos, no es palindromo Incrementa indice in %ebx Decrementa indice en %edx Compara los indices Si es negativo, es palindromo Recupera el registro %ebx de la pila Suponga que este código se ejecuta en un procesador segmentado de 5 etapas similar al visto en clases. a) Identifique en el código los posibles peligros de lectura y uso, indicando los números de las instrucciones donde se producen. b) Identifique en el código las posibles instancias de stalling. c) Identifique en el código las posibles instancias de forwarding. Solución a) Los peligros de lectura y uso se producen al leer un dato desde memoria en una instrucción, y utilizar este mismo dato en la instrucción siguiente. Esto ocurre: 1) entre las instrucciones 5 y 6, 2) entre las instrucciones 8 y 9, y Capítulo 4: Pipelining 51 3) entre las instrucciones 9 y 10. b) En nuestro procesador simple, stalling ocurre en caso de lectura y uso, en el caso de saltos condicionales, y en el retorno de una función. Esto ocurre: 1) entre las instrucciones 5 y 6 (lectura y uso), donde se introduce una burbuja, 2) entre las instrucciones 8 y 9 (lectura y uso), donde se introduce una burbuja, 3) entre las instrucciones 9 y 10 (lectura y uso), donde se introduce una burbuja, 4) entre las instrucciones 10 y 11 (salto condicional), donde se introducen dos burbujas, 5) entre las instrucciones 14 y 15 (salto condicional), donde se introducen dos burbujas, y 6) después de la instrucción 21 (retorno de función), donde se introducen tres burbujas. c) El forwarding se produce cuando existe dependencia de datos entre instrucciones, y es necesario redirigir un registro entre etapas de una instrucción en ejecución. Para este análisis, supondremos que los registros normalmente son leídos al comenzar el ciclo ID, y que normalmente son escritos al terminar la etapa WB. Entonces, cualquier transferencia de datos entre registros en otras etapas requiere forwarding. En este caso, se tienen las siguientes instancias: 1) Forwarding del registro %esp desde la etapa EXE de la instrucción 1 a la etapa ID de la instrucción 2. 2) Forwarding del registro %esp desde la etapa MEM de la instrucción 1 a la etapa ID de la instrucción 3. 3) Forwarding del registro %ebp desde la etapa MEM de la instrucción 2 a la etapa ID de la instrucción 4. 4) Forwarding del registro %ebp desde la etapa WB de la instrucción 2 a la etapa ID de la instrucción 5. 5) Forwarding del registro %edx desde la etapa MEM de la instrucción 5 a la etapa ID de la instrucción 6. Esta última etapa se repite 1 vez debido a que existe un peligro de lectura y uso. 6) Forwarding del registro %ebx desde la etapa EXE de la instrucción 7 a la etapa ID de la instrucción 8. Capítulo 4: Pipelining 52 7) Forwarding del registro %ch desde la etapa MEM de la instrucción 8 a la etapa ID de la instrucción 9. Esta última etapa se repite 1 vez debido a que existe un peligro de lectura y uso. 8) Forwarding de los códigos de condición desde la etapa MEM de la instrucción 9 a la etapa EXE de la instrucción 10. Además, en esta instrucción existe un peligro de lectura y uso. 9) Forwarding del registro %ebx desde la etapa MEM de la instrucción 11 a la etapa ID de la instrucción 13. 10) Forwarding del registro %edx desde la etapa EXE de la instrucción 12 a la etapa ID de la instrucción 13. 11) Forwarding de los códigos de condición desde la etapa MEM de la instrucción 13 a la etapa EXE de la instrucción 14. 4.6 Suponga ahora que este procesador tiene un CPI ideal de 1 instrucción por ciclo, sin considerar penalidades. Determine cuántos ciclos de reloj demorará la ejecución completa de esta función si sus argumentos son la ristra radar y el largo 5, considerando ahora las penalidades. En este caso, suponga que no hay predicción de saltos. Solución Ejecutar el código mostrado con los argumentos radar y 5 requiere la ejecución de 35 instrucciones. Éstas incluyen 7 casos de lectura y uso (que introducen 7 burbujas), 6 instrucciones de salto condicional (que introducen 12 burbujas) y una instrucción ret, que introduce 3 burbujas, para un total de 22 burbujas. Por ende, la ejecución de este código requiere 57 ciclos de reloj. A su vez, esto corresponde a un CPI de 57 35 = 1.628. 4.7 Suponga ahora que el procesador en cuestión se modifica de manera de incorporar predicción de salto. Los saltos condicionales hacia adelante se predicen como no tomados, y los saltos condicionales hacia atrás se predicen como tomados. No hay penalidad asociada al caso de una predicción acertada. Además, se sabe que el costo de esta modificación al procesador es el aumentar un 2 % su período máximo de reloj. Determine ahora el número de ciclos de reloj que demorará la ejecución completa de la función en este nuevo procesador, y compare la ejecución de este código con el caso anterior. Capítulo 4: Pipelining 53 Solución Ejecutar el código mostrado con los argumentos radar y 5 requiere la ejecución de 6 instrucciones de salto condicional, todas ellas hacia adelante. De éstas, 5 no se toman y sólo 1 de ellas se toma. Entonces, al usar el esquema de predicción descrito, hay 5 predicciones acertadas y 1 predicción errada. Esto reduce la penalidad asociada de 12 ciclos a sólo 2 ciclos. Por ello, este código se ejecuta en sólo 47 ciclos, para un CPI de 47 35 = 1.343. Como el ciclo de reloj es 2 % más lento, la ejecución del código demorará 47 × 1.02 = 47.94 ciclos del reloj original. Por lo tanto, esta solución es conveniente porque lleva a una ejecución más rápida de la función dada. Capítulo Optimización de código 5.1 Sea la siguiente función en C: void funcion(int *xp, int *yp) { *xp = *xp + *yp; *yp = *xp - *yp; *xp = *xp - *yp; } a) Identifique qué hace esta función b) Identifique qué problemas puede tener esta función si ambos argumentos apuntan a la misma posición de memoria. Solución a) La función intercambia los datos apuntados por los argumentos. b) Si ambos punteros apuntan a la misma posición de memoria, entonces esta posición toma siempre el valor 0. 54 5 Capítulo Sistemas de memoria 6.1 Suponga una memoria principal de 8192 bytes y una memoria cache de 64 bytes, organizada como 16 líneas de 4 bytes cada una, dividida en dos conjuntos. El contenido de la cache es como se muestra en la siguiente tabla, donde la primea columna es el número de línea, V es el bit de validez, y B0 a B3 son los bytes 0 a 3, respectivamente. 0 1 2 3 4 5 6 7 Tag 09 45 EB 06 C7 71 91 46 V 1 1 0 0 1 1 1 0 B0 86 60 2F 3D 06 0B A0 B1 Cache asociativa de 2 conjuntos B1 B2 B3 Tag V B0 30 3F 10 00 0 99 4F E0 23 38 1 00 81 FD 09 0B 0 8F 94 9B F7 32 1 12 78 07 C5 05 1 40 DE 18 4B 6E 0 B0 B7 26 2D F0 0 0C 0A 32 0F DE 1 12 B1 04 BC e2 08 67 39 71 C0 B2 03 0B 05 7B C2 D3 40 88 B3 48 37 BD AD 3B F7 10 37 a) Indique en un diagrama los bits de la dirección física correspondientes a 1) el tag 2) el índice 3) el número del byte en la línea 55 6 Capítulo 6: Sistemas de memoria 56 b) El procesador genera la dirección 0x0E34. Indique la línea de la cache accesada y el byte accedido. Indique además si el acceso genera un fallo en cache. 6.2 Ud. ha comprado un nuevo computador, y estudios de desempeño en éste le indican que: 95 % de los accesos a memoria son atendidos por la memoria cache. Cada bloque de memoria cache es de 2 palabras, y el bloque completo es transferido desde memoria RAM a la memoria cache en caso de una falta de cache. El procesador genera, en promedio, 109 direcciones de memoria por segundo. 25 % de estas direcciones son escrituras a memoria. Suponga que el sistema de memoria soporta 109 accesos por segundo, pero el bus de interconexión sólo soporta un acceso a la vez (no puede haber 2 lecturas o escrituras a memoria en forma simultánea). Suponga que, en promedio, el 30 % de los bloques en la cache han sido modificados. Calcule el tiempo de acceso promedio a memoria y el porcentaje de utilización del bus de interconexión para los siguientes casos: a) La memoria cache es Write-Through b) La memoria cache es Write-Back 6.3 Suponga que la ejecución de un programa en un computador con un sistema de memoria ideal muestra un CPI de 1.5. El sistema de memoria real del computador tiene una latencia de 40 ciclos, después de los cuales la memoria RAM es capaz de entregar 4 bytes en cada ciclo de reloj. Suponga además que hay 32 bytes en una línea de memoria cache, que el 20 % de la instrucciones son instrucciones de transferencias de datos, y que el 50 % de estas instrucciones son escrituras a bloques de memoria modificados. Se sabe que este computador puede utilizarse con Una memoria cache unificada de 16 KiB con asociatividad directa, que utiliza Write-Back y tiene una tasa de fallas de 2.9 %. Capítulo 6: Sistemas de memoria 57 Una memoria cache unificada de 16 KiB con asociatividad de dos vías, que utiliza Write-Back y tiene una tasa de fallas de 2.2 %. Una memoria cache unificada de 32 KiB con asociatividad directa, que utiliza Write-Back y tiene una tasa de fallas de 2.9 %. Calcule para todos los casos el CPI efectivo del sistema. 6.4 Suponga que tiene el siguiente código C que se ejecutará en un sistema con una memoria cache directamente asociativa de 1024 bytes de tamaño, con 16 bytes por línea. struct posicion { int x; int y; }; struct posicion malla[16][16]; int xTotal = 0; yTotal = 0; int i, j; for (i = 0; i < 16; i++) { for (j = 0; j < 16; j++) { xTotal += malla[i][j].x; } } for (i = 0; i < 16; i++) { for (j = 0; j < 16; j++) { yTotal += malla[i][j].y; } } Suponga además que: Un entero int ocupa 4 bytes. La cache está inicialmente vacía. La matriz malla se almacena en memoria a partir de la posición 0x00. Las variables i, j, xTotal y yTotal se almacenan en los registros del procesador. Entonces, a) Calcule la tasa de fallos de la memoria cache b) Repita lo anterior para el siguiente lazo Capítulo 6: Sistemas de memoria 58 for (i = 0; i < 16; i++) { for (j = 0; j < 16; j++) { xTotal += malla[i][j].x; yTotal += malla[i][j].y; } } c) Repita el punto anterior, pero ahora suponiendo una memoria cache del doble del tamaño d) Repita ahora los dos puntos anteriores para el siguiente lazo for (i = 0; i < 16; i++) { for (j = 0; j < 16; j++) { xTotal += malla[j][i].x; yTotal += malla[j][i].y; } } Solución La matriz malla contiene 256 elementos de tipo posicion, cada uno de ellos de 2 enteros, ocupando 8 bytes. Entonces, la matriz ocupa 2048 bytes, así que no cabe completamente en la memoria cache. Además, una línea de cache contiene 16 bytes o 2 elementos de la matriz malla. Por ello, no hay aciertos en cache por reutilización de datos y sólo puede haber aciertos en accesos a datos que hayan sido precargados en la cache. a) El primer ciclo anidado accede primero a todos los miembros x y luego a todos los miembros y de las estructuras posición de la matriz malla. El primer acceso a un miembro x transfiere 2 estructuras posicion a la memoria cache, pero sólo la escritura al miembro x del segundo elemento transferido serán aciertos en la cache. Esta situación se repite para los accesos a los miembros y de la matriz. Así, la tasa de fallos es 50 %. b) En este caso, se accede a los elementos de la matriz por filas, y a ambos miembros de una estructura posición en forma secuencial. El acceso al primer miembro x será un fallo en la cache, pero los tres accesos siguientes serán aciertos en la cache, así que la tasa de fallos será 25 %. c) La tasa de fallo no se ve afectada si el tamaño de la memoria se duplica. Capítulo 6: Sistemas de memoria 59 d) En este caso, se accede a los elementos de la matriz por columnas, por lo que el acceso al miembro x será un fallo de la cache, pero el acceso al miembro y del mismo elemento será un acierto. Por ello, la tasa de fallos será de 50 %. Si la memoria cache se duplica, entonces la tasa de fallos disminuye a 25 %. 6.5 Considere el siguiente código C, que calcula la traspuesta de una matriz typedef int matriz[2][2]; void traspuesta(matriz destino, matriz fuente) { int i, j; for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { destino[j][i] = fuente[i][j]; } } } Suponga que este código se ejecuta en una máquina de 32 bits con las siguientes características: La matriz destino comienza en la dirección de memoria 0 y la matriz fuente comienza en la dirección de memoria 0x0010. El sistema posee una cache de datos L1 de 16 bytes de tamaño, asociatividad directa, una política de escrituras Write-Through y líneas de 8 bytes. Esta cache está inicialmente vacía. Los únicos accesos a memoria son las lecturas y escrituras a las matrices destino y fuente. Entonces: a) Para cada elemento de las matrices destino y fuente, indique si su acceso fue una falla o un acierto en cache. b) Calcule la tasa de aciertos de la matriz. c) Calcule el número de bytes transferidos entre RAM y cache. 6.6 Ud. está escribiendo un nuevo juego 3D para celulares que le darán fama y fortuna, y hoy debe escribir una función que limpie la pantalla Capítulo 6: Sistemas de memoria 60 antes de dibujar la siguiente imagen. La pantalla tiene 480 líneas de 640 pixeles cada una. El procesador que Ud. está utilizando tiene una cache directa de 64 KiB con líneas de 4 bytes. Las estructuras de datos básicas que Ud. está usando son: struct pixel { char r; char g; char b; char a; }; struct pixel buffer[480][640]; int i, j; char *cptr; int *iptr; Suponga que sizeof(char) = 1, sizeof(int) = 4, que buffer está almacenado a partir de la dirección de memoria 0x0000, que la cache está inicialmente vacía y que las variables i, j, cptr y iptr están almacenadas en los registros del procesador, de manera que los únicos accesos a memoria son a direcciones dentro de buffer. a) Ud. primero escribe el siguiente código para limpiar la pantalla. Calcule la tasa de aciertos de la cache. for (j = 0; j < 640; j++) { for (i = 0; i < 480; i++) { buffer[i][j].r = 0; buffer[i][j].g = 0; buffer[i][j].b = 0; buffer[i][j].a = 0; } } b) Luego, Ud. escribe el siguiente código para limpiar la pantalla. Calcule la tasa de aciertos de la cache. char *cptr; cptr = (char *) buffer; for (; cptr < ((char *) buffer + 640*480*4; cptr++) { *cptr = 0; } Capítulo 6: Sistemas de memoria 61 c) Finalmente, Ud. escribe el siguiente código para limpiar la pantalla. Calcule la tasa de aciertos de la cache. int *iptr; iptr = (int *) buffer; for (; iptr < buffer + 640*480; iptr++) { *iptr = 0; } d) Cuál código usará Ud.? Solución En este problema, el tamaño de la pantalla determina la cantidad de memoria necesaria para desplegar una imagen como 640 ∗ 480 ∗ 4 = 1228800 bytes, donde cada pixel se almacena en 4 bytes. Para limpiar la pantalla, cada pixel será accedido sólo una vez, así que los únicos aciertos posibles son en accesos a datos precargados en una línea de la cache, de tamaño 4 bytes. a) En este caso, acceder al primer elemento r de la estructura de datos pixel es un fallo en la cache y provoca la transferencia de la estructura completa desde memoria a la cache. Luego, los accesos a los otros tres elementos de la estructura son aciertos en la cache. Este patrón se repite para cada pixel accedido, así que la tasa de aciertos de la cache es 75 %. b) En este caso, se accede secuencialmente a cada elemento de la memoria por byte. Nuevamente, no hay reutilización de datos excepto en la línea de cache. En este caso, el acceso al primer byte de la línea es un fallo en la cache, pero los accesos a los tres bytes siguientes son aciertos en la cache. Por ello, la tasa de aciertos es de 75 %. c) En este caso, se accede secuencialmente a cada elemento de la memoria en unidades de 4 bytes, que es el tamaño de la línea de cache. No hay reutilización de líneas de cache, así que todos los accesos a memoria son fallos en la cache, por lo que la tasa de aciertos de la cache es de 0 %. d) La principal componente del tiempo de ejecución de estos códigos será el tiempo para transferir los datos entre memoria principal y memoria cache. Aun cuando las dos primeras alternativas tienen 62 Capítulo 6: Sistemas de memoria tasas de acierto mayores, los tres códigos involucran igual cantidad y frecuencia de transferencias entre memoria principal y memoria cache. La última opción ejecuta menos instrucciones en un lazo más compacto. Si suponemos que el procesador puede escribir 4 bytes a memoria cache en un mismo ciclo de bus, la última alternativa bien puede ser la más rápida. 6.7 La siguiente tabla muestra los parámetros de diferentes caches para un procesador de 32 bits. En dicha tabla, C es el número de bytes de datos de la cache, B es el tamaño del bloque de cache en bytes, y E es el número de bloques por conjunto. Calcule S: el numero de vías de la cache, t: el numero de bits en el tag, i: el número de bits usados como índice, b: el número de bits usados como desplazamiento en el bloque, y T : el número total de bits en la cache, incluyendo datos, tag, bit de validez y de modificación. C B E 1024 4 4 1024 4 256 2048 8 1 2048 8 128 4096 32 1 8192 32 4 S t i b T Solución La tabla se llena usando las fórmulas S = t = 32 − b − i, y T = 8C + CB (t + 2). C B E 1024 4 4 1024 4 256 2048 8 1 2048 8 128 4096 32 1 8192 32 4 S 64 1 256 2 128 64 t 24 30 21 28 20 21 i 6 0 8 1 7 6 b 2 2 3 3 5 5 C BE , b = log2 (B), i = log2 (S), T 8192 + 6656 = 14848 8192 + 8192 = 16384 16384 + 5888 = 22272 16384 + 7680 = 24064 32768 + 2816 = 35584 65536 + 5888 = 71424 Capítulo 6: Sistemas de memoria 63 6.8 Se le ha pedido escribir el código para dirimir la elección de alcalde en la comuna de San Pablo de la Paz, donde hay 7 candidatos a la alcaldía. Ud. desarrolla entonces un programa en C y busca optimizar su desempeño. El software que Ud. desarrolle se ejecutará en un procesador de 32 bits que posee una memoria cache directa de 1024 bytes, con bloques de 64 bytes. Suponga además que inicialmente esta cache esta vacía. En su código, Ud. utiliza la siguiente estructura de datos struct Voto { int candidatos[7]; int valido; }; struct Voto matrizVotos[16][16]; // Estas variables se almacenan en registros de la CPU register int i, j, k; Entonces, Ud. escribe el siguiente código C para inicializar las estructuras de datos a utilizar. for (i = 0; i < 16; ++i) { for (j = 0; j < 16; ++j) { matrizVotos[i][j].valido = 0; } } for (i = 0; i < 16; ++i) { for (j = 0; j < 16; ++j) { for (k = 0; k < 7; ++k) { matrizVotos[i][j].candidatos[k] = 0; } } } Su colega escribe el siguiente código C para realizar la misma función. for (i = 0; i < 16; i++) { for (j = 0; j < 16; j++) { for (k = 0; k < 7; k++) { matrizVotos[i][j].candidatos[k] = 0; } matrizVotos[i][j].valido = 0; } } Capítulo 6: Sistemas de memoria 64 Su jefe dice que dará un bono de producción al programador cuyo código tenga el menor número de faltas de cache. Suponiendo que el área de datos de su programa comienza en la posición de memoria 0x4000, que un dato int ocupa 4 bytes, y que las variables i, j y k están en registros de la CPU, quién de Uds. dos recibe el bono? Justifique su respuesta calculando la tasa de aciertos en cada caso. Solución Dado que el bloque es de 64 bytes y cada estructura Voto ocupa 32 bytes, entonces un bloque de cache almacena 2 estructuras Voto. En ese caso, acceder a cualquier miembro del dato matrizVotos[0][0] transfiere las estructuras matrizVotos[0][0] y matrizVotos[0][1] a memoria cache. En el primer código, el primer conjunto de ciclos realiza 256 accesos a memoria, de los cuales la mitad son fallas en cache. El segundo conjunto de ciclos realiza 1792 accesos a memoria, de los cuales 1 de cada 14 es una falla en cache, y el resto son aciertos. Entonces, la tasa de aciertos es 128+1664 = 87.5 %. 2048 En cambio, el segundo código también realiza 2048 accesos a memoria, de los cuales 1920 corresponden a aciertos en la cache, lo que da una 1920 tasa de aciertos de 2048 = 93.75 %. Por ello, su colega recibe el bono. 6.9 Suponga que el programa palindromo del ejercicio 4.5 se ejecuta utilizando una cache directa unificada de 64 bytes de tamaño, con bloques de 4 bytes cada uno. Calcule la tasa de aciertos de la cache al ejecutar la función anterior con los argumentos radar y el largo 5, donde la ristra a procesar está residente en memoria a partir de la posición de memoria 0x200. Recuerde que la memoria cache es accedida por bytes por la CPU. Solución Analizaremos la ejecución de las instrucciones de la función con los argumentos radar y el largo 5. Por simplicidad y falta de información, no consideraremos el efecto de la pila. Inicialmente, se ejecutan las instrucciones 1 a 15, después se ejecutan las instrucciones 8 a 15, luego nuevamente las instrucciones 8 a 14, luego las instrucciones 16, 17 y finalmente las instrucciones 19 a 21. La ejecución de estas 27 instrucciones involucra leer 74 bytes desde memoria RAM. A su vez, estos bytes de memoria están almacenados en 13 líneas de 4 bytes que serán transferidas consecutivamente a los 65 Capítulo 6: Sistemas de memoria bloques 1 a 13 de la memoria cache. Como la cache inicialmente está vacía, estas 13 transferencias producen 13 fallos en cache. Entonces, la ejecución del código produce 13 fallos y 61 aciertos en cache. Por otro lado, los 5 bytes del argumento se almacenan a partir de la posición de memoria 0x200. Esto corresponde a 2 líneas de memoria, las que se transferirán a los bloques 0 y 1 de la memoria cache. Afortunadamente, no hay interferencia en el uso del bloque 1 de la memoria cache por parte del código. Durante la ejecución, se accede a los bytes 0x200, 0x201, 0x203 y 0x204 una vez, y al byte 0x202 dos veces. Estos accesos generan 2 fallos y 4 aciertos en cache. Entonces, podemos decir que, en total, se realizan 80 accesos a cache, de los cuales 15 son fallos y 65 son aciertos, para una tasa de aciertos de 81.25 %. 6.10 Intel está diseñando una nueva CPU y quieren consultarle su opinión. Las alternativas en consideración son: una cache de 32 KiB de arquitectura Harvard dividida en 16 KiB para instrucciones y 16 KiB para datos, o una cache unificada de 32 KiB. Mediciones realizadas entregan las siguientes tasas de fallo en función del tamaño de la cache: Tamaño Cache de instrucciones Cache de datos Cache unificada 16 KiB 32 KiB 0.64 % 0.39 % 6.47 % 4.82 % 2.87 % 1.99 % Además, se sabe que: El 75 % de los accesos a memoria son lecturas de instrucciones El tiempo de acceso a la cache es de 1 ciclo de reloj El tiempo de acceso a RAM es de 50 ciclos de reloj La memoria cache es de tipo write-through, con buffers de escritura La memoria cache de arquitectura Harvard tiene dos puertos de acceso, lo que permite leer una instrucción y un dato en un ciclo La memoria cache unificada tiene sólo un puerto de acceso, por lo que un acceso de lectura ó escritura de datos requiere de 1 ciclo de reloj adicional. Dado lo anterior, Capítulo 6: Sistemas de memoria 66 a) calcule la tasa de fallos para la cache de arquitectura Harvard, b) calcule el tiempo de acceso promedio para ambas arquitecturas de cache. Cuál solución recomienda Ud.? Solución El que la memoria cache sea de tipo write-through con buffers de escritura implica que, desde el punto de vista de la CPU, las escrituras a cache demoran un ciclo, ya que la transferencia de datos entre la cache y la memoria RAM ocurre bajo el control del buffer. a) La tasa de fallos para la cache de arquitectura Harvard se puede calcular como: (75 % ∗ 0.64 %) + (25 % ∗ 6.47 %) = 2.0975 % b) El tiempo de acceso promedio de la cache de arquitectura Harvard es: 1 3 ∗ ((1 − 0.64 %) + 0.64 % ∗ 50) + ∗ ((1 − 6.47 %) + 6.47 % ∗ 50) = 2.028 4 4 En cambio, el tiempo de acceso promedio de la cache unificada es: 3 1 ∗((1−1.99 %)+1.99 %∗50)+ ∗((1−1.99 %)+1+1.99 %∗50) = 2.225 4 4 Por lo tanto, la cache de arquitectura Harvard tiene un desempeño superior. Capítulo Planificación de procesos 7.1 En un centro de cómputo se desea ejecutar las siguientes tareas: Nombre Prioridad Duración TLlegada A B C D E F 4 8 1 3 2 5 4 3 8 2 6 5 0 2 3 5 6 8 Para cada uno de los siguientes algoritmos de planificación de procesos, determine el tiempo medio de retorno Tr , el tiempo medio de espera Tw y el largo promedio de la cola de procesos en espera L. Shortest Process Next (SPN) Shortest Remaining Time (SRT) Highest Response Ratio Next (HRRN) Planificación via prioridades Round Robin, con q = 2 Nótese que los algoritmos de planificación HRRN y SPN no son expropiativos, mientras que SRT, Round Robin y planificación por prioridades si lo son. Además, considere que, a menor valor, mayor es la prioridad. 67 7 68 Capítulo 7: Planificación de procesos En caso de ser necesario, suponga que la cola de procesos en espera es una cola FIFO que, en caso de coincidencia, da prioridad a los procesos nuevos por sobre los procesos expulsados por la CPU. Solución La siguiente tabla muestra los parámetros solicitados para los algoritmos de planificación dados. Algoritmo Tr Tw Shortest Process Next (SPN) 9.6̄ 5.0 Shortest Remaining Time (SRT) 9.6̄ 5.0 Highest Response Ratio Next (HRRN) 10.6̄ 6.0 Planificación via prioridades 16.0 11.3̄ Round Robin, con q = 2 13.5 8.83̄ L 1.071 1.071 1.285 2.428 1.892