domingo, 27 de diciembre de 2009

Programación del Cell

Como ya hemos visto, el procesador Cell tiene una arquitectura que exige un esfuerzo extra al programador a la hora de adecuar el programa a las peculiaridades del hardware. Esto es importante si se quiere aprovechar la capacidad del procesador y para ello hay varios modelos de programación que se pueden seguir.

En primer lugar se puede programar para Cell como si fuera un procesador de propósito general, al tener un núcleo PowerPC con VMX. Este modelo es muy sencillo pero obviamente desaprovecha la mayor parte de la potencia de cálculo del procesador. Sin embargo para tareas en las que esta potencia no es realmente necesaria compensa el menor tiempo y coste de desarrollo.

El siguiente paso en cuanto a complejidad consiste en utilizar un SPE para realizar ciertas tareas concretas. Mientras que el programa se ejecuta en el PPE, parte del algoritmo puede que se beneficie de la potencia de cálculo de los SPE. En ese caso se programa y compila para un SPE y desde el PPE se encarga de configurar el procesador para que se ejecute cuando haga falta. Este modelo de programación utiliza los SPEs como una ayuda en momentos puntuales. Esto introduce una dificultad añadida que es la gestión de la memoria. Cada SPE tiene una memoria local de 256 KB y puede acceder a la memoria principal mediante DMA. En caso de que los datos y el código no quepan en la memoria local hay que incluir código que gestione los datos de la memoria local y el acceso a memoria principal para que el SPE no se quede parado a la espera de los datos que necesite.

Para aprovechar al máximo los recursos que nos ofrece el procesador hay que utilizar modelos de programación paralela. Los modelos clásicos pueden adaptarse a este procesador, como por ejemplo el modelo de memoria compartida. Los SPE pueden procesar los datos en memoria compartida mientras que el PPE les facilita los servicios propios del sistema operativo, como acceso a la memoria, a sistemas de entrada/salida, etc.



Además del modelo de memoria compartida hay otros, como establecer una cola de trabajos de forma que se va repartiendo trabajo a los SPEs según van solicitándolo al unirse a dicha cola. Esta cola la gestiona el PPE y va asignando datos a procesar y espacio en memoria en la que dejar los resultados a los SPEs que estén bloqueados en la cola. Otra posibilidad es formar un pipeline con los SPEs, de manera que van realizando parte del trabajo y pasando el resultado al siguiente SPE que lo continuará, al estilo de una cadena de montaje.

Teniendo memoria compartida, esta puede utilizarse para implementar los métodos habituales de comunicación entre procesos como mutexes, semáforos o paso de mensajes y aprovecharlos para ejecutar varios hilos en paralelo en distintos SPEs.

Otra forma de gestionar los recursos del procesador es mediante el propio sistema operativo, en lugar de hacerlo el desarrollador de la aplicación. Esto se puede realizar de 2 maneras. La primera consiste en considerar los SPEs como un recurso al que se puede acceder representándolo mediante un sistema de ficheros. La segunda estrategia consiste en que el sistema operativo sea capaz de diferenciar entre hilos que deben ejecutarse en el PPE e hilos para los SPEs. En este caso es el kernel del sistema operativo el que se encarga de planificar los hilos y enviarlos a ejecutar a la unidad que corresponda del procesador. El programador puede crear y destruir hilos o tareas para los SPEs mediante llamadas al sistema. Este modelo es el que utiliza el kernel de Linux.

Patxi Astiz

miércoles, 23 de diciembre de 2009

Cell vs. Xenon

La última generación de consolas ha traído 2 procesadores que han introducido cambios importantes en la forma de diseñar procesadores de proposito general que se ha mantenido hasta ahora. Por una parte, el procesador de la Xbox360 (Xenon), al que hemos dedicado una entrada en esta blog y por otra el procesador Cell desarrollado por IBM, Sony y Toshiba. Este es el aspecto que tiene el Cell:


Como puede verse el diseño es muy diferente al del procesador Xenon, ya que aunque tiene varios núcleos, estos son heterogéneos. Sin embargo, el objetivo de los 2 procesadores no es tan diferente.

El PPE (Power Processor Element) del procesador Cell es muy parecido a un núcleo del Xenon. Ejecuta instrucciones en orden y puede ejecutar 2 hilos simultáneamente. También está basado en la tecnología PowerPC y funciona a la misma velocidad. También tiene la tecnología VMX. La memoria caché del PPE también tiene 2 niveles, con 32 KB para instrucciones y 32 KB para datos en la caché de nivel 1 y 512 KB en la de nivel 2. Como se puede ver este núcleo es bastante similar a un núcleo del procesador Xenon.

