You are on page 1of 18

EL MEJOR AMIGO DE LOS LENGUAJES DE ALTO NIVEL, ¿RISC

o CISC?

Manuel Alejandro Barranco González


Iván Guardia Hernández

Introducción
¿Para qué queremos realmente una computadora? ¿Qué es lo que realmente
importa en su ciclo de vida?.
La respuesta a la primera pregunta es evidente. Un ordenador está diseñado
para poder ejecutar programas, sino ¿para qué lo querríamos?. Evidentemente, existen
sistemas cuyo diseño está orientado a tareas específicas y otros que no. Pero todos
ellos deben dar el mejor soporte posible al software que procesarán.
La respuesta a la segunda pregunta depende del momento histórico en que nos
situemos. En los últimos años el coste del hardware se ha abaratado, mientras que el
del software se ha visto incrementado enormemente. Así pues, lo que realmente
importa en el ciclo de vida de una máquina es el software.
Es muy importante tener en cuenta que la demanda de software provoca la
aparición en el mercado de productos con una calidad en ocasiones más que dudosa
en cuanto a rendimiento se refiere.
Estas respuestas y consideraciones, unidas a que el diseño de aplicaciones está
inherentemente unido a la aparición de errores difíciles de detectar, han hecho que la
industria haya desarrollado lenguajes de alto nivel.
Sin embargo, la utilización de lenguajes de alto nivel provoca la aparición de
un gran problema. Existe un ‘salto semántico’ entre las operaciones propias de la
máquina y las de los lenguajes de alto nivel. Este salto semántico provoca una
ineficiencia en la ejecución y un tamaño excesivo en los programas, así como la
complejidad del diseño de compiladores. De esta manera, la respuesta a la pregunta
de cómo mejorar el software deriva inexorablemente en buscar la forma de poder
optimizar el uso de los lenguajes de alto nivel.
Para ello, lo que realmente importa no es la tecnología de semiconductores,
sino la arquitectura del microprocesador. La tecnología de los circuítos es un
problema más que solucionado. Sin embargo, conseguir una arquitectura óptima no
tiene fácil respuesta.
La arquitectura de un computador viene definida por el repertorio de
instrucciones, el tipo de direccionamientos y el tipo de operandos que es capaz de
ofrecer al compilador. Existen dos alternativas de arquitecturas contrapuestas para
solucionar los problemas derivados del salto semántico.
Por un lado hay toda una familia de microprocesadores conocida como CISC
(complex instruction set computer). Mientras que por otra parte se ha desarrollado
otra clase de microprocesadores conocidos como RISC (reduced instruction
setcomputer). Estas dos formas de arquitectura no sólo se diferencian en la
complejidad del conjunto de instrucciones, sino que poseen filosofías distintas para
solucionar otros problemas referentes al diseño de un computador. Sin embargo,
ambas poseen juegos de instrucciones basadas en registros. Es decir, basan el
almacenamiento interno de los operandos en los registros de la CPU (otras
alternativas consisten en utilizar una pila o un acumulador).
Veamos, pues, las filosofías de estas dos familias de microprocesadores para
solucionar los problemas derivados del salto semántico ¿Cómo hay que actuar para
conseguir programas cortos, rápidos y ayudar a los compiladores para que generen un
código óptimo?

Filosofía CISC
Históricamente la memoria a tenido un elevado coste y en consecuencia se
han ido buscando arquitecturas que maximizasen el uso de la misma. Las
arquitecturas de computadores que aparecieron en primer lugar fueron CISC. Por
tanto, no es de extrañar que una de las mayores preocupaciones de esta familia haya
sido siempre el ahorro de la memoria.
Como hemos comentado, han hecho su aparición los lenguajes de alto nivel,
imponiéndose en la programación al lenguaje más básico del computador, el
assember.
Así pues, CISC es una arquitectura que persigue la programación fácil y al
mismo tiempo un uso eficiente de la memoria.

Ayudar al compilador es ayudar al programador

Resulta evidente que hoy en día sería imposible atender a la demanda de


requerimientos y prestaciones software que hay en el mercado si no existiesen los
lenguajes de alto nivel. Sin embargo, debido al salto semántico entre éstos y el
assember la escritura de los compiladores se vuelve tremendamente complicada.
La escritura sencilla de un compilador depende mucho del repertorio de
instrucciones del que se disponga en cada arquitectura. Para CISC la simplificación
de los compiladores es obvia. Cuantas más instrucciones máquina den soporte directo
a sentencias de lenguajes de alto nivel, más fácil será escribir un buen compilador.
Además, poder ejecutar una instrucción de alto nivel en solamente una de bajo nivel
permite una optimización por hardware, debido a que cada instrucción esta diseñada
para un uso especifico y se puede conocer mejor cuáles son sus características.
Pero no sólo basta con dar soporte hardware a instrucciones de alto nivel para
simplificar la escritura de los compiladores. También es necesario disponer de un
gran número de modos de direccionamiento, ya que de esta manera el compilador
podrá decidir fácilmente cuál utilizar en cada caso.
La memoria se ahorra y los programas son rápidos si son cortos

