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

miércoles, 6 de enero de 2010

Itanium

Los procesadores Itanium fueron desarrollados por Intel y HP entre los años 1989 y 2000. El lanzamiento se produjo finalmente en el año 2001 y en 2002 se lanzó al mercado Itanium 2 que corregía la jerarquía de memoria del primer diseño, que se mostró ineficiente, además de añadir más unidades funcionales. Esta versión ha tenido revisiones en las que se ha mejorado el proceso de fabricación, aumentado el tamaño de la memoria caché, añadido núcleos y algunas características como soporte para virtualización o multithreading.

El objetivo del diseño del itanium era conseguir una arquitectura que maximice el número de instrucciones por ciclo. A esta arquitectura se le conoce con el nombre EPIC (Explicitly Parallel Instruction Computing). La base de este diseño es utilizar instrucciones muy largas (128 bits) que en realidad contienen 3 instrucciones de 41 bits que pueden ejecutarse simultáneamente. Esta agrupación la realiza el compilador, que es el que tiene toda la responsabilidad en lugar de la lógica del procesador como sucede en los procesadores x86 más comunes, que pueden ejecutar fuera de orden.

Este es un esquema de bloques del procesador Itanium original:

Como hemos dicho, el objetivo del diseño es ejecutar el mayor número posible de instrucciones en paralelo. Por ello tiene un gran número de unidades funcionales en 11 grupos diferentes:
  • 6 ALUs de propósito general
  • 2 unidades de enteros
  • 1 unidad de desplazamiento de bits
  • 4 unidades de acceso a caché
  • 6 unidades de cálculo multimedia
  • 2 unidades de desplazamiento de bits en paralelo
  • 1 unidad de multiplicación en paralelo
  • 1 unidad de "population count" (número de bits a 1 en un registro)
  • 2 unidades de coma flotante de 82 bits
  • 2 unidades de coma flotante SIMD
  • 3 unidades de cómputo de saltos
Cada grupo de unidades puede ejecutar un subconjunto de instrucciones, dependiendo del tipo que sean. Las instrucciones más comunes pueden ser ejecutadas por más de 1 tipo de unidades. Cada instrucción pertenece a uno de los siguientes tipos:
  • A: números enteros (ALU)
  • I: números enteros (no ALU)
  • M: acceso a memoria
  • F: Punto flotante
  • B: Salto
  • L+X: Tipo extendido, dependiendo del subtipo se ejecuta en distintas unidades.
Por ejemplo las instrucciones de tipo A pueden ejecutarse en las unidades de enteros, en las ALUs de propósito general y en las de cálculo multimedia.

El procesador tiene una gran cantidad de unidades disponibles y reparte el trabajo según el tipo de instrucción entre ellas, mediante estaciones de reserva siguiendo la estrategia del algoritmo de Tomasulo. Si nos fijamos, un conjunto de 3 instrucciones ocupa 41*3=123 bits. Los 5 bits que faltan hasta 128 bits indican al procesador que combinación de tipos de instrucciones hay. Por ejemplo, 00001 indica que la primera instrucción es de tipo M, mientras que la 2ª y 3ª son de tipo I. Además de una gran cantidad de unidades funcionales, también tiene una gran cantidad de registros. Hay 128 registros de proposito general de 64 bits, 128 registros de coma flotante de 82 bits y 64 registros de predicación de 1 bit.

Para mantener las unidades funcionales activas, el procesador implementa varias técnicas conocidas. En primer lugar, emite dos instrucciones por ciclo de reloj. Teniendo en cuenta que cada una es un paquete con 3 instrucciones, son 6 las instrucciones efectivas que puede llegar a emitir. Otra técnica de la que hace uso este procesador es la predicción de saltos. El procesador tiene 2 predictores, uno de ellos es un buffer de predicción de salto, mientras que el 2º es un predictor de 2 niveles. También implementa un predictor de dirección de retorno para las llamadas a funciones. Lo más distintivo de este procesador es que el compilador puede añadir sugerencias sobre los saltos, haciendo que el procesador lo evalúe de manera dinámica (utilizando los predictores) o de manera estática (haciendo caso a la recomendación del compilador).

