Buscar en este blog

Mostrando entradas con la etiqueta procesadores. Mostrar todas las entradas
Mostrando entradas con la etiqueta procesadores. Mostrar todas las entradas

Tema 2.a: RISC y CISC


Los acrónimos RISC y CISC hacen referencia a dos filosofías diametralmente opuestas en el diseño de microprocesadores y significan respectivamente: Reduced Instruction Set Computer y Complex Instruction Set Computer. Para entender en qué consisten estas dos arquitecturas distintas es necesario definir algunos conceptos.

El microprocesador es el núcleo de un ordenador, se encarga de realizar todas las operaciones que le sean requeridas para ejecutar un programa.

Los programas son conjuntos de instrucciones diseñados de forma que al llevarlas a cabo nos proporcionen unos resultados útiles para un determinado fin.

Los registros son memorias donde se almacenan datos temporalmente.

Para que un procesador pueda ejecutar una instrucción ésta tiene que estar en un lenguaje que pueda entender; lenguaje máquina o lenguaje de bajo nivel.

Cuando un programador crea un código, un conjunto de instrucciones, no lo hace empleando un lenguaje de programación de bajo nivel ya que sería muy engorroso. Lo que hace es usar un lenguaje de alto nivel, más adecuado para la capacidad cognitiva humana, y luego traducirlo a lenguaje máquina mediante un ensamblador. De esta forma se consigue que el procesador entienda las órdenes y las ejecute.

El procesador sólo entenderá un número determinado de instrucciones en lenguaje máquina que lleva almacenadas en su interior, así que las instrucciones del programa deberán ser traducidas a combinaciones de ellas.

Así pues, el número y complejidad de instrucciones que posee un procesador vendrá determinado por la filosofía de diseño que se esté siguiendo.

La filosofía de diseño CISC es anterior en el tiempo a su contrapartida RISC aunque su acrónimo no empezó a emplearse hasta después de la aparición de esta última. En los años 70 se creía que era mejor en términos de rendimiento el que el procesador contara con un gran número de instrucciones complejas que abarcara las operaciones más frecuentes. Es decir, a la hora de realizar una suma de dos números era más deseable que existiera una orden que llevara a cabo todo el proceso en vez de una que accediera a los números almacenados en memoria, otra que los llevara a la unidad aritmética donde eran sumados y otra que guardara el resultado en una posición de memoria.

Los diseños CISC hacían más fácil la labor del programador y permitían que hubiera un menor número de registros en el procesador, sin embargo hacen muy difícil la implementación de técnicas de mejora de rendimiento como las que se usan en la actualidad.

Aún así fueron durante mucho tiempo la filosofía de diseño más usada para los ordenadores personales, como por ejemplo toda la gama Intel X86, conocidos comúnmente como 286, 386 y 486 entre otros.

La técnica de diseño RISC surgió al comprobar que el software que se corría en los procesadores CISC no hacía un buen uso del conjunto de instrucciones del procesador y que se obtenía un mayor rendimiento si se traducían esas instrucciones complejas a varias más sencillas.

Al reducir la complejidad de las instrucciones se reducía también su tamaño y se podía usar ese espacio extra para almacenar los datos con los que se estaba trabajando, reduciendo así muchísimo el tiempo de acceso a memoria.

Con esta filosofía se dificultaba la labor del programador pero a cambio se abría la posibilidad de utilizar las técnicas de segmentación para mejorar drásticamente el rendimiento del procesador.

Un ejemplo de filosofía RISC son los procesadores PowerPC, presentes en los ordenadores Macintosh y en consolas de última generación.

Tema 2.b: Explicación en detalle de las técnicas de resolución de riesgos en procesadores segmentados utilizando como ejemplo la arquitectura MIPS.


En este artículo vamos a usar como ejemplo el procesador MIPS64 que tiene como características el ser un procesador de tipo RISC (ver artículo anterior) con cauce segmentado de 5 etapas por instrucción de 1 ciclo de duración cada una, 64 registros de 64 bits cada uno, memoria cache tipo Harvard y 66 instrucciones simples de longitud fija de 32 bits. Pero vayamos poco a poco antes de que alguien se asuste.

Ahora que ya sabemos lo que es un procesador tipo RISC (Reduced Instruction Set Computer) lo siguiente es comentar en qué consiste segmentar el cauce de ejecución de las instrucciones de un procesador. La segmentación (o pipelining) surge de la necesidad constante de aumentar la velocidad de proceso de las computadoras, para ello nos vienen rápidamente a la mente tres formas de hacerlo: mejorar el hardware para que sea más rápido, explotar la concurrencia poniendo varios procesadores a trabajar en la misma tarea o hacer lo mismo intentando que un procesador pueda hacer varias tareas a la vez. Todas ellas tienen sus ventajas y sus inconvenientes que se suelen traducir en costes de fabricación y/o de programación.