El diseño del procesador de la Xbox está pensado para ejecutar aplicaciones que procesan grandes cantidades de datos y para ello se han dispuesto 3 núcleos que funcionan a velocidades de reloj elevadas con unidades vectoriales potentes pero que no dejan de ser procesadores de propósito general. En el Cell el objetivo es el mismo, proporcionar mucha potencia de calculo para este tipo de aplicaciones pero para ello se han añadido al diseño procesadores mucho más especializados. Estos son los llamados SPE (Synergistic Processing Elements).


Los núcleos SPE tienen ciertas características que los hacen especialmente potentes para algunos cálculos a cambio de simplificar al máximo su diseño. Cada uno de estos nucleos tiene 128 registros de 128 bits y es capaz de hacer 4 operaciones de coma flotante de precisión simple en un ciclo de reloj. Un SPE tiene una memoria local de 256 KB , ejecutan las instrucciones en orden, carecen de caché y de predicción de saltos. Una unidad tiene un pico teórico de 25.6 GFLOPS, sin embargo el rendimiento con (por ejemplo) código con muchos saltos condicionales se reduce drásticamente. Este tipo de código debería ejecutarse en el PPE.

En ambos casos los procesadores dejan bastante responsabilidad al programador y al compilador que deben asegurarse de generar código paralelizado que utilice los recursos del procesador en cuestión adecuadamente. En el caso del Cell el hecho de tener núcleos heterogéneos y en el caso de los SPEs muy especializados en cierto tipo de cálculos, obliga a un esfuerzo mayor a la hora de decidir cómo realizar la división y adaptación de un programa en hilos, teniendo en cuenta en qué tipo de núcleo deben ejecutarse. Mientras tanto en el Xenon todos los núcleos son iguales y la división y adaptación es más sencilla. A cambio, el procesador Cell puede conseguir un rendimiento altísimo si se programa adecuadamente para él.

Patxi Astiz

jueves, 17 de diciembre de 2009

Xenon

Xenon es el procesador de la consola de Microsoft Xbox360. El diseño de este microprocesador es obra de IBM y aunque está basado en la arquitectura PowerPC, hay varios cambios que se han hecho pensando en el tipo de tareas que el procesador de una consola suele realizar. Este es el aspecto del procesador Xenon:



En un videojuego o en aplicaciones multimedia en general, es muy habitual el realizar procesado más o menos sencillo y uniforme sobre una gran cantidad de datos que normalmente son (casi) secuenciales. Por esto se ha diseñado un procesador con un pipeline de 21 etapas. Esto permite que la frecuencia de reloj sea muy alta y en el caso de ejecutar código sin muchos saltos condicionales y en el que se puede prever y leer los datos que se necesitarán con suficiente antelación, el rendimiento del procesador crece.

Otra característica importante de este procesador es que se ha intentado aprovechar el paralelismo a nivel de hilo en vez de la estrategia más clásica de explotar el paralelismo a nivel de instrucción. Esto se traduce en que el procesador tiene 3 núcleos, cada uno de los cuales puede ejecutar 2 hilos. Además estos núcleos no tienen capacidad para ejecutar instrucciones fuera de orden ni especulan, haciendo que cada núcleo sea más sencillo, consuma menos y ocupe menos espacio en el circuito integrado. Ahora el programador y el compilador son los responsables de asegurar el paralelismo en la ejecución en lugar del procesador. Otra consecuencia de este planteamiento es que en general no se replican las unidades funcionales, como es habitual en muchos procesadores actuales, ya que como se acaba de explicar, se busca que sean instrucciones de diferentes hilos que se ejecutan en distintos núcleos las que se ejecutan simultáneamente, en lugar de que un núcleo reordene las instrucciones para conseguir el paralelismo. Estas son las unidades que tiene cada núcleo:

  • 1 unidad de enteros
  • 1 unidad de coma flotante
  • 1 unidad de salto
  • 1 unidad load-store
  • 2 unidades VMX-128


Como se puede ver hay una excepción y se ha replicado la unidad VMX (con 128 registros) que es la encargada de ejecutar operaciones vectoriales sobre los datos. El motivo es que es de esperar que se ejecuten aplicaciones en las que hay paralelismo de datos (gráficos en 3D, cálculos cálculos físicos) que se ven muy beneficiadas por estas unidades.

Respecto a la jerarquía de memoria en el procesador, está dividida en 2 niveles de memoria caché. El primero al ser un procesador segmentado, consiste de una caché de instrucciones de 32 KB y otra de datos del mismo tamaño. El 2º nivel de caché tiene una capacidad de 1 MB. En comparación con procesadores de propósito general de PC, esta memoria es algo escasa, pero tiene su justificación en las limitaciones técnicas y en que juegos y aplicaciones similares no suelen beneficiarse de memorias caché muy grandes, ya que habitualmente leen datos y realizan cálculos con ellos, pero no suelen reutilizarlos. De hecho el desarrollador puede elegir deshabilitar la caché si en algún momento el código no lo necesita. También puede utilizarse para intercambiar datos con la GPU, ya que esta tiene acceso a la caché y enviar de manera muy eficiente información geométrica generada en el procesador.