El procesador también tiene la capacidad de especular y también implementa una técnica que no es común en los procesadores actuales, que es la predicación. Al usar esta técnica, el procesador ejecuta las 2 ramas de un salto y descarta la que no se toma una vez conocido el resultado de la instrucción de salto. Esta técnica encaja con la gran cantidad de registros y unidades funcionales del procesador. Esto es especialmente práctico en el caso de saltos condicionales que ejecutan pocas instrucciones. El compilador puede elegir qué debe realizar el procesador. Para ello, cada instrucción tiene 6 bits reservados para indicar que registro de predicación indica si la instrucción se debe ejecutar o no. Hay un registro cuyo valor es siempre verdadero, por lo que se puede usar para ejecutar instrucciones sin predicción. El funcionamiento de esta técnica se puede ver en este esquema:

En el caso de usar predicación, se evalúa que r1 valga 5 y el resultado se asigna a los registros de predicación qp1 y la negación a qp2. A continuación se ejecutan ambas ramas, solo que las operaciones cada una están asociadas un registro de predicación. El resultado de las ramas se utilizará o se desechará según el resultado final de la instrucción test. Otra vez más, es el compilador el que tiene la responsabilidad de utilizar la predicación adecuadamente.

La jerarquía de memoria está dividida en 3 niveles. La memoria de primer nivel está separada en 2, una para instrucciones y otra para datos. Ambas son asociativas por conjuntos (4) y tienen un tamaño de 16 KB cada una. La memoria de nivel 2 ha sufrido varios cambios. Inicialmente era una memoria asociativa por conjuntos (6) con un tamaño de 96 KB, compartida para código y datos. Con los procesadores Itanium II aumentaron el tamaño hasta 256 KB y la asociatividad a 8. Desde mediados del 2006 la memoria caché de nivel 2 está separada en una caché de datos de 256 KB y una de instrucciones de 1 MB. La memoria caché de nivel 3 se accedía a través del FSB en el caso de los primeros Itanium y consistía de una caché asociativa por conjuntos (2) de 2 ó 4 MB. Al revisar la jerarquía de memoria con los Itanium II se incluyó memoria caché asociativa por grupos (2 ó 4) de nivel 3 en el mismo integrado. El tamaño varía según el modelo desde 1.5 MB hasta 24 MB en los últimos modelos con 2 núcleos (12 por núcleo).


Este tipo de procesadores está orientado a la computación de alto rendimiento. La arquitectura Itanium consigue un rendimiento altísimo en cálculo en coma flotante y de hecho ha sido muy utilizada en supercomputadores.

Patxi Astiz

martes, 10 de noviembre de 2009

Core i7

Los procesadores Core i7 están fabricados con la última arquitectura de Intel. Esta es una evolución de la de los procesadores Core 2. Se han mejorado varios puntos de dicha arquitectura, se ha añadido multithreading y varios elementos al procesador que veremos al final de esta entrada. Primero vamos a concentrarnos en el núcleo de la arquitectura.

Empezaremos por el front buffer, encargado de suministrar las instrucciones a las unidades de ejecución. Es la parte que corresponde a las fases IF (instruction fetch) e ID (instruction decode) de un pipeline sencillo. Veamos un esquema.



