Hace ya cinco años que Intel nos dió el Pentium Pro con su arquitectura P6 que serviría de base para los Pentium II, Celeron y Pentium III. Y aunque la arquitectura P6 todavía se defiende admirablemente en las pruebas de velocidad contra el Athlon de AMD, ya está en los últimos momentos de su vida útil. Para el sucesor del Pentium III, Intel ha decidido cambiar radicalmente la arquitectura P6. El resultado se llama NetBurst.
Antes de ver las características más resaltantes de la arquitectura NetBurst, repasemos los problemas que trata de resolver.
A finales de los 70, cuando los ingenieros de Intel se sentaron a diseñar el sucesor de su exitoso procesador de 8 bits, el 8080 (y su hermano 8085), tomaron como punto de partida su conjunto de instrucciones. Extendieron las instrucciones para manejar también datos de 16 bits y añadieron otras que consideraron necesarias y/o deseables (por ejemplo, para multiplicar dos números). El procesador de 16 bits resultante, el 8086 y su hermano menor 8088, fue el primero de una gran familia que aún está con nosotros. Cada generación, 186, 286, 386, 486, Pentium, Pentium Pro, Pentium II (y su primo Celeron) y Pentium III, añadió instrucciones adicionales para aprovechar las características especiales de cada una (siendo MMX, SSE y SSE2 las más conspícuas).
Desde el punto de vista del usuario, el efecto de añadir instrucciones (en vez de crear un juego de instrucciones totalmente diferente en cada generación), es que los programas escritos para un procesador viejo pueden ser utilizados también con los nuevos, manteniendo nuestra inversión en software. Y también tiene mucho sentido desde el punto de vista de mercadeo.
Pero el conjunto de instrucciones x86 es un pesado lastre para Intel y sus competidores. Las instrucciones x86 son complejas, pudiendo ser de 1 byte en longitud hasta varios bytes. Esto hace que un procesador x86 tenga que gastar un tiempo considerable solamente en decodificarlas, así como también dedicar una buena parte del chip para esta tarea: el Pentium III tiene 3 unidades de decodificación (dos para instrucciones sencillas, una para instrucciones complejas).
Los procesadores pueden leer y escribir más rápido de lo que la memoria RAM puede funcionar. En consecuencia, los procesadores tienen que esperar a que la memoria termine de realizar la operación pedida. Una simple operación de leer memoria RAM implica:
Como se puede ver, una operación de lectura de memoria RAM es muy costosa en términos de tiempo. Y si a esto le agregamos que en la memoria RAM están tanto los datos como los programas, entonces es más aparente la magnitud del problema.
Una forma de minimizar esta espera es el uso de memoria caché.
La memoria caché es memoria muy rápida y se usa para almacenar los datos o partes de programa que se usan más frecuentemente. La idea del caché es sencilla: cuando el procesador quiere leer algo de la memoria, primero revisa si está en el caché; si lo consigue ya no necesita leerlo de la memoria principal con el consiguiente ahorro de tiempo; si no lo consigue tendrá que leerlo de la memoria y pone luego una copia en el caché; la próxima vez que necesite de nuevo el dato, ya lo tendrá en el caché. Como es de esperarse, su costo es mucho mayor que el de las memorias convencionales.
Si colocáramos el caché más cerca del procesador, por ejemplo en el mismo chip, podríamos saltar algunos de los pasos enumerados anteriormente. Mejor aún, si diseñamos el procesador de manera que incluya caché, el costo de leer o escribir en ella se reduciría al mínimo. Este es el caché L1 o nivel 1. El caché L2 puede estar en el mismo chip del procesador o fuera de ella. Si el caché L2 está en el mismo chip, y además agregamos otro nivel de caché fuera del chip, éste último sería caché L3.
Un truco complementario es que al leer de la memoria RAM no solamente se toma el dato buscado si no también los datos de las celdas siguientes. Esto tiene sentido en los programas, porque la siguiente instrucción a ejecutarse se encuentra (generalmente) en la siguiente posición de memoria. Con los datos también se puede aprovechar esta observación aunque en grado menor, por lo que también se acostumbra tener cachés diferentes para instrucciones y para datos.
Algunas instrucciones tardan más en ejecutarse que otras. Si tuviéramos varias unidades de procesamiento, podríamos ir ejecutando otras instrucciones para ir adelantando trabajo y aprovechar las unidades ociosas. Esto es algo que se hace ya desde el Pentium Pro: después de la etapa de decodificado de instrucciones, se generan micro-operaciones, las cuales son consumidas por las diversas unidades de ejecución. Las instrucciones pueden ser ejecutadas en un orden diferente al establecido en los programas, y una unidad especial se encarga de retirar los resultados de las micro-operaciones en el orden del programa. De más está decir que el procesador tiene circuitos especiales para apoyar esta actividad.
Por ejemplo, si un programa tiene que hacer los siguientes cálculos
a = b * c
d = a * 65.536
e = f + 31.1
el procesador puede ejecutar la tercera operación mientras está calculando la primera (el Pentium III tiene unidades de ejecución independientes para cálculos de punto flotante y para enteros), aún cuando no aparezcan en este orden en el programa. Note que no es posible calcular las dos primeras operaciones simultáneamente porque la segunda depende del resultado de la primera.
Pero hay una clase de instrucciones que interfiere con este tipo de optimización: los saltos condicionales. Estas instrucciones hacen que el programa salte a otro punto del programa dependiendo de una condición.
Por ejemplo, en el siguiente fragmento de programa
if a < b then c = 10 else d = a * b
e = 2 * d
"si a es menor que b se ejecuta c=10; en caso contrario se salta a la instrucción d=a*b y se ejecuta". Cuando el procesador llega a este punto del programa, no puede ejecutar e=2*d porque no sabe de antemano si se va a ejecutar c=10 ó d=a*b (y por tanto se modifica el valor de d, del cual depende).
No podemos predecir el futuro con una exactitud de 100% pero sí podemos aproximarnos. Esta es la idea de la "ejecución especulativa" y la "predicción de saltos". Cuando el procesador llega al punto del programa anterior, revisa el camino que tomó en ejecuciones anteriores (si no es la primera vez que pasa por este punto) y deduce el camino que con más probabilidad tomará. Del ejemplo anterior, si deduce que a<b entonces ejecutará c=10 y luego e=2*d, o sea, predijo el salto y está ejecutando especulativamente las instrucciones por adelantado.
¿Qué pasa si se equivoca en su predicción? Los resultados obtenidos en el "camino equivocado" se desechan y se toma el camino correcto. Esto quiere decir que el algoritmo utilizado para predecir qué camino tomar en los saltos condicionales es crítico para el rendimiento del procesador. Un procesador puede ser muy rápido, pero si el algoritmo usado no es lo suficientemente bueno - y por tanto desecha mucho trabajo ya hecho por haber tomado el camino equivocado - el rendimiento final puede ser pobre.