En resumen, el diseño de este procesador, aunque basado en un PowerPC de propósito general, se ha modificado sustancialmente favoreciendo a las aplicaciones que realizan cálculos sobre grandes cantidades de datos, como multimedia o 3D intensivo. Estas son las partes más costosas a la hora de ejecutar un videojuego moderno en tiempo real, por lo que aunque el código relacionado con la lógica del juego o la inteligencia artificial se vea perjudicado, en conjunto el rendimiento obtenido es bueno.

Patxi Astiz

sábado, 28 de noviembre de 2009

PCI vs AGP vs PCIe

La tecnología de los buses utilizados con las tarjetas gráficas ha evolucionado para adaptarse a las necesidades. Vamos a hablar de los más utilizados en la informática de consumo durante los últimos años.

En primer lugar el bus PCI apareció de la mano de Intel al comienzo de la década de los 90. Se trata de un bus síncrono de 32 bits de ancho con funcionando a 5 V y a una frecuencia de 33 MHz, lo que supone un ancho de banda de 133 MB/s. Este ancho de banda se comparte entre los componentes que estén conectados y que se reparten el uso del bus. Los dispositivos PCI funcionan junto con un árbitro que decide quien tiene permiso para realizar transacciones usando el bus. Más adelante hubo modificaciones al estándar que disminuyeron el voltaje a 3.3 V, aumentaron el bus a 64 bits y la frecuencia de funcionamiento a 66 MHz. Además de esto apareció una versión llamada PCI-X con mejores prestaciones (mayor frecuencia de reloj) para servidores.

Al final de la década de los 90 apareció AGP (accelerated graphic port) al hacerse necesario un aumento en el ancho de banda disponible para la tarjeta gráfica. En este caso, AGP no se trata de un bus sino que es un puerto por lo que no se comparte con otros dispositivos. Está destinado únicamente a comunicar la tarjeta gráfica con el chipset norte o el dispositivo que corresponda. Las características de este bus son similares a las de PCI. Su ancho de banda es 32 bits, funciona a 66 MHz, realizaba una transferencia por ciclo y el voltaje de señalización es 3.3 V lo que da un ancho de banda de 266 MB/s, el doble que PCI. Después de esta versión conocida como AGP 1x, hubo revisiones que aumentaban el número de transferencias por ciclo y en algunos casos reducían el voltaje, apareciendo AGP 2x (2 transferencias, 533 MB/s), AGP 4x (1.5V, 4 transferencias, 1066MB/s), AGP 8x (0.8V, 8 transferencias, 2133MB/s). Aparecieró una variación con un conector físico mayor que permitía un mayor consumo de potencia por parte de la tarjeta gráfica (AGP PRO) manteniendo el resto de características. En la actualidad, salvo algunas excepciones, está en desuso.

PCI-Express (abreviado como PCIe) es una tecnología de entrada/salida que tiene multitud de utilidades y que en la práctica ha sustituido a AGP. Aunque tome el nombre del bus PCI, lo cierto es que no tiene ningún parecido con él. El motivo es la compatibilidad lógica con el anterior estándar. En este caso no se trata de un bus paralelo compartido por varios dispositivos, sino que la arquitectura consiste en enlaces serie que puede unir dos dispositivos entre sí, o varios dispositivos que enlazan individualmente con un switch que los intercomunica. Esta arquitectura es mucho más parecida a una red full-duplex que a un bus clásico. Respecto a su rendimiento, la frecuencia de reloj es 1.25 GHz y realiza 2 transferencias por ciclo, lo que supone 2.5Gb/s. Al utilizar una codificación 8b/10b, el ancho de banda final es de 250MB/s en un enlace. El estándar permite usar varios enlaces simultaneos. En la práctica, las tarjetas gráficas suelen usar 16, por lo que el ancho de banda en cada sentido es 16*250 = 4 GB/s. Más adelante se estandarizó la versión 2.0 que es la que está en uso actualmente, la mayor diferencia es que la frecuencia de reloj es el doble, con lo que se consigue el doble de tasa de transferencia por lane (500 MB/s) y una tarjeta gráfica PCIe x16 (16 enlaces) tiene 8GB/s en cada dirección. La versión 3.0 está en proceso. Entre los cambios más importantes la frecuencia de reloj llega a 4 GHz y la codificación 8b/10b se ha eliminado por una técnica de scrambling, por lo que se espera 1 GB/s en cada sentido usando un solo lane.

Patxi Astiz

gestión de entrada/salida

A la hora de gestionar la comunicación del procesador con elementos externos (disco duro, tarjeta de red, etc...) hay varias estrategias posibles.