La primera solución es probablemente la más cara y está limitada por el estado actual del arte, la segunda hoy en día es muy común pero antes su precio era prohibitivo y además se puede usar junto con la tercera. Evidentemente la tercera es la segmentación.

Se suele recurrir al ejemplo de la lavadora para explicar cómo funciona, pero se pueden encontrar multitud de ejemplos hoy en día que lo ilustran de igual manera. Hoy vamos a usar como modelo la fabricación de un automóvil, en honor a Henry Ford que fue uno de los primeros en aplicar las cadenas de montaje.

Fabricar un automóvil era tanto en 1913 como hoy una tarea compleja, que requería un grupo experto de mecánicos trabajando muchas horas. La idea básica era dividir esa tarea compleja es varias más pequeñas y más sencillas, de manera que las pudieran realizar operarios menos cualificados en menos tiempo. Vamos a suponer, de forma totalmente falsa, que Henry Ford dividió su línea de producción en 5 pasos “sencillos”:

  1. Recoger las piezas del almacén.
  2. Leer las especificaciones del cliente y configurar las máquinas.
  3. Ensamblar todas las piezas en el chasis.
  4. Enviar el coche al concesionario para su venta.
  5. Escribir los datos de venta en los libros de cuentas.

Si tuviéramos una persona que sabe hacer todas las tareas y tarda 1 día en cada una, podría construir el coche él sólo en 5 días, si contáramos con 5 de estas personas y empezaran a trabajar el mismo día, venderían 5 coches cada 5 días pero necesitaríamos multiplicar por 5 los recursos de la fábrica.

Lo siguiente es tener a una persona formada en cada operación, de forma que tenemos un transportista de piezas, un operario de las máquinas, un montador, un transportista de vehículos terminados y un contable. Seguimos teniendo 5 operarios pero si les ponemos a trabajar sobre el mismo coche sólo venderemos 1 cada 5 días y siempre habrá 4 esperando a que se les dé trabajo.

Ahora bien, si encadenamos las operaciones de manera que tengamos varios coches en fabricación a la vez podemos llegar a lo siguiente:


Con este esquema llegamos a vender 1 coche en 5 días (como al principio), pero a partir del día 5 vendemos 1 coche al día, teniendo una producción igual a la del ejemplo de 5 operarios expertos en todas las tareas pero con unos costes mucho menores y vendiendo un coche diario en vez de tener que esperar 5 días para vender 5 coches.
Si ahora dividiéramos cada tarea en dos subtareas tendríamos que contratar 5 operarios más pero venderíamos dos coches al día, etcétera, etcétera.

Como puede verse la segmentación no reduce el tiempo de proceso sino que aumenta el rendimiento, racionaliza los recursos y disminuye los costes. Este aumento del rendimiento depende directamente del número de etapas de que se componga nuestro proceso.

Ahora traduciremos este ejemplo a la arquitectura MIPS64, para ello las fases por las que pasa una instrucción son:

  1. Instruction Fetch (IF).
  2. Instruction Decode (ID).
  3. Execution (E).
  4. Memory Access (M).
  5. Write Back (WB).

Ahora se entiende lo rebuscado de las operaciones de la analogía con los automóviles; el objetivo era que fueran similares a las etapas del procesador.
Las 3 primeras son fáciles de entender y corresponden con la carga de la instrucción a ejecutar, la segunda con la decodificación de la instrucción y lectura de los registros de los operandos, y la tercera es donde se hacen los cálculos. Para las otras dos etapas es conveniente explicar muy por encima la jerarquía de memoria.

Cuanto más cerca está la memoria del procesador, más rápida debe ser y menos tamaño puede tener. Conforme nos vamos alejando del procesador tenemos: registros, cache, RAM y disco duro. Las dos primeras están alojadas directamente en el procesador, la RAM está en la placa base y el disco duro se engancha a la misma.
En el MIPS64 tenemos 64 registros de 64 bits (32 de propósito general y 32 para operaciones en coma flotante) y una caché de primer nivel (puede haber más niveles…) de 64 kilobytes para instrucciones y otros 64 para datos.

Por tanto la etapa de Memory Access representa una lectura o escritura en la memoria RAM y la de Write Back representa la escritura en los registros de los resultados de las operaciones.