Como se puede ver, el procesador tiene una caché de instrucciones de 32 KB de la que se leen para pasar a decodificarlas. Esta caché está conectada a otra de nivel 2, de 256 KB. La decodificación y traducción desde instrucciones x86 hasta microOps, que son las que realmente maneja el procesador internamente se realiza en 2 pasos. El procesador al tener varios decodificadres, uno de ellos complejo y poder fusionar instrucciones, tiene un máximo teórico de 5 instrucciones x86 emitidas por ciclo. En este proceso de decodificación también se realiza la predicción de saltos y la detección de bucles. Aunque Intel no ha revelado qué tipo de predictor de saltos utiliza, se sabe que hay al menos 2 predictores de 2 niveles. También hay un buffer con direcciones de retorno de llamadas a procedimientos. De esta manera se gestionan más rápidamente estos saltos indirectos. Como novedad en esta arquitectura, la detección de bucles se realiza después de terminar la decodificación. De esta manera las instrucciones de un bucle ya están decodificadas y se libera de trabajo a los elementos previos del front-end, incluida la caché de instrucciones. Por último, se reservan registros y se leen los valores del buffer de reordenación que sean necesarios antes enviar una instrucción a la fase de ejecución, que vemos en el siguiente diagrama.

Como se muestra en el diagrama, solo hay una estación de reserva unificada de 128 entradas, que se encarga de suministrar instrucciones a las 6 unidades funcionales siempre que sea posible. De estas unidades, 3 hacen operaciones load/store y otras 3 calculos aritméticos y lógicos. Los resultados se llevan al buffer de reordenación que hemos visto anteriormente. Los resultados de las operaciones load/store además, no acceden directamente a la caché de datos de 32 KB que tiene el procesador, sino que pasan por un buffer intermedio, para evitar los riesgos de datos al leer y escribir en instrucciones diferentes que han podido ser ejecutadas fuera de orden. Desde este buffer pasa a la caché.

De manera resumida hemos visto el funcionamiento de la base de un procesador Core i7, y como en general sigue un diseño típico en los procesadores de propósito general de hoy en día, utilizando las técnicas de optimización comunes, como predicción de saltos, especulación o multithreading. Sin embargo lo que hemos visto corresponde solo a un nucleo del procesador. El diseño del Core i7 se ha hecho de manera modular, por lo que un procesador con esta arquitectura puede tener 2 o más nucleos como el descrito. Todos estos núcleos comparten otros elementos que vamos a ver a continuación:

En este ejemplo, hay 4 núcleos. Cada uno de ellos tiene los elementos que se han descrito anteriormente. Los 4 sin embargo comparten el resto de elementos, entre ellos la caché de nivel 3, cuyo tamaño es 8 MB.

Mientras que los núcleos de un Core i7 son en gran medida iguales a los de un Core 2 aunque con más recursos (más entradas en la estación de reserva, mayor buffer de reordenación, etc...) y algunas mejoras, en el diseño de esta arquitectura hay cambios muy importantes. Especialmente la inclusión del un controlador de memoria DDR3 de 3 canales en el chip y el uso de un bus (llamado QPI) de interconexión entre procesadores (para entornos multiprocesador) y entre el procesador y el hub de la placa base. De esta manera se evita el clásico FSB (front side bus) que en caso de haber más de un procesador, se compartía para acceder a la memoria así como para comprobar la coherencia de las cachés de cada procesador. La arquitectura Core i7 pasa a ser NUMA (Non-Uniform Memory Access). Este diseño es prácticamente el mismo que introdujo AMD hace varios años en sus procesadores Opteron y posteriores con buenos resultados.

En este diagrama podemos ver la configuración de una máquina con dos procesadores Core 2 y a la derecha la configuración NUMA equivalente con 2 procesadores Core i7.

Mientras que antes los procesadores tenían que repartirse el bus de salida para llegar a cualquier recurso que estuviera fuera del propio procesador, con la arquitectura Core i7, un procesador tiene buses diferentes para acceder al parte de la memoria, al hub de la placa base (y por lo tanto a periféricos como la tarjeta gráfica) y al otro procesador (y a la memoria que este controla), por lo que no habrá conflictos por compartir un mismo bus con el otro procesador. Otra de las ventajas de este diseño es que los accesos a memoria tienen menos latencia, al estar conectada directamente al controlador de memoria del procesador. Incluso en los casos en los que un dato está en los bancos de memoria del otro procesador, la latencia, aunque peor que si la memoria estuviera conectada directamente al procesador que pide el acceso, mejora respecto a las arquitecturas anteriores.