La forma más sencilla de tratar una petición de entrada/salida es la espera de respuesta o espera activa. El funcionamiento es muy sencillo. El procesador consulta cíclicamente al dispositivo en cuestión hasta que la operación por la que se esperaba se ha realizado. Mientras tanto el procesador no hace ninguna tarea útil, desaprovechando un tiempo que podría dedicarse a otras tareas. Por este motivo, este patrón de funcionamiento debería evitarse siempre que sea posible.

Otra forma de gestionar la entrada/salida es el uso de interrupciones. Una interrupción es una señal asíncrona que es recibida por el procesador. En ese momento el flujo de ejecución normal se detiene, se guarda el estado del procesador y se pasa a ejecutar lo que se denomina manejador de la excepción. Este manejador consiste en código que puede estar establecido por el sistema operativo o por el programador. De esta manera el procesador puede dedicarse a ejecutar tareas útiles que interrumpe cuando un dispositivo de entrada/salida avisa mediante una interrupción de un evento que necesita ser conocido por el procesador como la finalización de una operación, la llegada de un paquete a un interfaz de red u otros. Entonces el procesador realiza las operaciones necesarias, como copiar datos o procesarlos en el código del manejador.

Un tercer método es el denominado acceso directo a memoria (DMA, Direct Memory Access). Con este método se permite leer y escribir de la memoria a un dispositivo distinto a la CPU y se utiliza para agilizar las operaciones de entrada/salida. Con esta tecnología se libera a la CPU de la tarea de copiar datos entre un dispositivo de entrada salida y la memoria. Normalmente la CPU inicia la transferencia mediante un comando DMA. Mientras se realiza la copia entre un dispositivo y la memoria (o viceversa) el procesador puede realizar trabajo útil. Mediante una interrupción se notifica el final de la copia.

El objetivo final de los métodos anteriores es conseguir que la gestión de entrada/salida suponga el menor esfuerzo posible a la CPU. El siguiente paso es utilizar hardware especializado. El hardware utilizado es generalmente un procesador especializado al que la CPU envía pequeños programas. Estos coprocesadores tienen una memoria local y acceso a la memoria principal y se utiliza cuando se necesitan altas prestaciones en la entrada/salida de la arquitectura.De esta manera el coprocesador puede gestionar la entrada/salida de manera autónoma sin necesidad de mayor intervención por parte de la CPU.

Patxi Astiz

viernes, 27 de noviembre de 2009

Arquitectura de memoria del Phenom

Vamos a fijarnos a modo de ejemplo en una arquitectura actual. En este caso se trata del procesador Phenom de AMD. Este procesador es multi-core y además puede usarse en configuraciones con varios procesadores.

Vamos a comenzar observando una fotografía de un procesador con 4 cores:

Como se puede ver, sobre la foto se han marcado las areas que corresponden a algunas partes del procesador. En concreto en la parte superior izquierda, vemos las zonas más importantes en las que se divide un core. Respecto a la memoria vemos los siguientes componentes:
  • Memoria caché de nivel 1: Al ser un procesador segmentado las instrucciones están separadas del código. Cada core tiene 64 KB para la caché de instrucciones y la misma cantidad para la caché de datos.
  • Cada core tiene su propia caché de nivel 2 de 512 KB
  • Por último, los 4 nucleos comparten una memoria caché de nivel 3 de 2 MB.
Los nucleos del procesador se comunican entre ellos mediante el hardware etiquetado como northbridge en la fotografía, que hace las veces de bus de conexión. Este también conecta el controlador de memoria que se ve a la derecha y los puertos hypertransport de la parte superior e inferior del chip, etiquetados como "HT".

El esquema para una configuración con 4 procesadores es el siguiente:


Según las especificaciones del fabricante, cada uno de estos enlaces realiza hasta 4000MT/s y tiene un ancho de banda de 8 GB/s y hasta 16 GB/s funcionando en modo 3.0. Esto coincide con un bus de 16 bits de ancho, funcionando a 1 GHz. Al ser DDR (Double Data Rate), eso se traduce en 16 * 2 bits/ciclo * 1 GHz = 4GB/s. Se incluyen 2 buses hypertransport, uno en cada sentido, por lo que el total en los 2 sentidos son los 8 GB/s anunciados. El estándar 3.0 aumenta la frecuencia de funcionamiento permitida, por lo que a 2 GHz el bus daría 16 GB/s de ancho de banda en total (sumando los 2 sentidos).

El controlador de memoria DDR2 de doble canal está incorporado en cada procesador y en este caso acepta memoria funcionando a frecuencias hasta 266 MHz. La tecnología DDR2 dobla la frecuencia del bus para obtener un mayor ancho de banda (aumentando la latencia). Además se transmiten 2 bits por ciclo (Double Data Rate). Teniendo en cuenta esto, que el ancho del bus es de 64 bits y el hecho de que el controlador es de doble canal, tenemos un ancho de banda máximo (266 * 2) MHz * 64 pistas/canal * 2 canales * 2 bits/(pista*ciclo) = 17.024 GB/s.