Otra de las preocupaciones de la filosofía CISC es que la utilización de los


lenguajes de alto nivel tiene, como hemos visto, una influencia negativa sobre el
tamaño de la memoria requerida por las aplicaciones. Para paliar este problema los
compiladores tienen que optimizar el código assembler resultante para una ejecución
eficiente. En base a esto, CISC siempre ha intentado acercar el código máquina a los
lenguajes de alto nivel, evitando así montones de instrucciones equivalentes que
ocuparían más memoria .
Un programa corto posee mayores prestaciones que otro de mayores
dimensiones. En primer lugar porque debemos tener en cuenta que cuanto más rápido
se ejecuten los programas, mayor rendimiento se obtiene del computador. Los
programas cortos son más rápidos porque, además de tener que ejecutar un menor
número de instrucciones, se deben captar menos bytes de la memoria para ejecutarlo.
En segundo lugar, porque la gestión de memoria, sobretodo en un entorno que utilice
paginación y memoria virtual, se ve simplificada por la disminución de los fallos de
página.

La compatibilidad entre generaciones atrae al usuario

Uno de los aspectos más importantes en el mundo de la informática es la


compatibilidad entre distintas versiones de computadores y de software. CISC intenta
buscar flexibilidad de programación en los lenguajes de alto nivel para distintas
plataformas, permitiendo la portabilidad entre diferentes sistemas.
Es interesante de cara al usuario poder disponer de computadores orientados a
la optimización del cálculo en áreas de aplicación diferentes (cálculo científico,
cálculo gráfico..), pero que puedan ejecutar los mismos programas sin necesidad de
tener que recompilarlos. La compatibilidad entre unas versiones y otras ofrece al
usuario seguridad a la hora de invertir su dinero en la adquisición de un sistema
informático.
Por otro lado, es de vital importancia para los diseñadores de computadores
poder depurar, modificar y actualizar las instrucciones que brinda una arquitectura de
la forma más sencilla y fiable posible.
Como veremos más adelante, CISC consigue estos propósitos a partir de la
utilización de la microprogramación.

Arquitectura CISC resultante


Una arquitectura CISC se ve identificada, como hemos visto, con el deseo de
simplificar los compiladores y de aumentar las prestaciones del computador. Pero
CISC no sólo es esto, además hay que resaltar todo un conjunto de características
significativas. Históricamente casi todas las arquitecturas han sido CISC,
influenciadas básicamente por el ahorro de memoria (recordemos que hasta ahora era
carísima), por el afán de hacer super-computadores que fueran capaces de hacerlo
todo por sí solos y por un intento de mantener la compatibilidad de una familia a otra.
Actualmente los computadores CISC mas conocidos son la familia 8086 de
Intel (que se extiende hasta el Pentium), pero históricamente han habido otros como:
IBM 3090, NSC 32016, MC68040, VAX.

Un amplio repertorio de instrucciones y direccionamientos

Como hemos comentado en la explicación de la filosofía CISC, esta familia


de arquitecturas posee un gran repertorio de instrucciones. Su tamaño se debe tanto al
elevado número de operaciones que permite realizar, como al número de
direccionamientos que es capaz de soportar.
Las instrucciones del repertorio se separan del nivel assembler para acercarse
a las sentencias de los lenguajes de alto nivel. Se trata de instrucciones que realizan
operaciones complejas.
El formato de las instrucciones es muy irregular. La longitud de las palabras
varía dependiendo de la instrucción de la que se trate y sobretodo del modo de
direccionamiento que se esté utilizando.
Las instrucciones necesitan de múltiples ciclos de reloj para su ejecución.
Además, no todas precisan del mimo número de ellos, sino que tardan más o menos
dependiendo de la instrucción en concreto. Por ejemplo, se necesitan ciclos de reloj
extra en aquellos casos en los que se necesite información adicional para la ejecución
(como leer algún dato de memoria).
Los modos de direccionamiento varían desde el registro-registro hasta el de
memoria-memoria, incluyendo modos especiales para la indexación en arrays. Como
ejemplo ilustrativo, mostramos un cuadro con las características de los modos de
direccionamiento del VAX.

Modo de direccionamiento Sintaxis Longitud en bytes


Literal #valor 1(bit-6 valor signo)
Inmediato #valor 1 + longitud del inmediato
Registro Rn 1
Registro diferido (Rn) 1
Desplazamiento de Desplazamiento (Rn) 1 + longitud del
byte/palabra/largo desplazamiento
Desplazamiento de @Desplazamiento (Rn) 1 + longitud del
byte/palabra/largo desplazamiento
Escalado (indexado) Modo base [Rx] 1 + longitud del modo de
direccionamiento base
Autoincremento (Rn)+ 1
Autodecremento -(Rn) 1
Autoincremento diferido @(Rn)+ 1
Las instrucciones son microprogramadas

Debido al amplio repertorio de instrucciones y a la complejidad de las mismas