Como hemos visto esta arquitectura no presenta grandes cambios en lo que es el diseño del núcleo del procesador, pero sí supone un avance importante en la forma de conectarse con la memoria y otros elementos externos al procesador y que muchas veces puede ser el cuello de botella de un sistema.

Patxi Astiz

domingo, 8 de noviembre de 2009

Radeon HD5800

A modo de ejemplo vamos a estudiar la arquitectura de una GPU actual. Mas en concreto se trata de la arquitectura de la serie HD5800 de AMD. En primer lugar vamos a ver un esquema general.

Como se puede ver, se trata de una arquitectura unificada, es decir no hay hardware especializado en procesar vertices y hardware separado para procesar fragmentos. Otro aspecto importante es que la mayor parte del diseño lo ocupan unidades de ejecución que están replicadas. En la parte superior del esquema se puede ver lo que han llamado "Command Processor" y "Graphic Engine". Estas estructuras hardware se dedican a recibir las instrucciones que llegan del procesador a través del bus y crear los hilos que sean necesarios, de forma similar a la traducción y emisión de instrucciones de una CPU. Estos hilos se envían a las unidades de ejecución a través del "Ultra-Threaded Dispatch Processor", de manera similar a lo que hace una estación de reserva.

Como se puede ver, las unidades de ejecución están replicadas. En cada una de las 20 unidades que muestra el diagrama (llamadas "SIMD engine"), hay 16 subunidades ("thread procesors"). A su vez cada una de estas tiene 5 elementos que son las que realmente realizan el procesado y se muestran en la siguiente figura. Esto significa que hay un total de 1600 unidades de proceso


Como puede verse, se trata de pequeños procesadores especializados en operaciones en coma flotante y que dependiendo del tamaño de los datos pueden realizar hasta 4 sumas y 4 productos por ciclo. De esta manera se pueden realizar miles de operaciones por ciclo en paralelo, llegando a una capacidad de 2 TFLOPS según el fabricante. También hay una "branch unit" para calcular el destino de un posible salto lo antes posible.

Aunque en las imágenes anteriores se vean los elementos de la jerarquía de memoria, es la siguiente imagen se aprecia mejor.


Hay varios registros compartidos entre cada uno de las unidades ("stream core") del último diagrama. Cada "SIMD engine" tiene tanto 8KB de caché de nivel 1 como 32 KB de memoria local de acceso rápido. Para acceder a dicha caché hay una unidad de texturas para cada 4 "thread processors", es decir 4 unidades de texturas por cada "SIMD engine". Además de esta caché de nivel 1, existen 4 cachés de nivel 2, que están asociadas a un controlador de memoria cada una, que en este diagrama no se han representado. También existen cachés de constantes y de instrucciones para acelerar el funcionamiento del "Ultra-Threaded Dispatch Processor". Por último, vamos a ver lo que en este último diagrama aparece como "memory controller" en la siguiente imagen.

En este diagrama se aprecia como existe un hub que interconecta las 4 controladoras de memoria (cada una con su caché de nivel 2) con el bus de interconexión PCI Express, la salida digital de video y otro elementos a los que se envían datos o desde los que se leen.

A modo de resumen se podría decir que la serie Radeon HD5800 sigue la norma de todas las arquitecturas de procesadores gráficos del momento. El diseño no es especialemente complicado y su base es la replicación de recursos y el paralelismo masivo. También es importante ver que la memoria sí que es cada vez más compleja, ya que es necesario mover una gran cantidad de datos para poder alimentar a todas las unidades de ejecución que existen.

Patxi Astiz

Multithreading en GPUs

El multithreading en los procesadores de proposito general es una técnica muy común. Para aprovechar al máximo los recursos disponibles, en colaboración con el sistema operativo, se pueden procesar intrucciones de 2 o más hilos simultaneamente, aprovechando así el paralelismo a nivel de hilo.