Hay que fijarse que la arquitectura de este ejemplo es NUMA (Non-Uniform Memory Access) por lo que ambos anchos de banda son importantes a la hora de acceder a la memoria ya que un dato podría estar en cualquiera de los bancos de memoria, no necesariamente en el que está directamente conectado al procesador que necesita dicho dato, teniendo que pasar por un enlace hypertransport compartido para más tareas y más lento que los 17 GB/s máximos de ancho de banda del controlador de memoria. En este problema mucha responsabilidad recae en el sistema operativo, que debe repartir los procesos entre los procesadores y las páginas de memoria física entre los bancos de memoria de forma que estén los más cerca posible.

Patxi Astiz

miércoles, 25 de noviembre de 2009

Memoría en GPUs

Las tarjetas gráficas, aunque no siempre, incorporan su propia memoria. La tecnología utilizada en esta es muy similar a la memoria RAM de la CPU de un ordenador.

De hecho las tarjetas gráficas utilizaban memoria SDR, después DDR estándar y en algunos casos memoria DDR2 convencional. Sin embargo en los últimos años han aparecido variaciones específicas para su uso en tarjetas gráficas, con los nombres GDDR2, GDDR3, GDDR4 y GDDR5. Hay que decir que con estas memorias se utilizan diferentes anchos de bus, frecuencias y número de canales, por lo que el ancho de banda de una tarjeta gráfica a su memoria es muy variable y no depende únicamente de la tecnología utilizada.

La primera memoria especialmente pensada para tarjetas gráficas (GDDR2) era en realidad una variante de DDR normal. Funcionaban a 2.5 V e igual que DDR utilizaban el flanco de bajada y de subida para duplicar su tasa de transferencia, solo que a mayor frecuencia de reloj que la memoria DDR convencional. Esto hacía que el consumo y el calor disipado fueran bastante altos. Por ello algunos fabricantes que comenzaron a utilizarla rectificaron y volvieron a DDR. Con este tipo de memoria, funcionando a 900 MHz se han conseguido anchos de banda de hasta 14.4 G/s.

La siguiente variación bautizada GDDR3 fue diseñada por ATI y utilizada tanto por esta compañía como por nVidia. Un pin transmite 4 bits cada 2 ciclos de reloj. En este caso se redujo el voltaje (1.8 ó 2.0 V dependiendo del fabricante), con lo que el consumo es más reducido. Además de esto hay cambios en la señalización y en la terminación eléctrica de esta que permiten mejorar la velocidad de funcionamiento. Este tipo de memoria es muy popular en la tarjeta actuales y se utiliza (no exclusivamente) en las 3 consolas de la actual generación. Como ya se ha dicho, hay una gran variedad de configuraciones para la memoria. Pondremos com ejemplo extremo de esto un dispositivo Tesla de nVidia, que con una frecuencia de reloj de 1600 MHz, 512 bits de bus y 4 canales de memoria (uno por nucleo) llega a un ancho de banda combinado de 410 GB/s con memoria GDDR3. Una tarjeta gráfica normal sin embargo tiene un ancho de banda que puede ir desde los 25 GB/s para buses estrechos y velocidades de reloj bajas, hasta más de 150 GB/s para las configuraciones de memoria más eficientes.



GDDR4 es una mejora sobre GDDR3. Los cambios más importantes son la reducción del voltaje a 1.5 V y el aumento de la tasa de transferencia por ciclo, pasando a transmitir 8 bits cada 2 ciclos de reloj, doblando (teóricamente) el ancho de banda de GDDR3, aunque con una latencia bastante mayor. Este tipo de memoria no ha tenido una gran acogida en comparación con su predecesora y se utiliza en unos pocos modelos de tarjetas gráficas de ATI. Incluso algunos fabricantes han anunciado que no van a fabricar chips de memoria GDDR4. A modo de ejemplo, la primera tarjeta gráfica que usó este tipo de memoria tenía un ancho de banda entre GPU y memoria de 64 GB/s con un bus de 256 bits y una frecuencia de funcionamiento de la memoria de 1 GHz.

Por último tenemos la memoria GDDR5 cuya principal novedad respecto a GDDR4 es que dobla el número de pins de datos del chip de memoria, doblando la tasa de transferencia de su antecesor. Este tipo de memoria se utiliza en algunos modelos modernos de tarjetas gráficas de ATI. Estas tarjetas logran unos anchos de banda que van desde unos 50 GB/s hasta más de 100 GB/s para cada core de la GPU.

En el futuro se espera que muchos fabricantes adopten la futura DDR3 aunque seguramente tenga que competir con XDR o XDR2 (tecnología RAMBUS) en las tarjetas de gama alta.

Patxi Astiz

lunes, 23 de noviembre de 2009

Organización de una caché