se hace casi impensable el uso de una unidad de control cableada. En lugar de ello el
sistema está microprogramado. Esta característica facilita el diseño del procesador,
ahorrando la infinidad de líneas de control que supondría hacer la unidad de control
cableada y la enorme cantidad de transistores que sería necesario incorporar.
Cada instrucción es ejecutada mediante una serie de microinstrucciones
almacenadas en una memoria, típicamente una ROM, ubicada en el circuíto del
procesador. La captación de instrucciones es unas 10 veces más rápida desde una
ROM que desde la memoria principal y no supone ningún problema acceder
constantemente al microprograma. Por ello, los diseñadores incluyen microcódigo
para tantas instrucciones como les sea posible.
La microprogramación permite diseñar, depurar y modificar fácilmente
instrucciones realmente complicadas. Pero no sólo eso, cambiando simplemente
algunos trozos del microcódigo disponemos de un computador especifico para tareas
de investigación, gráficos, videojuegos...

Los registros son mayoritariamente de uso dedicado


Dentro de un procesador existen dos tipos de registros. Los registros de
propósito general son aquellos que el procesador puede utilizar para almacenar datos
temporales, variables locales... Por su parte, los registros de uso dedicado son
aquellos que están reservados para tareas muy específicas (almacenar el contador de
programa, el stack pointer...).
El número de registros de propósito general es reducido en las arquitecturas
CISC. Por un lado, se debe a que el elevado número de modos de direccionamiento
provoca que casi todo el tránsito de datos se produzca de memoria a memoria. Por
otro lado, la mayor parte del espacio del chip se utiliza para la decodificación y la
ejecución, así como para el almacenamiento del microcódigo, dejando poco espacio
para estos registros. El compilador que se use ha de ser capaz de maximizar el
rendimiento de los pocos registros de propósito general que hay, con el fin de lograr
una ejecución mucho más eficiente del programa.
En cambio, sí son abundantes los registros de uso dedicado que controlan el
tránsito de datos, y el estado del procesador. Algunos de estos registros son utilizados
para almacenar el stack pointer, para realizar la gestión de las interrupciones y para
almacenar los códigos de condición.
Por último, cabe destacar que los computadores basados en arquitecturas
CISC suelen incorporar una memoria intermedia rápida (caché) para agilizar cálculos
y para almacenar datos temporales muy usados.
¿Qué estrategia propone RISC para ayudar a los Lenguajes de
alto nivel?
Mientras se desarrollaban los procesadores CISC, se realizaron estudios
detallados sobre las características de la ejecución de las instrucciones generadas por
los lenguajes de alto nivel. La mayoría de ellos fueron realizados por David A.
Patterson y Tanenbaum. Los aspectos más relevantes de sus estudios se centraron en:
determinar que instrucciones son las que realmente ejecuta la CPU, el tipo de
operandos y su interacción con la memoria y el secuenciamiento de las instrucciones.
Así mismo, es muy importante darse cuenta de que realmente es un compilador el que
genera el código máquina y no el programador. Como consecuencia, la filosofía
RISC está muy encauzada a ayudar al diseño de compiladores optimizadores.

¿Qué instrucciones importan realmente?

Todos los estudios realizados para analizar el comportamiento de los


programas han señalado que el 80% de las operaciones que se realizan en un
programa descrito con un lenguaje de alto nivel utilizan, sólo, alrededor de un 20% de
las operaciones disponibles en el repertorio de instrucciones.
De hecho, las instrucciones que más importancia tienen son las de asignación
y las sentencias condicionales típicas if y loop. Esto implica, por un lado, que el
movimiento de datos es de vital importancia, y por otro, que se debe optimizar el
control del secuenciamiento de instrucciones (comparaciones y saltos) necesarios
para implementar las operaciones condicionales.
Sin embargo, no sólo hay que encontrar las sentencias más utilizadas, sino
aquellas operaciones descritas en lenguajes de alto nivel que provoquen la ejecución
de más instrucciones en lenguaje máquina y, como consecuencia, sean más lentas.
Patterson realizó toda una serie de mediciones sobre del número medio de
instrucciones máquina y referencias a memoria por cada tipo de sentencia en VAX,
PDP-11 y Motorola 68000 (máquinas con arquitecturas CISC). Multiplicando la
frecuencia de aparición de cada sentencia por el número de instrucciones que
necesitaba para ejecutarse obtuvo la siguiente tabla:

Frecuencia dinámica Instrucciones Referencias a


máquina ponderadas memoria ponderadas
Pascal C Pascal C Pascal C
Assign 45 38 13 13 14 15
Loop 5 3 42 32 33 26
Call 15 12 31 33 44 45
If 29 43 11 21 7 13
Goto - 3 - - - -
Otras 6 1 3 1 2 1
Como podemos observar, la operación de llamada a procedimiento y retorno
es la que más tiempo consume. Ya que precisa de muchas instrucciones y muchos
accesos a memoria.

¿Qué tipo de operandos son los más utilizados? ¿Cómo acceder a ellos?