Las ventajas de la segmentación son muchas pero como todo en la vida tienen un precio o unos inconvenientes. Los problemas que hacen que la segmentación no funcione al mismo rendimiento que en la teoría se llaman riesgos y puede haberlos de tres tipos: riesgos de datos, de control y estructurales.

Riesgos Estructurales: en este tipo de riesgo uno de los problemas es que no todas las instrucciones van a requerir hacer uso de todas las etapas pero pueden tener que esperar a que alguno de los recursos quede libre, tardando al final lo mismo que la instrucción más larga. Así mismo no todas las operaciones elementales tienen la misma duración, por ejemplo una suma se realiza en menos tiempo que una división.

Además, si queremos armonizar las etapas de manera que todas avancen a la vez, sincronizadas con el reloj del procesador, tenemos que hacer que todas las etapas duren lo mismo. Esta es la solución ideal y, por definición, inalcanzable por lo que en la realidad diseñaremos el procesador de manera que en un ciclo le dé tiempo a terminar a la etapa más lenta. En el resto de etapas estaremos “perdiendo” tiempo desde que se termine hasta que se pase a la siguiente.

Otra de las facetas de los riesgos estructurales es cuando dos instrucciones distintas tienen que hacer uso del mismo recurso:


Para resolver este problema se pueden introducir paradas (“no op”: etapas que no realizan ningún trabajo) que sincronizan el cauce a costa de hacerlo menos productivo. Otra opción, más cara, es replicar los recursos que más van a utilizarse. Una tercera opción es dividir las etapas en dos subetapas, en una de ellas se leen datos y en la otra se escriben de manera que en la misma etapa se pueden hacer operaciones concurrentes con el mismo registro.

Riesgos de Datos: este tipo de riesgos se producen cuando entre las instrucciones existen dependencias de datos (tienen operandos comunes). Hay tres tipos de dependencias de datos:

-          Verdadera dependencia o RAW (Read After Write).
-          Antidependencia o WAR (Write After Read).
-          Dependencia de salida o WAW (Write After Write).

En MIPS no hay riesgos WAR ya que se produce la emisión de instrucciones en orden, de manera que se leen los registros antes de que se puedan modificar por otra orden posterior.

Los riesgos WAW pueden entenderse con el siguiente esquema:


En la imagen siguiente puede verse un caso de riesgo RAW; la instrucción de multiplicación requiere un dato guardado en el registro 1 que es el resultado de una instrucción suma que aún no ha terminado.


Hay tres formas de solucionar esto: introduciendo paradas que sincronicen el código, reordenando las instrucciones para evitar esas dependencias o implementar la técnica del “data forwarding”.

La primera de estas técnicas ya se ha visto para riesgos estructurales, mientras que la segunda también se puede aplicar para estos y consiste en introducir otras instrucciones que no dependen de los registros en conflicto hasta que se resuelva la parte WB de la primera instrucción. Esta técnica forma parte de la llamada “planificación dinámica”.

El “data forwarding” o anticipación consiste en dotar al hardware de una manera de redirigir el resultado de la etapa E de la primera instrucción en conflicto, directamente a la entrada de la etapa  E de la otra instrucción en conflicto. Esto evita que se detenga el cauce pero hace que la etapa ID de la segunda instrucción lea valores incorrectos.


Riesgos de Control: este tipo de datos se dan cuando en el cauce de instrucciones hay “saltos” fruto de sentencias condicionales tipo if. Normalmente el registro contador de programa (PC) se va actualizando con el valor de la siguiente instrucción a ejecutar pero cuando tenemos sentencias condicionales tenemos que esperar hasta que se resuelva la comparación para saber qué instrucción va a ser la siguiente en ejecutarse.
Como siempre, podemos poner paradas en el cauce hasta que se resuelva la indeterminación pero ya sabemos que con eso estamos echando por tierra las ventajas de la segmentación.

Otras soluciones pasan por predecir el salto o retardar la decisión (bifurcación retardada). Además de esto, existe una mejora del cauce MIPS que intenta minimizar el efecto de estos riesgos adelantando el cálculo de la dirección de salto a la etapa ID, de esta manera sólo se retarda un ciclo la ejecución de la siguiente instrucción.

En la bifurcación retardada seguimos ejecutando las siguientes instrucciones que hay en cola hasta que se resuelva el salto, siempre y cuando estas instrucciones se deban ejecutar siempre independientemente de que se tome o no el salto.