En las tareas para las que se diseñan los procesadores gráficos, el paralelismo es fácilmente aprovechable. Hay cálculos que deben realizarse para cada vértice o para cada fragmento, lo que significa repetir la misma tarea una y otra vez sobre diferentes datos en memoria, por lo que el paralelismo y la idea de multithreading es básica en el diseño de GPUs actuales. Veamos un esquema de la arquitectura de un procesador gráfico moderno.


Como se puede ver, la arquitectura del ejemplo hay unas pocas estructuras hardware que están replicadas varias veces. Esto es una señal de que el diseño está hecho para aprovechar el paralelismo. Cada uno de los cuadrados verdes está representando un pequeño procesador, capaz de realizar operaciones encoma flotante. Las estructuras de control crean un hilo para cada operación que ha de realizase, ya esta esta relacionada con vertices, geometría o fragmentos. Estos hilos se asignan a una de las unidades de ejecución, que realiza la tarea y termina. Estos hilos, aunque comparten nombre con los que usamos en los sistemas operativos y CPUs actuales. La idea es la misma, es decir ejecutar en paralelo distintas partes de código compartiendo la memoria. Sin embargo, no tienen mecanismos como semaforos, o mutexes. Son mucho más simples y no requieren una gestión complicada.

La idea de hilos en una GPU se puede representar con el siguiente ejemplo. Imaginemos que queremos sumar las matrices A y B, de tamaño 1000x1000. Si pensamos en un solo hilo, el algoritmo sería algo parecido a lo siguiente:

for (i = 0; i<1000; i++)
    for (j=0; j<1000; j++)
         C[i,j]=A[i,j]+B[i,j]

Si quisieramos ejecutar esto en una GPU, la estrategia consiste en crear 1000x1000 hilos, cada uno de los cuales hace una suma y entre todos completan la matriz C con el resultado. Estos hilos se distribuyen por las numerosas unidades de ejecución, por lo que el nivel de paralelismo es altísimo. Como ya se ha dicho antes, no hay mecanismos complejos para sincronizar los hilos o reservar recursos. Esto sin embargo no es un problema, ya que como se puede ver, los hilos que se generan son muy simples y no necesitan este tipo de facilidades.

La capacidad de las GPUs actuales de realizar ciertos calculos de una manera muy eficiente, aprovechando el paralelismo como hemos visto, ha hecho que además de para generar gráficos, se utilicen en otras disciplinas como en cálculo científico, donde ciertos problemas pueden descomponerse en multitud de hilos que realizan pequeñas operaciones, ejecutándose en la GPU con un rendimiento mucho mayor que en la CPU.

Patxi Astiz

lunes, 26 de octubre de 2009

Predicción de saltos

En general las instrucciones de salto son muy habituales en el código y por lo tanto los riesgos asociados a estas también, por lo que la predicción de saltos es algo inévitable en cualquier procesador moderno. Recordemos brevemente los 2 tipos de predictores dinámicos más sencillos. Uno es el predictor de 1 bit, y simplemente predice que un salto hará lo mismo que hizo la vez anterior. El 2º es el predictor de 2 bits, cuya máquina de estados es la siguiente:En este caso se tiene en cuenta lo que ha sucedido en los 2 últimos saltos. En el diagrama de estados, 'T' significa 'Taken', es decir que se salta y 'NT' significa 'Not Taken', es decir que no se toma el salto. Como puede verse, si un salto se ha tomado sistemáticamente, hace falta que no se salte 2 veces seguidas para cambiar la predicción. Este predictor permite evitar los fallos del predictor de 1 bit al salir y entrar en los bucles, pero todavía es susceptible de mejora.

El predictor del que vamos a hablar es el predictor adaptativo de 2 niveles, originalmente propuesto por Tse-Yu Yeh y Yale N. Patt en 1991 en el paper "Two-Level Adaptative Training Branch Prediction". Hay variaciones sobre el esquema original que se utilizan en los procesadores modernos.

