2.10 Apuntadores contra arreglos. El manejo de apuntadores es uno de los aspectos más interesantes en los lenguajes de programación; los apuntadores y los arreglos comparten algunas características, sin embargo, muchas veces los compiladores trasladan de diferente manera a un programa con arreglos que otro con apuntadores. Consideraremos dos procedimientos, uno con arreglos y el otro con apuntadores, en ambos casos el objetivo será el mismo: Limpiar el arreglo, es decir, colocar 0 en todas sus localidades. void { clear1( int array[], int size ) /* Versión con arreglos */ int i; for( i = 0; i < size; i++) array[i] = 0; } void { clear2( int *array, int size ) /* Versión con apuntadores */ int *p; for( p =&array[0];p < &array[size]; p++) *p = 0; } Versión con arreglos de Clear Los dos argumentos se reciben en los registros $a0 y $a1, respectivamente. La variable i se asocia con $t0 puesto que clear no llama a otros procedimientos. clear1: add $t0, $zero, $zero # se inicializa i = 0 for1: slt beq add add add sw addi j $t1, $t0, $a1 $t1, $zero, fin_for1 $t2, $t0, $t0 $t2, $t2, $t2 $t2, $t2, $a0 $zero, 0($t2) $t0, $t0, 1 for1 # $t1 = 1 si i < size # Si $t1 tiene 0, el for termina # $t2 = 2*i # $t2 = 4*i # $t2 tiene la dirección de array[i] # Limpia la localidad array[i] # i ++ fin_for1: jr $ra Versión con apuntadores de Clear Se tiene la misma asociación de registros con los parámetros y el apuntador p se asocia con el registro $t0 clear2: add $t0, $a0, $zero # p apunta al comienzo del arreglo for2: add add add slt beq sw addi j $t1, $a1, $a1 $t1, $t1, $t1 $t1, $t1, $a0 $t2, $t0, $t1 $t2, $zero, fin_for2 $zero, 0($t0) $t0, $t0, 4 for2 # $t1 = 2*size # $t1 = 2*t1 = 4*size # $t1 = &array[size] # $t2 = 1 si p < &array[size] # Si $t2 tiene 0, el for termina # Limpia la localidad apuntada por p # p ++, el apuntador avanza 4 fin_for2: jr $ra En el código anterior se muestra una versión del código MIPS basada en apuntadores, puede observarse que, debido a que la dirección del elemento array[size] no cambia, las tres instrucciones empleadas para calcularlo pueden omitirse en el lazo, produciendo el código: clear2: for2: fin_for2: add $t0, $a0, $zero # p apunta al comienzo del arreglo add add add slt beq sw addi j $t1, $a1, $a1 $t1, $t1, $t1 $t1, $t1, $a0 $t2, $t0, $t1 $t2, $zero, fin_for2 $zero, 0($t0) $t0, $t0, 4 for2 # $t1 = 2*size # $t1 = 2*t1 = 4*size # $t1 = &array[size] # $t2 = 1 si p < &array[size] # Si $t2 tiene 0, el for termina # Limpia la localidad apuntada por p # p ++, el apuntador avanza 4 jr $ra Este código es mas rápido por que incluye menos instrucciones en la parte repetitiva. En general, los compiladores capaces de optimizar código intentan manejar los arreglos sumando una variable para obtener la dirección del i-ésimo elemento, en lugar de realizar multiplicaciones. El código resultante será más rápido por que para toda arquitectura, una suma es más rápida que una multiplicación. Si la función clear se aplica sobre un arreglo de 500 elementos, ¿Qué tan rápida es la versión 2 del código con respecto a la versión 1(Suponiendo que se realiza una instrucción en cada ciclo de reloj)? La respuesta a esta pregunta puede obtenerse recordando los aspectos vistos acerca del rendimiento de las computadoras (se deja como ejercicio). 2.11 Un simulador del repertorio de instrucciones En esta sección se describe al simulador SPIM, creado por el Dr. James Larus, graduado en la Universidad de Wisconsin, Madison. Y actualmente investigador de la empresa Microsoft. SPIM es un simulador autónomo para programas en lenguaje ensamblador escritos para los procesadores R2000/R3000, los cuales son procesadores de 32 bits de la corporación MIPS. SPIM lee y ejecuta el código en lenguaje ensamblador, proporciona un depurador simple y un juego simple de servicios del sistema operativo. SPIM soporta casi el conjunto completo de instrucciones del ensamblador-extendido para el R2000/R3000 (omite algunas comparaciones de punto flotante complejas y detalles del sistema de paginación de memoria.). El doctor Larus tiene disponible al programa SPIM para diferentes sistemas operativos, los cuales pueden obtenerse libremente desde su página web situada en: http://www.cs.wisc.edu/~larus/ en la que inmediatamente se encuentra el vínculo al programa SPIM. encuentra el código fuente completo y documentación. También se Es necesario descargar el programa para evaluar al repertorio de instrucciones bajo estudio. En esta sección solo se muestran algunos aspectos del programa SPIM útiles para simular los programas hasta el momento realizados. En la versión para WINDOWS el programa SPIM tiene el aspecto que se presenta en la figura 2.7, en la que se distinguen cuatro ventanas: La primer ventana contiene a los registros, se muestra el valor de todos los registros de propósito general (de $0 a $31), además del Contador del Programa (PC) y de otros registros para el manejo de excepciones (una excepción es un evento erróneo debido a alguna incongruencia durante la ejecución de un programa). También se muestran dos registros HI y LO, estos registros son dedicados a las multiplicaciones y divisiones La segunda ventana contiene una parte de memoria en la que se colocará el código de usuario (el código a evaluar), el código se muestra de manera simbólica (ensamblador). En esta ventana se observa al de código descrito en el archivo exceptions.s, este código corresponde a una especie del kernel de la máquina. Este código incluye una llamada a la función main, de manera que cualquier programa que se quiera simular deberá incluir al procedimiento principal (main). La idea es que los usuarios avanzados puedan hacer su propio archivo para que hagan un manejo diferente de las excepciones. La tercer ventana muestra una parte de la memoria en la que se colocarán los datos, en hexadecimal. Esto incluye una sección de propósito general, una parte dedicada a la pila (stack) y otra que forma parte del Kernel. La cuarta ventana es la ventana de mensajes, en la que se describen los diferentes eventos que van ocurriendo durante la simulación. Fig. 2.7 Aspecto del programa SPIM para Windows 2.11.1 La consola del programa SPIM Además de las cuatro ventanas que se encuentran en la ventana principal del programa, cuando el programa se ejecuta se despliega en pantalla otra ventana conocida como la consola del programa SPIM (ver figura 2.8). La consola es el mecanismo por medio del cual se van a insertar datos al programa o se van a observar algunos resultados del mismo. El Kernel incluido permite el manejo de una instrucción denominada SYSCALL. Con SYSCALL se realiza una llamada al Kernel para solicitar algún servicio, que puede consistir en la captura de un dato o bien la presentación de resultados en la consola. Antes de invocar a SYSCALL, se debe especificar el número de servicio en el registro $V0, y si el servicio requiere argumentos, éstos se deberán colocar en los registros $a0 y $a1, dependiendo del número de argumentos, sin embargo, si el servicio es para números en punto flotante, se utilizará al registro $f0 para el argumento (La arquitectura MIPS incluye 32 registros para el manejo de números en punto flotante, y un hardware dedicado para las operaciones en punto flotante). En la tabla 2.2 se muestran todos los servicios que soporta el Kernel. Fig. 2.8 La consola del Programa SPIM Tabla 2.2 Servicios que nos proporciona el Kernel del Programa SPIM 2.11.2 Pseudo instrucciones. Debido a que el repertorio MIPS es un repertorio de instrucciones reducido, para dar un poco mas de flexibilidad a los programadores, es posible generar un conjunto de pseudo instrucciones; una pseudo instrucción realiza algún tipo de operación, sin embargo no tiene una interpretación directa en Hardware, sino que tiene que traducirse a una o mas instrucciones reales para que pueda ser ejecutada. Así por ejemplo, la pseudo instrucción: move reg_destino, reg_fuente Mueve el registro fuente al registro destino, pero no es una instrucción real, sino que el simulador SPIM la traduce a: ori reg_destino, $zero, reg_fuente Para las multiplicaciones, se puede utilizar la pseudo instrucción: mul $s1, $s2, $s3 esta pseudo instrucción en realidad es traducida en las instrucciones siguientes: mult $s2, $s3 mflo $s1 # Esta es la instrucción que multiplica a $s2 con $s3, pero el # resultado lo deja en los registros HI y LO # Esta instrucción coloca la parte baja del resultado y la # coloca en $s1toma la parte baja del resultado Puede notarse que la pseudo instrucción es suficiente cuando se sabe que el resultado alcanza en un registro de 32 bits. Pero si se están manipulando números muy grandes, además de la pesudo instrucción se debería usar a la instrucción: mfhi $s4 # Para colocar la parte alta en un registro de propósito general La pseudo instrucción mul también puede usarse con el segundo parámetro con un valor inmediato, por ejemplo la pseudo instrucción: mul $t4, $t1, 4 Es traducida a: ori $1, $0, 4 mult $9, $1 mflo $12 Otra pseudo instrucción bastante útil es la siguiente: la $a0, str1 Cuando se codifica un programa y se van a utilizar cadenas constantes, se sabe que éstas se colocarán en memoria, sin embargo se ignora en que dirección serán colocadas, por lo que no se sabría como direccionarlas. Con la se obtiene en el registro $a0 la dirección donde inicia la cadena str1 (la – load address). Esta pseudo instrucción es traducida a dos instrucciones, la primera para cargar la parte alta de la dirección (lui) y la segunda para obtener la parte baja (ori). Existen más pesudo instrucciones, sólo se han mencionado las mas comunes y que son necesarias para el desarrollo de algunos programas que se realizarán para la evaluación del simulador. En el apéndice A del texto “Computer Organization & Design, The hardware/software interface” (página A-51) se encuentra un listado con todas las instrucciones de los procesadores MIPS R2000/R3000 y las pseudo instrucciones que soporta el simulador SPIM. Este apéndice esta disponible en formato PDF en la página del Dr. Larus, su referencia es: http://www.cs.wisc.edu/~larus/HP_AppA.pdf . 2.11.3 Ejemplos de uso del simulador: Ejemplo 1: El programa “Hola Mundo” Se trata de un programa que desplegará en la consola a la cadena “HOLA MUNDO”, básicamente se requiere obtener la dirección del inicio de la cadena y solicitar el servicio 4 al kernel (con la instrucción SYSCALL). El código del programa es: main: addi $v0, $zero, 4 la $a0, cadena syscall jr $31 .data cadena: .asciiz # Se usará el servicio 4 # Se obtiene el argumento # Solicita el servicio # Termina la función principal "Hola Mundo" La salida en la consola es: Observaciones: El código se puede escribir con cualquier editor de texto (texto sin formato) y salvarse con cualquier extensión, se sugiere s por asociación con el programa. El código principal debe incluir a la etiqueta main por que en el kernel del programa existe un salto hacia esa etiqueta. La ejecución puede hacerse paso a paso con <F10> o con múltiples pasos con <F11>. O simplemente con el comando go, con <F5>. Una vez que finalice el código de usuario, si se continúa ejecutando, se regresa del main al Kernel y solicita el servicio 10 (exit). Ejemplo 2: Un programa que suma dos números. En este ejemplo se usará a la consola para obtener dos enteros, luego los enteros se sumarán y se mostrará el resultado. El código del programa: main: addi $v0, $zero, 4 la $a0, str1 syscall str1: str2: str3: str4: # Servicio 4 # se imprime una cadena # para pedir un número addi $v0, $0, 5 syscall add $8, $0, $v0 # Servicio 5 # se lee el número # se coloca en $8 addi $v0, $zero, 4 la $a0, str2 syscall # Servicio 4 # se imprime una cadena # para pedir el otro número addi $v0, $0, 5 syscall add $9, $0, $v0 # servicio 5 # se lee el otro numero # se coloca en $9 addi $v0, $zero, 4 la $a0, str3 syscall # Servicio 4 # para indicar que se # dará el resultado add $a0, $8, $9 addi $v0, $0, 1 syscall # Se coloca la suma como argumento # Servicio 1 # se muestra el resultado addi $v0, $zero, 4 la $a0, str4 syscall # Servicio 4 # muestra una cadena de # terminación del programa jr # fin del main $31 .data .asciiz "Dame un numero: " .asciiz "Dame otro numero: " .asciiz "La suma de los numeros es : " .asciiz "\n\nFin del programa, Adios . . ." Una corrida generó en la consola: Ejemplo 3: El factorial de un número. Este programa está basado en la función recursiva que se presentó en la sección 2.5 (Soporte de procedimientos) sólo se le hicieron algunas modificaciones para el manejo correcto de las constantes. En este programa se utilizó la pseudo instrucción li (por load immediate) para cargar una constante en un registro, que es equivalente a hacer una operación OR del registro 0 con la constante y colocar el resultado en el registro que se quiere cargar. El código del programa: main: addi sw $sp, $sp, -4 $ra, 4 ($sp) li $v0, la $a0, syscall li $v0, syscall add $s0, 4 str1 # Salida a la consola 5 # Lectura de un numero $0, $v0 # El numero esta en $v0, se copia a $s0 add jal $a0, $0, $s0 fact # Prepara el parametro # Llama al factorial add $s1, $v0, $zero # Respalda el resultado li $v0, la $a0, syscall addi $v0, add $a0, syscall li $v0, la $a0, syscall addi $v0, add $a0, syscall li $v0, la $a0, syscall fact: # Hace espacio en la Pila # Salva la dirección de retorno 4 str2 # Una cadena a la consola $0, 1 $s0, $zero # Una entero a la consola 4 str3 # Una cadena a la consola $0, 1 $s1, $zero # Una entero a la consola 4 str4 lw addi jr $ra, 4 ($sp) $sp, $sp, 4 $31 addi sw sw $sp, $sp, -8 $ra, 4 ($sp) $a0, 0 ($sp) # Recupera la dirección de retorno # Restablece el tope de la Pila # # # # La funcion que obtiene el factorial Hace espacio en la Pila Salva la dirección de retorno Salva al argumento n # Se evalúa para ver si ocurre el caso base (cuando n < 1): slti $t0, $a0, 1 # $t0 = 1 si n < 1 beq $t0, $zero, L1 # salta a L1 si no ocurre el caso base # Si ocurre el caso base, deberían recuperarse los datos de pila, # pero como no se han modificado, no es necesario. # Lo que si se requiere es restablecer al puntero de la pila. addi addi jr $v0, $zero, 1 $sp, $sp, 8 $ra # retorno = 1 # Restablece al apuntador de la pila # Finaliza regresando el resultado en $v0 # Si no ocurre el caso base, prepara la llamada recursiva L1: addi $a0, $a0, -1 # n = n - 1 jal fact # llama a fact con n - 1 # Después de la llamada, se hace la restauración de los registros: lw $a0, 0($sp) # Recupera el valor de n lw $ra, 4($sp) # recupera la dirección de retorno addi $sp, $sp, 8 # Restablece al apuntador de la pila #Para concluir, se actualiza el valor de retorno y se regresa el control al invocador: mul $v0, $a0, $v0 # Retorno = n * fact (n - 1) jr $ra # regresa al invocador str1: str2: str3: str4: .data .asciiz .asciiz .asciiz .asciiz "Dame un numero: " "El factorial del numero " " es : " "\n\nFin del programa, Adios . . ." Como ejemplo, se obtuvo el factorial de 7. Ejemplo 4: Manejo de un arreglo. En este ejemplo se pretende mostrar como un arreglo de n enteros puede ser ubicado en la pila. Para ello solo se piden algunos enteros al usuario para luego imprimirse en pantalla en orden inverso. El código C que realiza las actividades deseadas es: void main() { int i, n, *A, *t; printf("Manejo de un arreglo\n"); printf("Indica el tamaño del arreglo: "); scanf("%d", &n); A = (int *) malloc(n*sizeof(int)); for( t = A, i = 0; i < n; i++, t++) { printf(" Dame el dato %d : ", i); scanf("%d", t); } printf("\nLos números en orden inverso: \n"); for( t = &A[n-1], i = 0; i < n; i++, t--) printf(" %d\n", *t); } free(A); Las variables i, n, A y t se asocian con los registros: $t0, $t1, $t2 y $t3 (este procedimiento es aislado). El código MIPS correspondiente al código C anterior es: # Programa que maneja un arreglo imprimiendo los elementos en orden inverso main: # El Primer Mensaje li $v0, 4 la $a0, str1 syscall #Mensaje que pide el tamaño del arreglo li $v0, 4 la $a0, str2 syscall #Se toma el valor li $v0, 5 syscall add $t1, $v0, $0 #El tamaño del arreglo se guardo en $t1 # Se reserva el espacio en memoria mul $t4, $t1, 4 sub $sp, $sp, $t4 addi $t2, $29, 0 #en $t4 esta el total de bytes requeridos en la pila #Se hace espacio en la pila para los datos #$t2 es el apuntador al comienzo del arreglo #inicia el primer for: addi $t0, $0, 0 addi $t3, $t2, 0 for1: # i = 0 # t = A slt $t5, $t0, $t1 beq $t5, $zero, fin_for1 #Se pide el número li $v0, 4 la $a0, str3 syscall #imprime el indice del numero li $v0, 1 add $a0, $0, $t0 syscall #imprime los dos puntos li $v0, 4 la $a0, str4 syscall #Toma el valor de la consola li $v0, 5 syscall sw $v0, 0($t3) addi $t0, $t0, 1 addi $t3, $t3, 4 #i ++ #t ++ j for1 #En este ciclo se pidieron los Numeros fin_for1: #El mensaje de aviso li $v0, 4 la $a0, str5 syscall # Inicio del segundo for addi $t0, $0, 0 # i = 0 addi $t4, $t1, -1 # $t4 = n - 1 mul $t4, $t4, 4 # $t4 = 4 * (n - 1) add $t3, $t2, $t4 # t = &A[n-1] for2: slt $t5, $t0, $t1 beq $t5, $zero, fin_for2 #imprime el numero lw $t6, 0($t3) li $v0, 1 add $a0, $0, $t6 syscall # Primero lo carga #imprime el retorno de carro li $v0, 4 la $a0, str6 syscall addi $t0, $t0, 1 addi $t3, $t3, -4 #i ++ #t -- j for2 fin_for2: mul $t4, $t1, 4 add $sp, $sp, $t4 jr $ra str1: str2: str3: str4: str5: str6: #en $t4 esta el total de bytes requeridos en la pila #Se libera el espacio solicitado en la pila #fin de la función main #Cadenas del programa .data .asciiz " Manejo de un arreglo \n" .asciiz "Indica el tamaño del arreglo : " .asciiz "Dame el número " .asciiz " : " .asciiz "\nLos numeros en orden inverso son: \n .asciiz "\n " " Los resultados arrojados en la consola para una corrida del programa son: 2.11.4 Configuración del programa PSIM El programa PSIM permite configurar algunos parámetros; la consola de configuración se encuentra en la opción settings del menú simulator, y tiene la siguiente forma: En la ayuda del programa se describe el objetivo de cada una de las opciones de configuración. El programa funciona correctamente con los valores preestablecidos. Tarea 5 Esta tarea es para familiarizarse con el simulador SPIM, se entregará en un disco flexible o en una memoria USB, para cada ejercicio se debe considerar: i. El archivo con el código MIPS, con comentarios. ii. En el código de cada programa se deberán incluir instrucciones que envíen su nombre y grupo a la consola, al inicio o al final del programa. iii. Un archivo en Word con la consola pegada, mostrando al menos tres ejemplos de ejecución. Ejercicios: 1.- El código que obtiene el menor de tres números inclúyalo en una función principal y simúlelo. 2.- Realice la función principal para el programa SORT y simúlelo, recordar que debe incluirse a la función SWAP (sug. Puede usarse la pila para almacenar el arreglo). 3.- Realice un programa que obtenga el mayor de un arreglo de n elementos proporcionados por el usuario (sug. Usar la función desarrollada en la tarea anterior). 4.- Realice un programa que obtenga al n-ésimo termino de la serie de Fibonacci (sug. Usar la función desarrollada en la tarea anterior).