Los estudios de Patterson revelan que la mayoría de las referencias que se dan
en un programa se hacen a variables escalares simples. Además, estas variables están
muy bien localizadas por ser la mayoría de ellas locales al procedimiento que las
referencia. No sólo son locales las variables escalares, sino que las referencias a
estructuras más complejas como arrays y matrices precisan de una referencia previa
a un índice o puntero que también suele ser local al procedimiento.
Otro aspecto relevante que se puede extraer de los estudios de Patterson es
que cada instrucción referencia en promedio 0,5 operandos de memoria y 1,4
registros. Esto nos hace pensar que no es tan necesario disponer de modos de
direccionamiento complejos que permitan operar directamente con posiciones de
memoria, ya que la proporción de referencias a registros es mucho más elevada.
Así pues, es muy importante poder disponer de un acceso y almacenamiento
rápido de los operandos, sobre todo para aquellos que son locales dentro de un
procedimiento. Como veremos más adelante, esta es una de las razones básicas que
motivan que las arquitecturas RISC también sean conocidas como arquitecturas
load/store.

¿Cómo son las llamadas a procedimientos?

Como ya hemos comentado, es importantísimo poder realizar un diseño que


optimice las llamadas y retornos a procedimientos y funciones, debido a la gran
cantidad de tiempo que consumen.
En primer lugar, debemos dilucidar el número de parámetros y de variables
con los que trabaja un procedimiento. Los estudios realizados por Tanenbaum y el
equipo de RISC de Berkeley coincidieron en que la aparición de nuevas palabras en
la llamada a un nuevo procedimiento era muy pequeña y las referencias seguían
siendo, en su mayoría, locales. Así mismo, el número de parámetros que se pasaban
en cada llamada no superaba los seis en un 98% de las veces.
En segundo lugar, se debe tener en cuenta el nivel de anidamiento al que se
suele llegar en una aplicación. Este aspecto es extremadamente importante en los
lenguajes de alto nivel, ya que la mayoría de ellos dan soporte a la programación
estructurada. Según los estudios del grupo de Berkeley, un programa suele
permanecer estable dentro de una amplitud de profundidad de invocación bastante
pequeña. Esto supone un apoyo más a la idea de que las referencias a operandos están
muy localizadas.
A continuación se expone una tabla con los resultados obtenidos por el equipo
de Berkeley sobre un estudio del número de parámetros que se pasan en las llamadas
a procedimientos.

Porcentaje de llamadas a Compilador, intérprete y Pequeños programas no


procedimientos composición de textos numéricos
ejecutadas con
> 3 argumentos 0-7 % 0-5 %
> 5 argumentos 0-3 % 0%
> 8 palabras para 1-20 % 0-6 %
argumentos y datos locales
> 12 palabras para 1-6 % 0-3 %
argumentos y datos
escalares locales

¿Qué es lo que quieren los compiladores?

Como ya hemos comentado, no es el programador quién genera el código


máquina de un programa. Esta tarea se deja a cargo de los compiladores. Una de las
máximas de un diseño RISC es ofrecer una arquitectura que ayude al diseño fácil y
eficiente de los mismos. Puesto que cuanto mejores sean éstos, más eficientes serán
los programas compilados en tamaño y velocidad.
El trabajo del compilador se divide en diversas fases en las que transforma
expresiones de un nivel de abstracción determinado a otro menor. Las decisiones de
optimización se dan en cada una de esas fases sin saber con detalle como será el
código resultante. Así pues, la dificultad en el diseño de los compiladores no reside
en que deben traducir instrucciones complejas, sino que aparece en la traducción
global de programas grandes, complejos y con un gran número de interacciones.

Para ayudar a los diseñadores de compiladores es coherente cuestionarse las


siguientes preguntas: ¿Qué tipo de optimizaciones suele llevar a cabo un compilador?
¿Qué influencia tienen las optimizaciones sobre el rendimiento de los programas?
¿Qué es lo que resulta bueno y malo para los diseñadores de los mismos?

A continuación se exponen cuáles son las optimizaciones más típicas.

- Integración de procedimientos: Como hemos visto, las llamadas a


procedimientos suponen una carga de tiempo elevada. Se debe reducir tal carga
sustituyendo la llamada por el procedimiento en sí.

- Eliminación de subexpresiones comunes: Debe ser posible sustituir dos


instancias de un mismo cálculo por una copia.
- Propagación de constantes: El compilador debe ser capaz de sustituir todas
aquellas variables a las que se les asigna una constante por la constante.

- Reducción de la altura de la pila: Se debe reorganiza el árbol de la expresión


para poder evaluarla de una forma más eficiente.

- Propagación de copia: El compilador debe poder sustituir una variable X por


otra V en todas las instancias en las que exista la asignación X=V.

- Optimización de bucles: El compilador debe reconocer aquellas expresiones


que, aún estando situadas dentro de un bucle, no cambian. En tales casos debería ser
capaz de extraerlas, siempre y cuando no sean susceptibles de ser cambiadas por la
CPU u otros dispositivos

- Gestión de registros: Es muy importante que se reserven registros para