La idea original es dividir el predictor en 2 estructuras. Por una parte un historial de saltos y por otra una tabla con un historial de patrones. En la primera tabla, para cada instrucción de salto, se almacenan en registros de desplazamientos la información de si se tomó o no en las anteriores 'n' ejecuciones. Un '1' significa que sí mientras un '0' que no se saltó. Si por ejemplo un salto se toma 3 veces seguidas y a la 4ª no, el valor de su historial de saltos será '1110'. Este valor no se utiliza directamente para hacer la predicción, sino que sirve de índice para mirar en la 2ª tabla, la que contiene los patrones de comportamiento del salto. El valor que leemos de esta segunda tabla es en realidad el estado de una máquina de estados como puede ser el predictor de 1 bit o el de 2 bits que hemos visto anteriormente. Veamoslo con un esquema.
Cómo se puede ver, básandose en el historial de saltos tenemos el índice con el que buscar en la tabla de patrones, que nos devolverá el estado con el cual haremos la predicción definitiva. Cuando el salto se resuelva definitivamente, podremos actualizar el estado correspondiente.

Veamos un ejemplo, supongamos que tenemos un salto que siempre se toma 3 veces seguidas, la 4º no se toma y este patrón se repite indefinidamente. Tras unas cuantas ejecuciones de dicha instrucción, tendremos información almacenada en la tabla de patrones, para diferentes índices. Por ejemplo, para el índice '1011' el estado almacenado indicará que hay que tomar el salto, ya que en las anteriores veces siempre se ha saltado. Sin embargo para el índice '0111' el estado almacenado en la tabla debe indicar que no hay que tomar el salto.

Este diseño ha sido el origen de una familia de predictores que mantienen 2 estructuras (una con un historial de saltos y otra con el comportamiento), pero pueden diferenciarse en 3 aspectos:
  • El historial de saltos se guarda por instrúcción (P), es global (G), en cuyo caso solo hay un registro o se guarda una entrada por cada N saltos (S).
  • La tabla de patrones es de tipo dinámica o adaptativa (A) o estática (S).
  • La tabla de patrones tiene entradas para cada instrucción de salto (p), para conjuntos de N saltos (s) o son globales (g).
Según estos 3 parámetros decimos que tenemos un predictor PAg en el caso del expuesto en el paper de Yeh y Patt, ya que el historial de saltos se almacena para cada instrucción de salto (P), la tabla de patrones guarda un estado dinámico (A) y es una tabla global (g), es decir no guarda valores diferentes para diferentes instrucciones de salto. Por ejemplo el predictor GAg es a veces conocido como gshare y lo utilizan los últimos procesadores tanto de AMD como de Intel.

Los procesasdores modernos hacen una predicción de saltos muy compleja ya que esto puede suponer una ventaja apreciable de rendimiento sobre sus competidores. En la actualidad es bastante común que los procesadores tengan predictores híbridos, es decir, mezclen la estimación de distintos tipos de predictores, como puede ser uno de 2 niveles GAg con uno de 2 bits. Además pueden incluir predictores especializados en detectar bucles o en predecir las direcciones de retorno de llamadas a funciones entre otros.

Patxi Astiz

viernes, 23 de octubre de 2009

Resolución de riesgos

En la actualidad, como ya hemos visto, la mayoría de los procesadores son RISC y su arquitectura se basa en muchos puntos en la arquitectura MIPS, que se ve en la siguiente imagen:


Como se puede ver en la imagen, el procesador está segmentado en 5 etapas, que son:
  1. Emisión de instrucciones
  2. Decodificación de instrucciones
  3. Ejecución
  4. Acceso a memoria
  5. Escritura de resultados
En teoría, un procesador de este tipo podría finalizar una instrucción por ciclo pero en la práctica hay una serie de circunstancias que hacen que no sea así. A estas circunstancias las llamamos riesgos. Veremos 3 tipos de riesgos y qué se puede hacer para evitarlos o minimizarlos.

