En este artículo de AnandTech, el jefe de tecnología (CTO) de AMD, comenta que hoy en día todos los fabricantes de µP(microprocesadores) tienen que pasar de estar volcados en aumentar el ILP a centrarse en aumentar el TLP. Pero, ¿qué significa eso? ¿Qué tiene que ver con AMD, con Cell, y con otros microprocesadores?
ILP: Instruction Level Parallelism
Los procesadores, tanto CISC como RISC, han intentado aumentar el número de instrucciones que se podían ejecutar por ciclo de reloj: es lo que se llama Paralelismo de Instrucciones(Instruction Level Parallelism, ILP). Por ejemplo, si una instrucción intentaba incrementar un valor, y en una segunda se multiplicaban otros valores, esto podía hacerse simultáneamente si había dos unidades libres.
Está claro que para lograr el máximo grado de paralelismo entre instrucciones, necesitamos:
- Un conjunto amplio de unidades de ejecución, para poder repartir las instrucciones
- Un conjunto amplio de registros, para que las operaciones puedan hacerse simultáneamente
En los procesadores RISC, el número de registros siempre ha sido alto: un PowerPC pre-G4 dispone de 64 registros, y un PowerPC G4 o posterior dispone de 96 registros “brutos”. Además, existen registros de renombrado, que sirven para que dos tareas puedan utilizar registros diferentes aunque en realidad estén intentando utilizar el mismo registro físico. Comparemos esto con la arquitectura x86, con un total de 18 registros en su versión de 32 bits, y de 26 registros en su versión de 64 bits (AMD64 o Intel x86-64).
Los problemas que tenemos con esta aproximación son los siguientes:
- Si varias instrucciones seguidas necesitan la misma unidad de ejecución, no se pueden ejecutar en paralelo
- Si una instrucción depende del resultado de la anterior, no se puede ejecutar en paralelo.
El primer problema se resuelve intentando maximizar el número de unidades de ejecución, pero este no puede crecer indefinidamente. A veces se opta por crear versiones “reducidas” de una unidad de ejecución, que sólo pueda realizar ciertas tareas (por ejemplo, una unidad de enteros que sólo suma o resta, pero no multiplica). También se puede resolver convirtiendo la instrucción en otra que se pueda pasar a otra unidad de ejecución. En cualquier caso, hay un límite, que es el número de unidades de ejecución disponibles.
El segundo problema es más peliagudo: una instrucción que depende del resultado de la anterior no se va a poder ejecutar nunca. Y hay un tipo particular de instrucción que depende del resultado de instrucciones anteriores: el salto condicional.
Un adivino en el silicio
Muchas veces, un programa tiene esta pinta:
- Guardar 30 en la variable “pasos”
- Hacer una operación
- Decrementar la variable “pasos”
- Si “pasos” no es cero, volver al paso 2
- Si es cero, seguimos adelante
En este caso, la evaluación del paso 4 no podría hacerse nunca a la vez que el paso 3, aunque los saltos sean responsabilidad de una unidad de ejecución diferente.
Sin embargo, en este mismo ejemplo vemos que la mayoría de las veces (29 de cada 30, en este caso), el paso 4 se limita a ser igual a “saltar al paso 2”, de modo que la mayoría de las veces podríamos hacer en paralelo las operaciones 3 y 2, en ese orden. De eso se encarga las unidades de predicciones de saltos, auténticos adivinos dentro de nuestro microprocesador.
El adivino se encarga, por un lado, de preparar las cosas como si hubiera acertado, pero por otro se encarga de evaluar si ha acertado o no. En el caso de haber fallado, tiene que descartar lo que ha empezado a hacer, y devolver al procesador al estado en el que debería estar. Esto lleva tiempo, y produce las llamadas burbujas en la tubería de instrucciones del procesador. Cuantas más instrucciones pueda tener al vuelo un microprocesador (por ejemplo el paso 2 puede ser un paso compuesto, que también incluya saltos), más problemático es equivocarse. Por eso hay gran cantidad de circuitos dedicados a esta tarea de adivinación en los microprocesadores modernos.
Queda claro, por tanto, que la única forma de acelerar el microprocesador de esta forma, una vez agotados los incrementos en unidades de ejecución —que se verían ahogados por la falta de registros, o por el número de accesos a memoria necesarios para llenarlos—, y el incremento en el número de registros —algo que normalmente no se puede hacer, porque supone cambiar la arquitectura del microprocesador; AMD e Intel sólo lo han hecho al cambiar a una arquitectura de 64 bits, y esos registros no son visibles en aplicaciones de 32 bits—, es aumentar la velocidad de reloj.
Sin embargo, esto también tiene varios límites:
- A mayor velocidad de reloj, mayor consumo
- La velocidad de reloj no puede aumentarse tanto como se quiera, sino que ese incremento está limitado por el tamaño de las unidades de ejecución: hay que dividir el microprocesador en unidades de ejecución cada vez más pequeñas, de forma que cada vez hacen menos, y es necesario encadenarlas. En ese caso, una burbuja en la secuencia de instrucciones supone una penalización mayor
- La velocidad de reloj no puede subir mucho más que la velocidad de acceso a la memoria, puesto que las instrucciones deben de ejecutarse sobre datos que estén en el microprocesador. Si hay que acceder a la memoria, hay una gran penalización. No digamos ya si el dato se encuentra en memoria virtual, en lugar de memoria real…
Ejecución multitarea y paralelismo de instrucciones
Otro escenario en el que el paralelismo a nivel de instrucciones no puede explotarse bien es en el caso de la multitarea. En este caso, lo que se produce es que se ejecutan una serie de instrucciones pertenecientes a un programa, se guarda el estado, se ejecutan instrucciones pertenecientes a otro, se guarda el estado, se recupera el estado del primero, se ejecutan más instrucciones, y así sucesivamente. Además, no se pueden poner en paralelo instrucciones de tareas diferentes.
Está claro que cada uno de esos cambios de contexto suponen una penalización en el rendimiento del microprocesador: todo el tiempo dedicado a guardar y recuperar estados es tiempo en el que no se ejecutan tareas útiles.
TLP: Paralelismo a nivel de tareas
La solución a este problema es lo que se llama paralelismo a nivel de tarea, o TLP. Consiste, ni más ni menos, que disponer de otro núcleo que sea capaz de ejecutar otro hilo de instrucciones —del mismo, o de otro programa—, en un procesador o núcleo indistinguible.
Apple fue uno de los primeros fabricantes de ordenadores personales en utilizar —a la fuerza, debido a los problemas de Motorola para incrementar la velocidad de reloj del G4— esta técnica, utilizando configuraciones de dos procesadores, pero hasta la llegada del Mac OS X, debido al historial monoprocesador del Mac OS 9, la mayoría de las veces el segundo procesador estaba ocioso. Con Mac OS X —o cualquier otro sistema operativo construido para multiprocesadores, como otros Unix, Linux, Windows NT/2000/XP/2003—, cada hilo se asigna a un procesador diferente —si es posible ;-)—, o al menos a un procesador que tenga menos carga.
Si en lugar de tener dos microprocesadores completos, dispusiéramos de un procesador único con dos núcleos, tendríamos las siguientes ventajas:
- Mayor facilidad y rapidez en la comunicación entre procesadores
- Caché compartida, de modo que un procesador está al tanto de los cambios en memoria que el otro ha realizado.
Rendimiento, consumo energético y disipación de calor
Un sistema de dos núcleos puede, en teoría, llegar a un rendimiento doble al de un sistema monoprocesador, pero esto se alcanza únicamente cuando las tareas que se realizan son absolutamente independientes. Si es necesario establecer algún tipo de comunicación entre los programas que están trabajando en procesadores diferentes, este número baja.
Sin embargo, hoy en día son muchas las tareas simultáneas que se ejecutan en un ordenador: creación del interfaz de usuario, envío y recepción de datos en red, mensajería instantánea, audio/video-conferencia… estas tareas son, en principio, independientes entre sí, y son candidatas a ejecutarse en unidades de ejecución diferentes, aprovechando al máximo la capacidad disponible. También es posible reducir la velocidad del procesador para adecuarla a la de la memoria, o el FSB, y repartir el acceso entre dos tareas, que se pueden ejecutar con un menor tiempo de espera a memoria. Aún así, las velocidades de los buses a memoria también van creciendo, y podremos aprovecharlos aumentando el número de núcleos que pueden acceder simultáneamente a la memoria.
Además, es más sencillo disipar 100W repartidos entre dos procesadores, que disipar esos mismos 100W concentrados en un único núcleo.
¿Qué tiene esto que ver con Cell, y con los PowerPC?
Todos los fabricantes de µP han anunciado versiones multi-núcleo de sus microprocesadores. De hecho, Cell es uno de ellos —”recordemos”: que Cell, en su configuración normal, consta de 10 unidades de ejecución—, y dispone de un rendimiento pico tremendo —256GFlops—. La unidad principal de Cell es un doble núcleo PowerPC de 64 bits, lo que nos indica que IBM está en camino. Ya se prevé un PowerPC 970MP, y hay otros indicios de que Apple va a utilizar dos procesadores multi-núcleo en sus próximos equipos.
Lo interesante es que, de nuevo, el rendimiento de los sistemas tendrá que ser establecido por la cantidad de trabajo que son capaces de soportar, en lugar de fijarnos en una única métrica, la velocidad de reloj, que no era sinónimo de rendimiento. ¡Van a ser tiempos interesantes dentro de la tecnología de microprocesadores y la arquitectura de sistemas!