guardar aquellos datos a los que se accede con más frecuencia . De esta manera se
reduce el tráfico a memoria y se consigue mayor velocidad en las operaciones.

- Reducción de complejidad: Se deben poder reemplazar aquellas operaciones


caras en tiempo de ejecución por otras más sencillas que realicen la misma tarea. Por
ejemplo, si accedemos a la posición [a,b] de un array bidimensional de dimensión
[A,B], la posición se calcula como: inicio_del_array+[(b*A)+a]*tamaño_del_dato. Se
podría simplificar esto si, trabajando en la misma fila, se guardase en un registro el
valor (b*A)*tamaño_del_dato, ahorrando tiempo en el acceso.

- Optimización del desplazamiento de saltos: El compilador debe escoger el


desplazamiento de salto más corto que consiga el objetivo.

Para poder apreciar los efectos que las optimizaciones de los compiladores
tienen sobre el rendimiento de los programas, ilustramos el siguiente cuadro.

Optimizaciones realizadas Porcentaje más rápido


Sólo integración de procedimientos 10 %
Sólo optimizaciones locales 5%
Optimizaciones locales + ubicación de 26 %
registros
Optimizaciones globales y locales 14 %
Optimizaciones locales y globales + 63 %
ubicación de registros
Optimizaciones locales y globales + 81 %
integración de procedimientos +
ubicación de registros
Veamos, ahora, cuáles son los requerimientos que todo diseñador de
compiladores espera de la arquitectura de una máquina y en los que RISC basa su
forma de ayuda.

- Regularidad: Las operaciones, los tipos de operandos y los modos de


direccionamiento deberían ser ortogonales (siempre que tenga sentido). Es decir,
deberían ser independientes unos de otros, permitiendo cualquier combinación de
estas tres características del sistema. De este modo, se facilita la optimización del
código por parte del compilador.

- Proporcionar primitivas: Lo importante no es aportar operaciones complejas


a nivel de arquitectura, ya que este tipo de instrucciones son en muchos casos
inutilizables. Por otro lado, acercar demasiado una arquitectura a un determinado
lenguaje de alto nivel impide la generación de código eficiente para otro distinto.

- Simplificar compromisos entre alternativas: Un diseñador de compiladores


debe decidir que secuencia de instrucciones máquina es la más apropiada para cada
fragmento del programa. Pero esta decisión no es nada fácil porque influyen
características relacionadas con la caché y la segmentación. Por tanto, una
arquitectura simple puede ayudar al diseñador de compiladores a tomar compromisos
complejos de optimización con más facilidad.

- Proporcionar instrucciones que interpreten todas las cantidades que se


conocen en tiempo de compilación como constantes: Es muy importante que un buen
compilador interprete en tiempo de compilación todas aquellas expresiones que se
pueden conocer antes de la ejecución.

Estructura RISC resultante


Las arquitecturas RISC no sólo se caracterizan por tener un repertorio de
instrucciones restringido, sino que engloban una gran cantidad de aspectos que la
diferencian de forma drástica de la opción CISC. Algunos computadores basados en
la arquitectura RISC son el RISC I, el Motorola 88000, el MIPS R4000 y el IBM 801.

RISC es una arquitectura de tipo load/store

Como hemos visto, el número de referencias por instrucción en un lenguaje de


alto nivel es elevado y la mayoría de las instrucciones sólo requieren un simple flujo
de datos. Como resultado, la gran mayoría de direccionamientos en las instrucciones
RISC son de tipo registro-registro. Es decir se cargan los operandos en los registros
mediante una operación de tipo load, se realizan las operaciones pertinentes entre los
registros y los resultados se almacenan en memoria mediante una instrucción de tipo
store. A este modelo de arquitectura, exclusiva de los RISC, se le conoce con el
nombre de load/store.

Evidentemente, los registros del procesador son los que brindan los tiempos
de acceso más cortos debido tanto a la tecnología de acceso como al número de bits
necesarios para direccionarlos. En una arquitectura de tipo load/store Lo ideal sería
poder tener todos los operandos que se necesiten en la ejecución de un programa
ubicados en registros. Pero entonces el coste del hardware se vería incrementado
notablemente.
Se necesita una estrategia para ubicar aquellos operandos a los que se accede
con más frecuencia y reducir el tráfico registro-memoria generado en las operaciones
de tipo load y store. Una estrategia consistiría en confiar al compilador la
maximización del uso de los registros. La otra estrategia, más acorde con la filosofía
RISC, es disponer de una cantidad elevada de registros para poder mantener ubicadas
las variables durante un período de tiempo mayor.
Siguiendo la segunda estrategia, cabe considerar que con cada llamada de
procedimiento el aspecto local de las variables cambia y se debe realizar un paso de
parámetros. Si todos los registros fuesen visibles para cada procedimiento, se
deberían salvar en memoria muchos de ellos con cada llamada. Por esta razón se
utiliza un conjunto de numerosos registros de propósito general, al que se llama
fichero de registros.
Se define una ventana o marco de visibilidad para cada procedimiento. Es
decir, cada procedimiento posee asociado un marco de registros del procesador para
almacenar sus operandos. En cada momento sólo es visible una ventana de registros
que se direccionan como si sólo existiesen ellos. Los marcos se dividen en tres
secciones que suelen ser de la misma longitud:

- Registros de parámetros: Estos registros contienen el conjunto de parámetros


que se pasan del procedimiento padre al que está en curso. A través de ellos un
procedimiento hijo se puede comunicar con su padre sin que exista un flujo real de
datos entre ambos.

- Registros locales: Los registros locales sirven para almacenar aquellos


operandos que el compilador haya seleccionado con fines optimizadores, debido
sobre todo al gran número de asignaciones en las que se ven involucrados.

- Registros temporales: Los registros temporales sirven para realizar el paso


de argumentos desde un procedimiento a sus hijos. Del mismo modo que ocurría con
los registros de parámetros, no es necesario un flujo real de datos entre los dos
procedimientos.

Tal como podemos observar, existe solapamiento entre diversas ventanas


siempre y cuando se realicen llamadas y exista paso de argumentos. Sin embargo, el
número de procedimientos activos en un sistema y el nivel de anidamiento son
imprevisibles y es imposible disponer de un número infinito de ventanas. Por ello, el
aspecto real del fichero de registros es el de un buffer circular. Sólo se mantienen
marcos o ventanas para aquellos procedimientos que sean más recientes. Los más
antiguos se han de guardar en memoria para, posteriormente, cargarlos de nuevo en el
fichero de registros.

Por último, es interesante anotar que en muchas ocasiones los computadores


RISC incorporan una pequeña memoria caché dedicada exclusivamente para
instrucciones, consiguiendo así una mejora adicional.

RISC es una arquitectura basada en la sencillez

La arquitectura RISC apuesta por la sencillez del diseño de todas sus


características: tipo de operaciones, modos de direccionamiento y formato de las
instrucciones.
En las arquitecturas RISC no se implementan operaciones complicadas como
se suele hacer en las CISC. Su objetivo es proporcionar operaciones básicas que se
usen con gran frecuencia, dejando de lado otras más sofisticadas pero de un uso tan
restringido que no merece la pena contemplar.
Los modos de direccionamiento son pocos y sencillos. Debido a la gran
utilización de variables locales y a la estrategia de adoptar un fichero de registros, el
modo de direccionamiento más importante es el efectuado a registro. También se
utilizan otros, como el de desplazamiento y el relativo al contador de programa. El
resto de los modos de direccionamiento que posean una mayor complejidad se
pueden obtener a partir de los simples vía software.
El formato de las instrucciones es sencillo y regular. Existen pocos formatos
distintos (3 ó 4 como máximo). El tamaño de las instrucciones es idéntico en todos
ellos (32 ó 64 bits suele ser lo habitual) y están alineadas en los límites de una
palabra. Por último, las posiciones de los campos dentro de una instrucción son fijos,
especialmente el de código de operación.
Debido a la sencillez de las instrucciones, es posible ejecutar una instrucción
en cada ciclo máquina. Un ciclo máquina se define como el tiempo que es necesario
para obtener dos operandos de los registros, realizar una operación en la ALU y
guardar el resultado en un registro del procesador.
La reducción del tiempo necesario en la ejecución tiene varias consecuencias.
La primera de ellas es que es posible la reorganización de la ejecución de las
instrucciones por parte del compilador de una forma sencilla. En segundo lugar, es
factible poder ejecutar varias instrucciones simultáneamente, mediante una técnica
denominada Pipeline. En tercer lugar, no es necesario disponer de microcódigo para
poder ejecutar una instrucción, sino que pueden estar cableadas directamente en la
CPU sin que suponga grandes complicaciones de circuitería. Por último, se produce
una reducción del tiempo invertido en los ciclos de diseño, permitiendo la aplicación
de las tecnologías más vanguardistas en la implementación de los circuítos y la
posibilidad de producir grandes mejoras en las prestaciones de una versión respecto a
la siguiente.

Las arquitecturas RISC simplifican también la estructura del procesador,


reduciendo notablemente el consumo de potencia y la superficie de circuito integrado
necesaria para construirlo. Esta reducción del tamaño permite ubicar una gran
cantidad de registros en el procesador y se aprovecha para implementar otras
funciones tales como: una unidad para el procesamiento en punto flotante, una unidad
de administración de memoria y funciones de control de memoria caché.

Las comparaciones no son siempre odiosas


Ha llegado el momento de comparar ambos tipos de arquitecturas y discutir
cuál es mejor y por qué. Realmente, no existe una opción clara entre RISC y CISC,
por lo que la comparación nos debe ayudar a aprender cuáles son los puntos débiles
de ambas filosofías. Se trata pues de una comparación constructiva.

RISC pone en compromiso los pilares de la filosofía CISC

Repasemos los compromisos que CISC quiere alcanzar y veamos como no


están tan claros los argumentos en los que se apoya.

En primer lugar, debemos recordar que CISC basa la ayuda para la