En primer lugar hay riesgos estructurales. Estos se dan cuando 2 o más instrucciones necesitan utilizar el mismo recurso a la vez. Si por ejemplo intentamos leer una instrucción a la vez que un dato de memoria (LOAD), nos encontramos con un riesgo estructural. Hay varias formas de actuar ante una situación así:
  • Que todas las instrucciones en conflicto menos una esperen a otro ciclo.
  • Replicar recursos.
  • Si el recurso lo permite, establecer turnos de uso dentro de un ciclo.
Por ejemplo, para resolver el riesgo que se ha planteado en el parrafo anterior, los procesadores MIPS duplican la memoria, pasando a tener una de instrucciones y una de datos. Cada solución tiene su inconveniente. Si hacemos que las instrucciones esperen, el procesador está perdiendo rendimiento. Si replicamos recursos, la fabricación será más cara. Si establecemos turnos, el recurso debe ser lo suficientemente rápido para relizar varias operaciones por ciclo, lo que seguramente aumente la complejidad y el precio.

El segundo tipo de riesgos que nos podemos encontrar son los riesgos de datos y son de alguno de estos 3 tipos:
  • RAW (Read After Write): Se produce cuando una instrucción necesita el resultado de una anterior que aún no se ha computado. Por ejemplo:
ADD R3,R1,R4
SUB R9,R3,R7
  • WAR (Write After Read): Sucede cuando una instrucción escribe en un registro que lee una instrucción anterior, como en este ejemplo:
ADD R3,R1,R4
SUB R1,R7,R8

Si la 2ª instrucción termina antes de que la 1ª lea sus operandos, el valor de R1 será incorrecto.
  • WAW (Write after Write): Se produce cuando una instrucción escribe en un registro en el que también escribe otra instrucción anterior, como en este ejemplo:
ADD R3,R1,R4
SUB R3,R8,R5

En este caso, si la 2ª instrucción termina antes que la primera, cuando esta lo haga modificará el valor de R3 incorrectamente.
Para evitar estos riesgos, se pueden utilizar las siguientes técnicas:
  • Se puede parar la ejecución hasta que la instrucción correspondiente haya terminado
  • Si nos fijamos en el diagrama del principio, tras la fase de ejecución ya podemos tener el resultado de la operación por si una posterior lo necesita. Si modificamos el procesador para conectar la salida de la ejecución directamente con la decodificación de instrucciones, podemos minimizar los riesgos RAW. Esto está implementado en los procesadores MIPS.
  • Como no todas las instrucciones tienen dependencias entre sí, podemos reordenarlas en tiempo de compilación de manera que las que sí tienen dependencias estén los más separadas que se pueda. De esta manera se minimizan los riesgos vistos anteriormente.
El último tipo de riesgos son los riesgos de control. Aparecen cuando hay un salto condicional. La resolución del salto no es inmediata, por lo que no sabemos cual es la siguiente instrucción hasta que la instrucción pase por la fase de ejecución. Esto hace que el procesador tenga que pararse momentáneamente. Para mitigar esto podemos hacer lo siguiente:
  • Como ya hemos dicho, una posibilidad es pararse hasta que se resuelva el salto
  • El procesador predice si se salta o no. En caso de acertar no hay penalización. Si no, el resultado es el mismo que si se hubiera parado.
  • Se añade lógica a la fase de decodificación de instrucción para saber si se toma el salto lo antes posible. De todas formas en esta arquitectura habría un ciclo en el que todavía no se ha resuelto el salto. Para evitar perderlo, ese ciclo se utiliza para ejecutar (si es posible) una instrucción que no dependa del salto. Esto último puede realizarlo el procesador o el compilador.