Las memorias caché son elementos básicos en cualquier CPU y GPU actual. Su funcionamiento básico es muy sencillo. Se trata de una tabla. Consta de una serie de marcos en los que se almacenan copias de bloques de la memoria principal. Cada uno de estos marcos va asociado a una etiqueta que permite saber que bloque de memoria se ha copiado en dicho marco. Los datos en una memoria caché pueden organizarse según varios parámetros que afectan a su rendimiento de una manera u otra.

El primer factor importante es el tamaño. En general una caché grande tiene menos fallos (es más probable que los datos pedidos estén en la caché), pero a cambio es lenta, además de que una caché muy grande supone ocupar una superficie del chip considerable.

Otra decisión a tomar es el tamaño de los marcos de la caché. En una caché con marcos pequeños es más fácil que el procesador según vaya pidiendo datos (por ejemplo procesando un array) llegue a pedir alguno que no está en la caché, por lo que hay más probabilidad de tener fallos. Sin embargo, en caso de tener un fallo, copiar un bloque a un marco de la caché es una operación menos costosa que si el marco fuera más grande.

Como hemos visto, las cachés pueden alojar algunos bloques de memoria para poder acceder a ellos rápidamente. Existen estrategias a la hora de emplazar un bloque memoria en un marco de la caché:
  • emplazamiento directo: A un bloque le corresponde un único marco. Esta estrategia es sencilla, hace que la caché sea rápida, pero provoca más fallos de caché.
  • emplazamiento asociativo: Un bloque puede almacenarse en cualquier marco. En este caso se minimizan los fallos, pero la caché es más lenta.
  • emplazamiento asociativo por conjuntos: A un bloque le corresponde un pequeño conjunto de marcos. Es una solución intermedia entre las dos anteriores.

Lo ideal sería poder tener una caché lo más grande posible y lo más rápida posible al mismo tiempo. Tecnológicamente no es posible por lo que se tiende a utilizar varios niveles de caché. Pueden usarse varios niveles de caché que son cada vez más grandes y más lentas. De esta manera el procesador accede a la caché de nivel 1, que es la más rápida, pero la más pequeña. En caso de haber un fallo la información no se busca directamente en la memoria principal, sino que se busca en la caché de nivel 2, que es más lenta, pero aún así sensiblemente más rápida que la RAM. En la actualidad es muy común utilizar 3 niveles de caché. El objetivo es que desde el punto del vista del procesador se vea una memoria del tamaño de la memoria principal (GB) con la velocidad de acceso de la caché de nivel 1 (ns).

En general un procesador puede incluir diferentes cachés diseñadas especialmente para una función específica, lo que hace que tengan sus peculiaridades. Por ejemplo es habitual que existan cachés separadas para de datos y de instrucciones, al menos en el primer nivel, para evitar los riesgos estructurales.

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

viernes, 16 de octubre de 2009

Breve historia de las APIs gráficas

Aunque en un principio era habitual que en una aplicación accediera directamente al hardware gráfico, según aumentó su complejidad se comenzaron a utilizar APIs que cumplieran con un estándar para abstraer al programador de la tarjeta gráfica. De esta manera se evitaba el esfuerzo que suponía escribir código específico para cada modelo de hardware gráfico.