simplificación de los compiladores en un gran repertorio de instrucciones y en la
aproximación de las sentencias de lenguaje máquina a las de los lenguajes de alto
nivel. Sin embargo, este razonamiento está bastante equivocado. Por un lado, el
hecho de tener un repertorio tan amplio y tan complejo supone un derroche de
esfuerzo. Porque, como hemos visto, la mayoría de las instrucciones que se utilizan
en los programas escritos en lenguajes de alto nivel son sencillas. Por otro lado, las
instrucciones complejas son difíciles de explotar por parte del compilador, porque se
deben buscar los casos concretos en que se ajusten perfectamente a la construcción
del código. Por último, acercar demasiado un repertorio de instrucciones a las
sentencias de un determinado lenguaje de alto nivel supone una perdida de
flexibilidad a la hora de programar con diferentes lenguajes. Como consecuencia, la
filosofía RISC encaminada a simplificar la escritura de los compiladores es más
acertada que la CISC.

Otra gran preocupación de la filosofía CISC es la de conseguir programas


cortos y, como consecuencia, programas que ocupen poca memoria y sean rápidos.
Para ello también se apoya en la idea de disponer de un repertorio de instrucciones
amplio y complejo.
Sin embargo, no es cierto que un programa con menos instrucciones ocupe
menos memoria que otro con mayor número de ellas. La siguiente tabla muestra los
resultados de tres estudios sobre el tamaño ocupado por diversas máquinas RISC y
CISC para un conjunto de programas compilados en C.
11 programas en C 12 programas en C 5 programas en C
RISC I 1,0 1,0 1,0
VAX-11/780 0,8 0,67
M68000 0,9 0,9
Z8002 1,2 1,12
PDP-11/70 0,9 0,71
*Tamaño del código relativo al RISC I

Se debe tener en cuenta que los bits necesarios para codificar una instrucción
típica de CISC es mayor que para una RISC. Esto es debido, en primer lugar, a que
un mayor número de instrucciones en el repertorio implica un mayor número de bits
para codificar el código de operación. En segundo lugar, es debido a que el uso de
modos de direccionamientos a registros, utilizados mayoritariamente por un RISC,
precisan de menor número de bits para realizar la referencia que los
direccionamientos a memoria que suele utilizar CISC.
El hecho de que instrucciones complejas aceleren la ejecución de funciones
complicadas no está tampoco tan claro. Hemos visto que, aunque se disponga en el
repertorio de instrucciones muy complejas, se acaban utilizando las más sencillas.
Pero poder disponer de tantas instrucciones hace que la unidad de control se
complique y que se necesite utilizar microcódigo para ejecutarlas. Por muy rápida que
sea la captación de las microinstrucciones desde una ROM con respecto a la
captación de instrucciones simples desde memoria principal, la fase de decodificación
consume un tiempo mayor en las arquitecturas CISC.

Por último, el compromiso que posee CISC para mantener la compatibilidad


entre unas máquinas y otras no posee ninguna réplica por parte de las arquitecturas
RISC. La facilidad de depuración y modificación del repertorio de instrucciones que
mantiene CISC tampoco se ve cuestionada, salvo en el hecho de que la mejora de
prestaciones de unas versiones a otras es más acentuada en RISC.

¿Una caché o un fichero de registros?

Hemos visto que las arquitecturas RISC poseen un gran número de registros
de uso general o fichero de registros, mientras que las arquitecturas CISC poseen casi
exclusivamente registros de uso dedicado y utilizan una caché para almacenar los
datos más utilizados. ¿Qué opción es mejor?

En un fichero de registros se almacenan casi todas las variables locales de los


procedimientos activos, permitiendo un ahorro de tiempo que no se da con una caché
(pues ésta tendrá que acceder con más frecuencia a memoria principal para traer
nuevos datos). Además, el flujo de datos entre los registros y la memoria es pequeño
porque queda determinado por el nivel de anidamiento que, como hemos visto, suele
ser suficientemente estrecho para que no se produzcan solapamientos entre distintas
ventanas. Con una caché la fluctuación de información con la memoria principal es
mayor porque los datos que ya están almacenados pueden ser sobrescritos por otros.
El acceso a la información ubicada en los registros es notablemente superior a
la que reside en una caché, debido a que el direccionamiento es mucho más simple.
Así, aunque una caché sea tan rápida como los registros en la transferencia de datos,
el tiempo de acceso es notablemente más lento.
Sin embargo, una caché puede almacenar instrucciones y otros tipos de datos,
cosa que no puede hacerse con un fichero de registros. Se puede añadir un conjunto
de registros para almacenar variables globales, pero éstas han de ser descubiertas en
tiempo de compilación (tarea que es extremadamente ardua para un compilador).
En cuanto a la utilización del espacio, el fichero de registros es una opción
claramente más ineficiente. Esto es debido a que no todos los procedimientos
utilizarán al máximo el espacio de ventana que se les asigne. Sin embargo, la caché
lee la información por bloques, desperdiciando muchos de los datos que se transfieren
desde memoria principal.