Aunque en todos los casos parar la ejecución, o lo que es lo mismo intercalar instrucciones NOOP (no operation) en el flujo de ejecución evita todos los riesgos, esta solución siempre reduce el rendimiento del procesador y hay que recurrir a otras técnicas que añaden más o menos complejidad al procesador. Más adelante veremos cómo en la práctica las técnicas que se utilizan son más complejas que las descritas y los procesadores son capaces de reordenar las instrucciones dinámicamente para evitar los riesgos que hemos visto, evitando las paradas y la pérdida de rendimiento que conllevan.

Patxi Astiz

martes, 20 de octubre de 2009

CISC y RISC

Una de las primeras decisiones a la hora de diseñar un procesador es el conjunto de instrucciones. Básicamente hay 2 tipos diferentes, que son CISC y RISC. Vamos a ver en que consiste cada uno de ellos.

CISC significa Complex Instruction Set Computer. Los procesadores de este tipo tienen un conjunto de instrucciones amplio y que realizan operaciones complejas entre operandos en memoria o en registros. Este tipo de procesadores son los primeros que se desarrollaron. Los motivos para seguir esta estrategia a la hora de diseñar este tipo de conjunto de instrucciones fueron muchos. En aquella época era habitual programar en lenguaje ensamblador por lo que instrucciones de este tipo hacían el procesador más sencillo de cara al programador. Se consideraba más sencillo diseñar un procesador CISC que un compilador más complicado. De hecho en muchas ocasiones ni se disponía de uno. Otro motivo era que al describir con una sola instrucción acciones más o menos complejas, que de otra manera supondrían varias instrucciones más sencillas, se ahorraba memoria que resultaba cara y de acceso lento. Más adelante, según se extendían los lenguajes de alto nivel y los compiladores, se añadían instrucciones muy especificas, que no estaban pensadas para ser usadas para programar en ensamblador, pero que eran prácticas para el compilador, ampliando y haciendo más complejo el conjunto de instrucciones. El problema de estos procesadores es que son dificiles de optimizar. Hay muchas instrucciones y algunas demasiado complejas, mientras que los recursos hardware a la hora de diseñar un procesador son limitados. También se dificulta el paralelismo, debido a lo heterogeneo del conjunto de instrucciones.

Más adelante surgió la idea de limitar el conjunto de instrucciones y hacer que cada una haga una tarea sencilla. A los procasadores que utilizan este tipo de estrategia se les conoce como RISC (Reduced Instruction Set Computer). Este tipo de procesadores que aparecieron desde el final de los años 70, se basan en operaciones load-store que permiten pasar datos de memoria a registro y viceversa y operaciones básicas cuyos operandos siempre son registros. De esta manera lo que en un procesador CISC suponía una instrucción, puede y suele traducirse en varias instrucciones RISC. Las ventajas de esto es que cada instrucción es más sencilla por lo que puede ejecutarse más rápidamente y no hay tanta diferencia en el tiempo de ejecución de instrucciones diferentes. También se puede fijar el tamaño de una instrucción. Por todo esto la segmentación y el paralelismo en la ejecución es más sencilla para el diseñador del procesador. También se limita la gestión del acceso a memoria y su retardo a 2 únicas instrucciones.

Existen otras tecnologías como VLIW y EPIC, aunque RISC y CISC son las que se utilizan en la gran mayoría de los casos.

Vistas las diferencia y teniendo en cuenta lo que han evolucionado los compiladores desde su aparición, es lógico que RISC sea lo que utilizan todos los procesadores modernos.

Antes de terminar, una puntualización. Los procesadores que utilizamos están basados en la arquitectua x86 que utiliza un conjunto de instrucciones de tipo CISC. En la época de los 486 (segmentados) y los primeros Pentium, (superescalares) estos procesadores optimizaban ciertas instrucciones en detrimento de otras. Las instrucciones optimizadas eran un subconjunto similar a lo que sería un procesador RISC. En la actualidad los procesadores convierten las instrucciones CISC en una o varias micro-operaciones según convenga al diseño del procesador que se hace siguiendo la filosofía RISC.

Patxi Astiz