En la predicción de saltos lo que se hace es asumir que siempre se va a tomar el salto (o lo contrario) y actuar en consecuencia mientras no se sepa el resultado. Una vez que se sabe el resultado del salto se sigue con la ejecución ya planeada o se desechan las instrucciones que se habían alimentado. Analizando los costes de las dos opciones (siempre sí o siempre no) se llega a la conclusión de que en MIPS lo que más cuenta trae es actuar siempre como si nunca se fuera a tomar el salto. Esto se puede apreciar en el esquema siguiente.


 Bibliografía:

-          Patterson and Hennessy: Computer Organization and Design. The Hardware/Software Interface. Morgan Kaufmann Publishers. ISBN 1-55860-604-1
-          Apuntes de Arquitectura de Computadores y AGM de varias facultades de Informática

Tema 2.d: Explicación en detalle de los predictores de salto multinivel y adaptativos


En este artículo exploraremos con un poco más de profundidad algunas de las técnicas de resolución de riesgos de control, de los que ya hablamos en un artículo anterior.
Los riesgos de control son aún más frecuentes que los de datos, se dan en una de cada cinco instrucciones habitualmente, y tienen un impacto muy negativo sobre el cauce de ejecución de las instrucciones, penalizando incluso las soluciones tomadas para resolver los otros tipos de riesgo.

Nos centraremos en los predictores de salto multinivel y adaptativos.

Los predictores multinivel se denominan así porque pueden tener en cuenta información tanto local (del salto que se está intentando predecir) como global (de los demás saltos del programa). Estarán compuestos entonces de predictores locales y predictores globales y seguirán una notación como la siguiente (G, L) en la que la letra G indica el número de saltos globales que se tienen en cuenta y la L el número de bits del predictor local.

De los predictores locales hay dos tipos que son los que se usan con más frecuencia: los de 1 y 2 bits que en la notación vista se denominarían (0,1) y (0,2) respectivamente.

El de 1 bit sigue el esquema siguiente: tiene valor 1 si la última vez se tomó el salto y 0 si no fue así. Si la predicción falla se cambia su valor. Es un planteamiento muy sencillo y con una utilidad limitada ya que para ciclos repetitivos se suele fallar dos veces.

Con dos bits el esquema se vuelve un poco más complejo y nos protegemos de las excepciones en bucles condicionales repetitivos, se puede observar mejor su comportamiento en la siguiente figura:


Pueden implementarse predictores de más de dos bits pero la práctica ha demostrado que no merece la pena.

El uso de los predictores multinivel se basa en que se puede predecir el resultado del salto de una instrucción en función de los resultados de instrucciones de salto anteriores.

Los predictores adaptativos (o tournament predictors) tienen la capacidad de escoger un predictor local o un predictor global según cuál se vaya a comportar mejor para un determinado salto.

Si el buffer de predicción predice que el salto no se toma, no hay ningún retraso puesto que al ciclo siguiente se busca la instrucción siguiente. Pero si se predice que el salto se va a tomar, no se puede buscar la instrucción destino de salto hasta que no se haya calculado la dirección de esta instrucción. Y una vez que se conozca esta instrucción ya se conoce si el salto se toma realmente o no, así que no tiene sentido continuar utilizando la predicción.

La solución está en implementar algún mecanismo que permita predecir también la dirección destino de salto. Este mecanismo es el buffer de predicción de destino de salto llamado BTB (Branch Target Buffer).

El uso del BTB resulta especialmente importante en arquitecturas como la MIPS en la que conocemos a la vez el resultado de evaluar la condición de salto y la dirección de destino del mismo. El BTB es en esencia una caché que almacena la dirección destino que tuvo cada salto la última vez que se tomó. El acceso al BTB se hace durante la etapa IF de las instrucciones: al mismo tiempo que se busca la instrucción en la memoria de instrucciones con su dirección se accede al BTB para saber si se trata de una instrucción de salto.

Otra forma de predicción de saltos adaptativa y que resulta especialmente útil para aquellos saltos que se evalúan en tiempo de ejecución (saltos indirectos) es la pila de direcciones. La razón por la que no se puede usar el BTB con la misma fiabilidad para este tipo de saltos radica en que la dirección de destino del salto va variando en cada ejecución de la instrucción de retorno.

Esta pila funciona de manera que se almacenan las direcciones desde las que se llama a los procedimientos. Si la pila es lo suficientemente profunda, cuando se realicen los retornos se irán recuperando las direcciones adecuadas de la pila y se cumplirán las predicciones con un elevado porcentaje de acierto.

En el gráfico siguiente se puede ver el rendimiento de esta técnica en distintos procesadores.