Seguidamente se ilustran las características de ambas organizaciones en un


cuadro de resumen.

Gran fichero de registros Caché


Todos los datos escalares locales Datos escalares locales usados
recientemente
Variables individuales Bloques de memoria
Variables globales asignadas por el Variables globales usadas recientemente
compilador
Salvaguarda/restauración basadas en la Salvaguarda/restauración basadas en el
profundidad de anidamiento algoritmo de reemplazo
Direccionamiento de registros Direccionamiento de memoria

Conclusión
Las opciones RISC y CISC se deben entender como arquitecturas
complementarias y no como rivales. Los diseños RISC pueden sacar provecho de
características CISC, como por ejemplo añadir una caché dedicada exclusivamente a
instrucciones. Por su parte, los CISC pueden adquirir características RISC, como
implementar sólo aquellas instrucciones que son usadas con mayor frecuencia y dejar
de lado otras tan complejas como inútiles. Ejemplos de arquitectura ‘híbridas’ son el
PowerPC (basado en RISC, pero con características CISC) y el Pentium (basado
mayoritariamente en CISC).

Las arquitecturas RISC son más recomendables para aquellas aplicaciones que
necesiten una importante capacidad de cálculo, mientras que las CISC ofrecen
grandes posibilidades dentro del sector industrial. Sin embargo, las arquitecturas
RISC están desplazando a las CISC en la mayoría de las áreas de aplicación. Esto es
debido a que el deseo por mantener la compatibilidad entre distintas versiones le
privan de una mayor capacidad de innovación. De hecho, la aparición de
arquitecturas CISC que incorporan características RISC está motivada por el deseo de
ofrecer nuevas soluciones.

Finalmente, cabe destacar que lo que realmente importa es la cantidad de


prestaciones que la arquitectura puede brindar a la producción de software. Como las
aplicaciones se escriben en lenguajes de alto nivel, está claro que triunfarán aquellas
arquitecturas con las que mantenga una ‘amistad’ más estrecha. Por el momento,
quien ‘mejor se lleva’ con los lenguajes de alto nivel es, sin lugar a dudas, RISC.
Problemas resueltos
1.- Los compiladores de lenguajes de alto nivel ¿Qué es lo que buscan de una
arquitectura determinada para mejorar el código resultante?

Regularidad: Las operaciones, los tipos de operandos y los modos de


direccionamiento han de ser regulares, es decir, se ha de permitir cualquier
combinación de ellas dentro del código.
Proporcionar primitivas básicas que permitan realizar cualquier
implementación sea cuál sea el lenguaje de alto nivel que se haya utilizado.
Simplificar instrucciones alternativas: Evitar instrucciones semejantes que
hagan difícil la elección entre unas u otras.
Proporcionar instrucciones que puedan interpretar que valores son constantes
en tiempo de compilación.

2.- ¿Qué arquitectura ayuda más a los lenguajes de alto nivel RISC o CISC, y
como pretenden hacerlo principalmente?

RISC propone un modelo sencillo pero eficaz, basado en el uso de pocas


instrucciones muy eficientes y con un nivel alto de utilización.
En cambio CISC quiere abarcar todas posibles instrucciones que simplifiquen
el código (sobre todo en tamaño), y acercar los lenguajes de bajo nivel a los de alto
nivel.
No hay uno mejor que otro, cada uno es más apropiado en determinadas
circunstancias. Aunque sí es cierto que RISC está mejor preparado para soportar
lenguajes de alto nivel.

3.- ¿En qué se basa y qué ventajas tiene una arquitectura load/store?

Una arquitectura load/store realiza la operaciones del siguiente modo: primero


carga todos los operandos mediante un load en unos registros, luego los opera entre
sí, finalmente almacena el resultado en la dirección de destino especificada mediante
un store.
La principal ventaja viene dada por la velocidad de tránsito de datos entre los
registros, que permite realizar todas las operaciones entre registros internos del
procesador. Además, simplifica la labor al compilador, ya que no tiene que precisar
en cada momento que modo de direccionamiento es más conveniente para un
conjunto de operaciones dado.

4.- ¿Cuáles son las ventajas y desventajas del microcódigo?

El microcódigo tiene como principal ventaja su fácil programación a la hora


de implementar las operaciones del procesador, así como su depuración, modificación
y actualizaciones futuras. No requiere hardware adicional para poner nuevas
instrucciones, y tampoco es necesario cablear todas las líneas de control para llevar a
cabo las operaciones.
Por el contrario, supone más accesos a memoria y aumenta el número de
ciclos de reloj por instrucción

Problemas propuestos
1.- ¿Qué tipo de instrucciones son las más usadas, y conviene que el
compilador optimice?

2.- ¿Cómo hay que gestionar los diferentes tipos de operandos para mejorar el
rendimiento del computador?

3.- ¿Cuáles son las optimizaciones típicas de un compilador para obtener un


código resultante eficiente?

4.- ¿Qué conviene más la utilización de un gran fichero de registros internos


del procesador o una memoria cache?