Download UNIDAD 2: Instrucciones: el lenguaje de las computadoras. 2.1

Document related concepts
no text concepts found
Transcript
UNIDAD 2: Instrucciones: el lenguaje de las computadoras.
2.1 Introducción
Para comandar una computadora se le debe “hablar” en su lenguaje. Las palabras del
lenguaje de una máquina son llamadas instrucciones, y su vocabulario es llamado
repertorio de instrucciones. En esta sección se expone el repertorio de instrucciones de una
computadora real, considerando una forma entendible para los humanos (ensamblador) y
una forma entendible por la computadora (código máquina).
Podría pensarse que los lenguajes de máquina son tan diversos como los lenguajes de los
humanos, sin embargo no es así, los lenguajes de máquina son muy similares entre sí; esto
se debe a que todas las computadoras son construidas con tecnologías de hardware basadas
en principios fundamentales similares y por que hay unas cuantas operaciones básicas que
todas las máquinas deben proporcionar. Más aún, los diseñadores de computadoras tienen
una meta común: Encontrar un lenguaje que haga fácil la construcción del hardware y el
compilador; mientras se maximiza el rendimiento y se minimiza el costo. Esta meta ha sido
honrada a lo largo del tiempo; la siguiente cita fue escrita antes de que fuera posible
adquirir una computadora y es tan cierta actualmente, como lo fue en 1947:
Es fácil ver por métodos lógicos formales que existen ciertos [repertorios de
instrucciones] que son en abstracto adecuados para controlar y causar la ejecución
de cualquier secuencia de operaciones. . . . Las consideraciones realmente
decisivas desde el punto de vista actual, en seleccionar un [repertorio de
instrucciones], son más de naturaleza práctica: La simplicidad del equipo
demandado por el [repertorio de instrucciones], y la claridad de su aplicación a
problemas realmente importantes junto con la velocidad del manejo de esos
problemas.
Burks, Goldstine, y von Neumann, 1947
La “simplicidad del equipo” es una consideración tan valiosa para las máquinas actuales
como lo fue en los 50’s. La meta de este capítulo es mostrar un repertorio de instrucciones
que siga este consejo, mostrando como éste es representado en hardware y su relación con
los lenguajes de alto nivel.
El repertorio de instrucciones bajo estudio es de una arquitectura MIPS, usado por NEC,
Nintendo, Silicon Graphics, y Sony; entre otros. Y es un repertorio de instrucciones típico
diseñado en los 80’s.
2.2 Operaciones y operandos.
Toda computadora debe realizar operaciones aritméticas. La notación en el lenguaje
ensamblador MIPS
Add a, b, c
Instruye a la computadora a sumar las dos variables b y c, y colocar la suma en a.
Esta notación es rígida, en el sentido que cada instrucción aritmética de MIPS realiza una
sola operación y debe tener tres variables (“variable” en un sentido vago, mas adelante se
analizarán los operandos en MIPS). Si se quieren sumar las variables b, c, d y e en la
variable a, será necesaria la secuencia:
add
add
add
a, b, c
a, a, d
a, a, e
# La suma de b y c es puesta en a
# La suma de b, c y d es puesta en a
# La suma de b, c, d y e es puesta en a
Se requiere de tres instrucciones para sumar cuatro variables.
Como en todos los ensambladores, cada línea contiene a lo más una instrucción. Los
comentarios inician con el símbolo # y terminan al final del renglón.
El número natural de operandos para una operación aritmética como la suma es tres, los dos
operandos a sumar y el operando donde se colocará el resultado. Por lo que es adecuado
que las instrucciones aritméticas cuenten con tres operandos: no más o menos; de acuerdo
con la filosofía de mantener un hardware simple, es evidente que el hardware para un
número variable de operandos es más complicado que el hardware para un número fijo.
Esta situación ilustra el primero de cuatro principios para el diseño de hardware:
Principio de diseño 1: La simplicidad favorece la regularidad.
En los ejemplos siguientes se muestra la relación que existe entre un lenguaje de alto nivel
y el código MIPS.
Ejemplo: Compilando dos asignaciones a código MIPS
Este segmento de un programa en C contiene cinco variables a, b, c, d y e:
a = b + c;
d = a – e;
La traducción de C al ensamblador MIPS la realiza el compilador correspondiente, mostrar
el código MIPS que produciría para estas asignaciones.
Respuesta:
Una instrucción MIPS opera con dos operandos fuentes y pone el resultado en un operando
destino. Para dos asignaciones simples, solo se requiere de dos instrucciones:
add
sub
a, b, c
d, a, e
Ejemplo: Compilando una asignación mas compleja
Para la siguiente asignación en C:
f = (g + h) – (i + j);
¿Qué produciría el compilador?
Respuesta:
El compilador separa la asignación y utiliza variables temporales, de manera que el
resultado es:
add
add
sub
t0, g, h
t1, i, j
f, t0, t1
# La variable temporal t0 contiene g + h
# La variable temporal t1 contiene i + j
# f obtiene t0 – t1, que es (g + h) – (i + j)
Hasta el momento no se ha puesto atención a los símbolos involucrados en el código MIPS,
sin embargo, a diferencia de los lenguajes de alto nivel, los operandos de las instrucciones
no pueden ser cualquier variable; mas bien quedan limitados por un número especial de
localidades llamadas registros.
Los registros son los ladrillos en la construcción de una computadora, se definen durante el
diseño del hardware y quedarán visibles al programador cuando la computadora este
completada. El tamaño de los registros en la arquitectura MIPS es de 32 bits; los grupos de
32 bits son tan frecuentes que por lo general se les da el nombre de palabra en la
arquitectura MIPS.
Una principal diferencia entre las variables de un lenguaje de programación y los registros,
es el limitado número de registros. MIPS tiene 32 registros, por lo que para una instrucción
aritmética se puede elegir entre 32 registros de 32 bits para los tres operandos. La razón del
límite en 32 registros se explica con el segundo principio del diseño de hardware.
Principio de diseño 2: Si es más pequeño es más rápido.
Un número muy grande de registros automáticamente incrementa el tiempo del ciclo de
reloj, simplemente por que las señales eléctricas requieren de más tiempo cuando necesitan
viajar más lejos.
Directivas tales como “Si es mas pequeño es más rápido” no son absolutas; 31 registros no
pueden ser mas rápidos que 32. Los diseñadores de hardware deben tomar muy en cuenta
este principio y balancear entre el deseo anormal de los programadores por contar con un
número grande de registros con el deseo de los diseñadores de mantener un ciclo de reloj
rápido.
Aunque se podrían simplemente considerar los números del 0 al 31 para nombrar a los
registros, para no confundirlos con valores constantes, por conveniencia se les antepone el
símbolo de pesos ($), de manera que los registros son $0, $1, ..., $31. Además, para
simplificar el trabajo del compilador, los registros que correspondan directamente con los
nombres de las variables en alto nivel, se nombrarán con $s0, $s1,...; y los registros
temporales serán $t1, $t2, $t3,... Esto sólo por convención, en la tabla 2.1 se muestran estas
y otras convenciones utilizadas.
N am e
$zero
$v0-$v1
$a0-$a3
$t0-$t7
$s0-$s7
$t8-$t9
$gp
$sp
$fp
$ra
R egister num ber
U sage
0
the constant value 0
2-3
values for results and expression evaluation
4-7
argum ents
8-15
tem poraries
16-23
saved
24-25
m ore tem poraries
28
global pointer
29
stack pointer
30
fram e pointer
31
return address
Tabla 2.1 Convenciones utilizadas aplicadas en el uso de registros.
Puede notarse que el registro $0 siempre contendrá el valor 0, este convención es bastante
útil cuando se realizan comparaciones con cero o brincos condicionales.
Ejemplo: Uso de registros.
Se repite el ejemplo anterior, pero al utilizar los nombres de los registros en el código
ensamblador.
f = (g + h) – (i + j);
¿Qué produciría el compilador?
Respuesta:
Suponiendo que las variables f, g, h, i y j se asocian con los registros $s0, $s1, $s2, $s3 y
$s4, respectivamente.
add
add
sub
$t0, $s1, $s2
$t1, $s3, $s4
$s0, $t0, $t1
# El registro $t0 contiene $s1+ $s2
# El registro $t1 contiene $s3+ $s4
# f obtiene $t0 – $t1, que es (g + h) – (i + j)
Los lenguajes de programación usan variables simples que se asocian directamente con
registros; sin embargo también utilizan estructuras de datos un poco más complejas como
es los arreglos; y es imposible que un arreglo alcance dentro de los registros del procesador.
Entrada
Control
Memoria
Camino
de los
datos
Salida
Procesador
Fig. 2.1 Cinco componentes clásicos de una computadora
Recordando los cinco elementos clásicos de la computadora (que se repite en la figura 2.1,
por conveniencia), se observa que debido al número limitado de registros, el lugar mas
adecuado para las estructuras de datos es la memoria.
Sin embargo, puesto que las instrucciones aritméticas solo se realizan con los operandos en
registros, la arquitectura MIPS debe incluir algunas instrucciones que permitan la
transferencia de datos de memoria a registros y viceversa. La memoria puede ser
considerada como un arreglo unidimensional, grande, con las direcciones actuando como
índices en el arreglo, iniciando en la 0. Por ejemplo, en la figura 2.2, la dirección del tercer
dato en memoria es 2 y su valor es Memoria [2] = 10.
Fig. 2.2 Direcciones de Memoria y su contenido en algunas localidades
La instrucción que mueve datos desde la memoria a un registro se le conoce como carga, y
su formato es:
lw
$s0, 100 ( $s1 )
que significa
$s0 = Memoria[$s1+ 100 ]
lw es el nombre de la instrucción (load word), el primer registro que aparece es el que será
cargado ($s0, en este caso), luego se incluye una constante (100, en este caso) la cual se
considera como un desplazamiento (offset) y finalmente entre paréntesis aparece otro
registro ($s1, en este caso), el cual es conocido como registro base. La dirección de la
palabra a cargar se forma sumando el valor del registro base con el desplazamiento.
Ejemplo: Carga de memoria.
Para la siguiente asignación en C:
g = h + A[8];
¿Qué produciría el compilador? Suponiendo que el comienzo del arreglo A se encuentra en
el registro $s0, y que las variables g y h se asocian con los registros: $s1 y $s2,
respectivamente.
Respuesta:
Primero se debe tener acceso a la memoria para la lectura del dato:
lw
$t0, 8( $s0)
# El registro $t0 contiene A[8]
Ahora ya es posible realizar la suma:
add
$s1, $s2, $t0
# g = h + A[8]
Los compiladores son los que se encargan de asociar las estructuras de datos con la
memoria. Por lo que el compilador debe poder colocar la dirección adecuada en las
instrucciones de transferencias.
Puesto que 8 bits (1-byte) son útiles en muchos programas, la mayoría de computadoras
conservan el direccionamiento por bytes individuales. Por lo que el direccionamiento por
palabras se refiere a la lectura de un conjunto de 4 bytes. Esto significa que las direcciones
secuenciales de palabras deben diferir en 4. En la figura 2.2 se mostraron a los datos con
direcciones continuas, sin embargo, puesto que se están considerando palabras (de 32 bits),
la distribución correcta de los datos sería la que se muestra en la figura 2.3.
Debido a que las memorias organizan a los datos por bytes, cuando se manejan arreglos de
palabras se afecta a los índices de los mismos, entonces el compilador debe calcular
adecuadamente la dirección del dato que será transferido. En el último ejemplo, para que se
haga la lectura correcta del elemento 8 del arreglo A, el desplazamiento debe multiplicarse
por 4, de manera que a $s0 se le sume 32 (8x4), así se seleccionará al dato A[8] y no a
A[8/4].
Fig. 2.3 Direccionamiento por palabras.
La instrucción complementaria a la carga se llama almacenamiento (store), para transferir
un dato desde un registro a la memoria. Su formato es similar al de las instrucciones de
carga:
sw
$s0, 100 ( $s1 )
que significa
Memoria[$s1+ 100 ]=$s0
sw es el nombre de la instrucción (store word).
Ejemplo: Carga y Almacenamiento.
Para la siguiente asignación en C:
A[12] = h + A[8];
¿Qué produciría el compilador? Suponiendo que el comienzo del arreglo A se encuentra en
el registro $s0, y que la variable h se asocia con el registro $s1:
Respuesta:
Primero se debe acceder a la memoria para la carga del dato:
lw
$t0, 32( $s0)
# El registro $t0 contiene A[8]
Ahora ya es posible realizar la suma:
add
$t0, $s1, $t0
# El registro $t0 contiene h + A[8]
Por último se realiza el almacenamiento:
sw
$t0, 48( $s0)
# A[12]= h + A[8]
Los índices del arreglo (8 y 12) se deben multiplicar por 4 para obtener las direcciones
adecuadas de los datos en memoria.
Ejemplo: Usando una variable como índice de un arreglo.
La siguiente asignación utiliza a la variable i como índice del arreglo A:
g = h + A[i];
¿Qué producirá el compilador? Suponiendo que el comienzo del arreglo A se encuentra en
el registro $s0, y que las variables g, h e i se asocian con los registros: $s1, $s2 y $s3,
respectivamente.
Respuesta:
Antes de accesar a la memoria se debe obtener la dirección adecuada del dato a leer, puesto
que solo se han considerado instrucciones de suma, primero se obtendrá i x 4, realizando i
+ i = 2i, y luego 2i + 2i = 4i:
add
add
$t0, $s3, $s3
$t0, $t0, $t0
# $t0 = i + i = 2i
# $t0 = 2i + 2i = 4i
La carga se hace con la instrucción:
add $t0, $t0, $s0
# $t0 tiene la dirección de A[i]
lw
$t1, 0( $t0)
# $t1 = A[i]
Finalmente se realiza la suma:
add
$s1, $s2, $t1
# g = h + A[i]
Muchos programas tienen más variables que registros en el procesador. En consecuencia, el
compilador intenta mantener a las variables más usadas en registros y el resto en memoria,
usando cargas y almacenamientos para mover variables entre registros y memoria. El
proceso de poner a las variables menos usadas (o aquellas que se usarán posteriormente) en
memoria se conoce como derramamiento de registros (spilling registers).
El principio de hardware que relaciona el tamaño con la velocidad sugiere que la memoria
debe ser mas lenta que los registros porque el tamaño del conjunto de registros es menor
que el de la memoria. El acceso a los datos es más rápido si los datos están en registros.
Y los datos son más útiles cuando están en registros por que una instrucción aritmética se
aplica sobre dos registros, mientras que los accesos a memoria solo manipulan un dato.
En conclusión, los datos en los registros en MIPS toman un menor tiempo y tienen una
productividad más alta que los datos en la memoria. Para aumentar el rendimiento, los
compiladores MIPS deben usar los registros eficientemente.
2.3 Representación de instrucciones.
Hasta el momento se han considerado algunas instrucciones MIPS compuestas de un
nombre (nemotécnico) y una serie de operandos; sin embargo dentro de la computadora las
instrucciones se almacenan en memoria como pequeños capacitores cargados o descargados
y se transfieren entre dispositivos como señales eléctricas con niveles de voltajes altos (5
volts) y bajos (0 volts). Por lo que son suficientes dos símbolos para la representación de
las instrucciones. Un sistema numérico con dos símbolos es el sistema binario, por eso se
utiliza para la representación de las instrucciones.
Sólo hemos considerado 2 tipos de instrucciones, aritméticas y de transferencia de datos, si
las comparamos podremos notar que en ambos casos existen tres operandos, en el caso de
las instrucciones aritméticas los tres operandos son registros (y como son 32 registros, con
5 bits es suficiente su representación) y para las transferencias de datos dos operandos son
registros y el tercero es una constante, es evidente que no es posible disponer solo de 5 bits
para el valor de la constante, puesto que su valor sería muy limitado. Lo que implica que si
se quiere conservar el mismo formato para ambas instrucciones, estas tendrían diferentes
tamaños. Esto da pie al tercer principio de diseño:
Principio de diseño 3: Un buen diseño demanda compromisos.
Los diseñadores de MIPS enfrentaron el problema de decidir si mantenían a todas las
instrucciones del mismo tamaño, generando con ello diferentes formatos de instrucciones o
si mantenían el formato ocasionando instrucciones de diferentes tamaños. Se
comprometieron con el primer punto y buscando regularidad con el tamaño de los datos,
todas las instrucciones en MIPS son de 32 bits.
De manera que se tienen diferentes formatos, las instrucciones aritméticas son del Tipo-R
por que solo se aplican sobre registros. El formato para las instrucciones Tipo-R es:
op
rs
rt
rd
shamt
funct
6 bits
5 bits
5 bits
5 bits
5 bits
6 bits
El significado para cada uno de los campos es:
ƒ
ƒ
ƒ
ƒ
ƒ
ƒ
op: Operación básica de la instrucción, tradicionalmente llamado opcode. Su valor es 0
en el caso de las operaciones aritméticas.
rs: El primer operando fuente.
rt: El segundo operando fuente.
rd: El registro destino, obtiene el resultado de la operación.
shamt: Cantidad de desplazamiento (shift amount), solo se aplica a las instrucciones de
desplazamiento, aquí su valor será 0 (se revisa mas adelante).
funct: Función, selecciona una variante de la operación, por ejemplo, la suma y resta
ambas son operaciones aritméticas, pero realizan diferentes operaciones. Su valor es de
32 para la suma y 34 para la resta.
Las instrucciones lw y sw son del tipo-I, por que incluyen una constante (dato inmediato).
El formato para las instrucciones del tipo-I es:
op
rs
rt
inmediato
6 bits
5 bits
5 bits
16 bits
En este caso tenemos:
ƒ
ƒ
ƒ
ƒ
op: Su valor es 35 para las cargas y 43 para los almacenamientos.
rs: Registro base para la dirección de memoria.
rt: Registro a ser cargado o almacenado.
inmediato: Constante que corresponde al desplazamiento.
Ejemplo: Trasladando a código máquina.
¿Cuál es el código máquina de la instrucción: add $t0, $s1, $s2?
Respuesta:
Tomando como referencia la tabla 1, el registro $t0 es el 8 , $s1 es el 17 y $s2 es el 18. De
manera que la representación en decimal de esta instrucción es:
0
17
18
8
0
32
Y su representación binaria corresponde a:
000000
10001
10010
01000
00000
100000
6 bits
5 bits
5 bits
5 bits
5 bits
6 bits
Ejemplo: Trasladando cargas y almacenamientos.
La asignación en C: A[12] = h + A[8];
Produjo el siguiente código en ensamblador:
lw
add
sw
$t0, 32( $s0)
$t0, $s1, $t0
$t0, 48( $s0)
# El registro $t0 contiene A[8]
# El registro $t0 contiene h + A[8]
# A[12]= h + A[8]
¿Cuál es su correspondiente código máquina?
Respuesta:
La versión decimal del código máquina es:
35
16
8
0
17
8
43
16
8
32
8
0
32
48
Para obtener los números a los que corresponden los registros, puede usarse la tabla 2.1, la
versión binaria del código corresponde a:
100011
10000
01000
000000
10001
01000
101011
10000
01000
0000000000100000
01000
00000
100000
0000000000110000
Actualmente las computadoras se construyen bajo dos principios clave:
1. Las instrucciones se representan como números.
2. Los programas pueden ser almacenados en memoria para ser leídos o escritos al
igual que los números.
Estos principios permiten el concepto del programa almacenado, en la figura 2.4 se
muestra la potencialidad de este concepto, específicamente la memoria puede contener el
código fuente para un programa editor, el correspondiente código máquina compilado, el
texto que el programa compilado está usando, y también al compilador que generó el
código máquina.
Fig. 2.4 El concepto del programa almacenado,
ilustrado por medio de un ejemplo.
2.4 Instrucciones para tomar decisiones
Lo que distingue a las computadoras de las simples calculadoras es su capacidad para tomar
decisiones basadas en los datos de la entrada. En los lenguajes de alto nivel esta acción se
realiza con la expresión if algunas veces acompañada con expresiones goto y etiquetas. La
arquitectura MIPS cuenta con dos instrucciones de brincos condicionales. La primera:
beq
registro1, registro2, L1
compara el contenido del registro1 con el del registro2, si son iguales, la siguiente
instrucción será la que se encuentra ubicada en la etiqueta L1 (beq – branch if equal). La
segunda instrucción es:
bne
registro1, registro2, L1
y en este caso el brinco se realizará si los contenidos de los registros no son iguales (bne –
branch if not equal).
Ejemplo: Compilando una expresión if en un brinco condicional.
Para el siguiente código:
L1:
if ( i == j ) goto L1
f = g + h;
f = f – i;
Suponiendo que las cinco variables (f a j) se asocian con los registros ($s0 a $s4) ¿Cuál es
el código MIPS compilado?
Respuesta:
L1:
beq
add
sub
$s3, $s4, L1
$s0, $s1, $s2
$s0, $s0, $s3
# Se realiza la comparación
# Si no fueron iguales se hace la suma
# Si ocurrió la igualdad, la siguiente instrucción es la
# resta
Los compiladores se encargan de colocar etiquetas cuando éstas no aparecen en el código
de alto nivel. Esta es otra ventaja de la escritura de programas en lenguajes de alto nivel.
Como un complemento a los brincos condicionales, la arquitectura MIPS cuenta con la
instrucción:
j
Etiqueta
Por medio de la cual se realiza un salto incondicional, de manera que la siguiente
instrucción a ejecutarse es la que se especifica después de la etiqueta.
Ejemplo: Compilando una estructura if-then-else.
Para el siguiente código:
if ( i == j )
f = g + h;
else
f = g – h;
Si nuevamente las se asocian con los registros ($s0 a $s4) ¿Cuál es el código MIPS
compilado?
Respuesta:
El compilador generará una serie de etiquetas en forma automática, de acuerdo al flujo del
programa. Una opción es la siguiente:
De manera que si i == j se continúa con la suma y luego un salto a la etiqueta exit. En caso
de que la igualdad no se cumpla, se hace el salto a la etiqueta else:
bne
add
j
Else: add
Exit:
$s3, $s4, Else
$s0, $s1, $s2
Exit
$s0, $s1, $s2
# Si no son iguales brinca a la etiqueta Else
# Si fueron iguales hace la suma
#
y salta a la etiqueta Exit.
# Si no fueron iguales hace la resta
#
y termina la decisión.
Además de las elecciones entre dos alternativas, con estas instrucciones es posible la
ejecución de ciclos repetitivos.
Ejemplo: Compilando un lazo simple.
Se tiene el lazo en C:
Loop: g = g + A[i]
i = i + j;
if( i != h ) goto Loop;
Suponiendo que las variables g, h i y j se asocian con los registros $s1, $s2, $s3 y $s4,
respectivamente y que el registro base del Arreglo A es $s5 ¿Cuál es el código MIPS
compilado?
Respuesta:
Primero se requiere obtener el valor de A[i] en un registro temporal:
Loop: add
add
add
lw
$t0, $s3, $s3
$t0, $t0, $t0
$t0, $t0, $s5
$t1, 0( $t0)
# $t0 = i + i = 2i
# $t0 = 2i + 2i = 4i
# $t0 contiene la dirección de A[i]
# $t1 = A[i]
Luego se realizan las sumas:
add
add
$s1, $s1, $t1
$s3, $s3, $s4
# g = g + A[i]
#i=i+j
Por último se hace el brinco condicional:
bne
$s3, $s2, Loop
# Si I != j continúa en el lazo.
Las sentencias goto son poco usadas por los entendidos con la programación estructurada,
pero con estas instrucciones es posible la compilación de los ciclos: while y do-while.
Ejemplo: Compilando un ciclo while.
Se tiene el ciclo repetitivo:
while (save[i] == k)
i = i + j;
Si las variables i, j y k se asocian con los registros $s3, $s4 y $s5, respectivamente y que el
registro base del Arreglo save es $s6 ¿Cuál es el código MIPS compilado?
Respuesta:
Para que pueda compararse el valor de save[i], debe obtenerse en un registro temporal:
Loop: add
add
add
lw
$t1, $s3, $s3
$t1, $t1, $t1
$t1, $t1, $s6
$t1, 0( $t1)
# $t1 = i + i = 2i
# $t1 = 2i + 2i = 4i
# $t1 contiene la dirección de save[i]
# $t1 = save[i]
Ahora es posible comparar a save[i] con k, si son diferentes, termina el ciclo:
bne
$t1, $s6, Exit
# si save[i] es diferente de k, termina el ciclo.
Dentro del ciclo se realiza la suma:
add
$s3, $s3, $s4
#i=i+j
El ciclo se repite:
j
Loop
# Salta a la siguiente iteración
Exit:
La prueba de igualdad o desigualdad para un salto es la mas popular, sin embargo algunas
veces es útil evaluar si una variable es menor que otra, por ejemplo en los ciclos repetitivos
for, en los cuales se va incrementando (o decrementando) una variable y se continúa en el
ciclo mientras sea menor que otra (o mayor que 0).
MIPS cuenta con la instrucción slt (set on less than), que compara dos registros y modifica
a un tercero de acuerdo con el resultado de la comparación. Por ejemplo:
slt
$t0, $s1, $s2
Pondrá un 1 en $t0 si $s1 < $s2, en caso contrario, $t0 contendrá 0.
Ejemplo: Prueba de la instrucción slt.
¿Cuál es el código que prueba si una variable a (asociada con $s0) es menor que una
variable b (asociada con $s1) y brinca a la etiqueta less si la condición se mantiene?
Respuesta:
slt
bne
$t0, $s0, $s1
$t0, $zero, less
# $t0 = 1 si a < b y $t0 = 0 en caso contrario
# $t0 no es igual a 0, brinca a la etiqueta less
Notar que se está aprovechando el hecho de que el registro cero contiene el valor 0.
Las estructuras de decisión if-then-else son ampliamente usadas, sin embargo en muchos
programas se tiene diferentes alternativas a seguir después de evaluar una expresión. Para
ello algunos lenguajes manejan estructuras de decisión múltiple, por ejemplo, la estructura
switch-case del lenguaje C (o similares).
Se espera que una estructura de este estilo sea más eficiente que múltiples comparaciones
individuales. Para conseguirlo, los compiladores deben generar una tabla de direcciones de
salto, de manera que se obtenga la dirección destino de la tabla y se realice el salto en
forma inmediata.
Para tales situaciones, la arquitectura MIPS incluye a la instrucción jr (jump register) la
cual realizará un salto incondicional a la dirección contenida en el registro especificado en
la instrucción.
Ejemplo: Compilando una estructura switch-case.
El siguiente código C selecciona entre cuatro alternativas dependiendo si el valor de k es 0,
1, 2 o 3:
switch ( k ) {
case 0:
case 1:
case 2:
case 3:
}
f = i + h; break;
f = g + h; break;
f = g - h; break;
f = i - j; break;
/*
/*
/*
/*
k = 0 */
k = 1 */
k = 2 */
k = 3 */
Suponer que las seis variable f a k corresponden a los registros $s0 al $s5 y que el registro
$t2 contiene 4. ¿Cuál es el correspondiente código MIPS?
Respuesta:
El objetivo es evaluar a la variable k para indexar a la tabla de direcciones, y
posteriormente saltar al valor cargado. Pero primero es necesario asegurarse que k está en
un caso válido:
slt
bne
slt
$t3, $s5, $zero
$t3, $zero, Exit
$t3, $s5, $t2
# Prueba si k < 0
# Si k < 0, termina
# Prueba si k <4
beq
$t3, $zero, Exit
# Si k >= 4, termina
Si el valor de k es válido, para que pueda utilizarse como índice, debe multiplicarse por 4
add
add
$t1, $s5, $s5
$t1, $t1, $t1
# $t1 = k + k = 2k
# $t1 = 2k + 2k = 4k
Supongamos que existen cuatro palabras secuenciales en memoria que inician en la
dirección contenida en $t4 y contienen la dirección correspondiente a las etiquetas L0, L1,
L2 y L3. Para obtener la dirección adecuada para el salto se utilizan las instrucciones:
add
lw
$t1, $t1, $t4
$t0, 0( $t1)
# $t1 = dirección de la tabla_de_saltos[k]
# $t1 = tabla_de_saltos[k]
Un salto a registro desviará el flujo del programa a la opción correspondiente:
jr
$t0
# salto basado en el registro t0.
Las instrucciones que se realizarán en cada caso, de acuerdo con el valor de k son:
L0:
L1:
L2:
L3:
add
j
add
j
sub
j
sub
$s0, $s3, $s4
Exit
$s0, $s1, $s2
Exit
$s0, $s1, $s2
Exit
$s0, $s3, $s4
Exit:
Resumen:
Los operandos en MIPS son:
# k = 0 => f = i + j
# k = 1 => f =g + h
# k = 2 => f = g - h
# k = 3 => f = i – j
# fin del switch-case
Las instrucciones consideradas hasta el momento del repertorio MIPS son:
El lenguaje de máquina para las instrucciones consideradas hasta el momento es:
Tarea 3:
1.- Obtener el código MIPS de la asignación:
x[10] = x[11] + c;
Suponer que c corresponde al registro $t1 y el arreglo x tiene una dirección base de 4000.
2.- Escriba el código máquina generado para el ejercicio anterior.
3.- Con el ensamblador MIPS, indique la secuencia de instrucciones que evalúe a los
registros $s0, $s1 y $s2 y deje el valor del menor en $s3.
4.- Escriba el código máquina generado para el ejercicio 3.
5.- El siguiente código acumula los valores del arreglo A en la variable x:
for ( x = 0, i = 0; i < 10; i++ )
x = x + A[i];
¿Cuál será el código MIPS para este código?
Suponga que el comienzo del arreglo A esta en el registro $s3, que el registro $t1 contiene
10, que la variable x se asocia con $s1 y la variable i con $s2.
6.- Transforme la siguiente asignación:
c = ( a > b ) ? a : b;
a código MIPS. Asocie a, b y c con $s0, $s1 y $s2, respectivamente.