El desarrollo del primer estándar comenzó en 1972 cuando el ACM SIGGRAPH (Association for Computing Machinery's Special Interest Group on Computer Graphics and Interactive Techniques) formó el grupo Graphics Standards Planning Committee. Del trabajo de este grupo surgió "Core" en 1977, que se trata del primer estándar gráfico que sirvió de base a las que serían las primeras APIs. La primera de ellas fue GKS (Graphic Kernel System) que se estandarizó al principio de los 80 y es el conjunto de la propuesta del instituto DIN con algunas modificaciones tomadas de "Core". Aunque "Core" tenía en cuante los gráficos en 3D, GKS solo contemplaba gráficos vectoriales en 2D. Para superar esta limitación se propuso utilizar un estándar diferente llamado PHIGS (y su posterior ampliación PHIGS+) o utilizar GKS-3D, que es un superset que permite especificar y generar gráficos en 3D. Finalmente ambos coexistieron.

Por otra parte, en esta época (principios de los 90), Silicon Graphics era la empresa lider en la informática gráfica y había creado su propio API propietario para sus productos. Este se llamaba IRIS GL, que se consideraba muy superior a los estándares mencionados anteriormente, aunque solo disponible para los clientes de Silicon Graphics. La llegada y crecimiento de la competencia, que utilizaba PHIGS y extensiones de esta API, hizo que la compañía se replanteara su estrategia y surgió OpenGL como un estándar gráfico independiente del resto del sistema operativo, sistema de ventanas, etc... De esta manera cualquier fabricante podia escribir un driver y permitir al desarrollador utilizar OpenGL. Finalmente en 1992 se constituyó el ARB (architectural review board), un grupo formado por varias empresas que mantiene y expande la especificación. De esta manera, si uno o varios fabricantes añaden una nueva funcionalidad al hardware, puede proponer una extensión a la API que permita hacer uso de esta mejora.




Las tarjetas gráficas con GPU comienzan a llegar a los ordenadores domésticos en la última mitad de los 90, principalmente de la mano de 3dfx. Estas tarjetas tienen que tener un precio moderado por lo que no pueden tener toda la funcionalidad de una estación de silicon graphics utilizando OpenGL. La estrategia que siguieron es la de utilizar un API con una cantidad limitadad de llamadas, totalmente implementada en el hardware. A este API le llamaron Glide y durante varios años fue el que dominó este sector del mercado. Un hito especialmente importante fue MiniGL, que consistía en un driver que implementaba el subconjunto de llamada de OpenGL que utilizaba la versión de quake con soporte para esta API. Más adelante salieron más versiones de este driver que incluían más y más llamadas. De esta manera OpenGL apuntaba como una posible API para las tarjetas gráficas de consumo y Glide se fue abandonando en favor de esta según la competencia de 3dfx (principalmente ATI y nVidia) mejoraba sus productos y los superaba en rendimiento y en ventas.

Por esta época, Microsoft que formaba parte de los fundadores del ARB de OpenGL decide impulsar su propia API, llamada Direct3D, que competiría con Glide (en un primer momento) y OpenGL. Las primeras versiones fueron muy impopulares entre los programadores, que preferían utilizar OpenGL y aunque hubo un primer intento por unificar OpenGL y Direct3D en un único API, este fracasó y Microsoft continuó evolucionando su API propietaria.

Desde el final de los 90, OpenGL es la API mayoritaria excepto en el campo de los juegos para plataformas Windows donde coexiste con Direct3D y para Xbox, donde tan solo se utiliza Direct3D. Ambas APIs han evolucionado en estos años de distintas maneras.

Tal y como hemos visto, las decisiones acerca de OpenGL los toma un comité en el que están implicadas varias empresas. Una empresa puede proponer una extensión a la API y utilizarla. Más adelante el ARB si lo considera oportuno la aceptará, modificandola o incluso mezclandola con otras extensiones de otros fabricantes que tengan una funcionalidad parecida. Con el paso del tiempo, se han publicado versiones nuevas de OpenGL que incluyen al nucleo de la API lo que en su momento eran extensiones. Estas modificaciones pueden ser desde nuevos tipos de texturas hasta llamadas que permiten usar la memoria gráfica para almacenar vertices. Una de las modificaciones más importantes fue la inclusión de GLSL en la versión 2.0, en Septiembre del 2004. Esto consiste en un lenguaje con el que se pueden programar las GPUs que por la época comienzan a incluir este tipo de funcionalidades. La última versión es la 3.2 que fue publicada en Agosto del 2009. También existen versiones reducidas del estándar bajo el nombre OpenGL ES. Las versiones de OpenGL ES son subsets del conjunto completo de llamadas de la API completa y están pensadas para dispositivos móviles o con recursos limitados.

Por su parte Microsoft ha públicado 6 versiones diferentes de Direct3D hasta la época. Durante este tiempo, además de incluir funcionalidades según el hardware evolucionaba, han cambiado partes de la API. Por ejemplo se eliminaron los criticados execution buffer de las primeras versiones, se incluyó la funcionalidad otra API llamada DirectDraw en Direct3D y se hicieron más cambios para hacerlo más sencillo para los desarrolladores, pasando a ser una buena API en la que se han corregido los errores iniciales. La última versión se lanzó junto con el sistema operativo Windows Vista, mientras que la siguientes (Direct3D 11) ha sido anunciada como parte de Windows 7, de próxima publicación.

Existen y han existido más APIs gráficas, pero consideramos que estas son las más importantes por cuota de mercado o por importancia histórica y especialmente OpenGL y Direct3D serán las que más se usen en el futuro cercano. El ámbito de cada una en los próximos años parece estar claro, mientras que Direct3D se utiliza en la mayoría de los juegos para Windows y en todos los de la consola Xbox360, OpenGL está presente en algunos juegos para PC, en gran cantidad de software profesional y en las demás consolas, como Playstation 3, psp, Wii y Nintendo DS, por lo que parece claro que estos 2 APIs van a seguir cubriendo la mayor parte del mercado.

Patxi Astiz

martes, 13 de octubre de 2009

SPECviewperf 10.0

En primer lugar, antes de comenzar con las entradas más técnicas vamos a comenzar con algo al alcance de todo el mundo. Vamos a probar el rendimiento de la tarjeta gráfica del ordenador desde el que escribo.

Muchas veces el análisis del rendimiento de una GPU se realiza ejecutando una serie de programas, típicamente juegos, con una configuración predeterminada. Esto permite comparar el rendimiento de distintas GPUs entre sí. En este caso vamos a utilizar un benchmark gráfico, es decir un programa específicamente creado para probar la tarjeta gráfica y puntuar su rendimiento.

Hay 2 benchmarks gráficos que son los más utilizados. Uno es 3DMark que está orientado a probar las capacidades de una tarjeta con videojuegos modernos que demandan mucho procesado gráfico. Además de los jugadores suelen utilizarlo aficionados a la informática en general, especialmente al overclocking, que suelen competir por conseguir la mayor puntuación modificando el hardware para ello. El API que utiliza es Direct3D. El otro benchmark, que es el que vamos a utilizar es SPECviewperf. Este benchmak está orientado a probar el rendimiento de la tarjeta gráfica con aplicaciones de diseño y modelado profesional, en lugar de con videojuegos. De hecho está formado por una serie de test pensado cada uno para un programa entre los que están 3DStudio, Catia o Maya entre otros. En este caso el API es OpenGL.

El equipo es un portátil con un procesador centrino 1.6 GHz y una tarjeta gráfica ATI Mobility X700. Los test los hemos ejecutado bajo Windows XP y Linux 2.6.28 y en ambos casos hemos utilizado el driver oficial, aunque hay que decir que en el caso de Linux el soporte está abandonado para esta tarjeta, por lo que la última versión del driver que funciona con el kernel y servidor gráfico instalado es mas vieja que en el caso de windows. Lógicamente hemos usado la versión de 32 bits del test y un único hilo de ejecución. La resolución del test ha sido 1280x1024 que aunque no coincide con el tamaño de la pantalla, es la más parecida. Y sin más dilación, estos son los resultados:

Test

3dsmax-04
Windows

5.49
Linux

5.48
catia-02 3.38 3.72
ensight-03 4.83 4.57
maya-02 11.41 9.35
proe-04 2.90 2.78
sw-01 6.87 6.45
tcvis-01 1.89 N/A
ugnx-01 2.43 N/A

Lo primero es apuntar que la versión de Linux del benchmark está en estado alpha por lo que no todos los test han dado un resultado válido. Aunque en general las puntuaciones son similares, sí que aparecen ciertas diferencias, especialmente en el test maya-02. Las tarjetas gráficas de los últimos años son un hardware complejo y la forma de programar hace que mejore o empeore el rendimiento. Conocer la arquitectura del hardware para el que se va a programar es muy importante y más en estos casos al tratarse de un driver. Como se ve, la diferencia en el rendimiento puede llegar a ser notable.

También podemos comparar nuestros resultados con los que aparecen en la propia página de SPEC (results overview). Nuestro sistema seguramente no sea apropiado para utilizar las herramientas que se prueban con suficiente fluidez. Como se puede ver, los resultados de los equipos utilizados en la tabla del enlace son prácticamente un orden de magnitud mayores. Hay que tener en cuenta que las tarjetas gráficas utilizadas son mucho más modernas (aproximadamente 4 años) que la de nuestro ordenador y además son modelos pensados para utilizar con programas de diseño y modelado. Esta diferencia de rendimiento nos da una idea de como los sistemas gráficos han evolucionado durante los últimos años y lo siguen haciendo.

Si queréis podéis descargar un benchmark vosotros mismos, tanto 3DMark como SPECviewperf y probarlos en vuestros equipos. En esas mismas páginas hay resultados de otros equipos con los que comparar. También hay varias páginas dedicadas al hardware de PC en general, que entre otras cosas analizan y prueban el rendimiento de las tarjetas gráficas, generalmente utilizando juegos para ellos. Las 2 páginas más conocidas son tomshardware.com y anandtech.com.

Según se vayan explicando más conceptos de arquitectura de GPUs haremos una breve reseña a la GPU utilizada en estas pruebas y podremos compararla con una actual para ver las diferencias del hardware y comprender mejor el motivo de la diferencia de rendimiento.

Patxi Astiz

Presentación

Este primer post va a ser una presentación. Mi nombre es Patxi Astiz y soy estudiante de la Universidad Rey Juan Carlos, donde estoy cursando el Máster en Informática Gráfica, Juegos y Realidad Virtual.

Este blog va a servir para ir publicando pequeños artículos acerca de los contenidos de la asignatura Arquitecturas para Gráficos y Multimedia. Los temas centrales de esta asignatura son la arquitectura de procesadores de proposito general, que servirá de introducción a la arquitectura de procesadores gráficos, arquitectura de memoria y entrada/salida, arquitectura de consolas modernas y finalmente evaluación de rendimiento en arquitecturas para gráficos. Aunque estos son los temas principales, habrá contenido sobre otros temas siempre relacionados con tecnología que permita generar gráficos por ordenador.

Espero que según vaya escribiendo en este blog os parezca al menos interesante y os animéis a leerlo, apuntaros al RSS y a dejar vuestros comentarios.

Patxi Astiz