Sei sulla pagina 1di 12

ARQUITECTURAS DE PALABRA DE INSTRUCCIÓN MUY LARGA

Compilando programas de aplicaciones científicas ordinarias con una técnica radical llamada
“Programación de trazas”. Nosotros estamos generando un código para una maquina paralela que
ejecutara estos programas más rápido que una maquina secuencial equivalente. Esperamos de 10
a 30 veces más rápido.

La “Programación de trazas” genera código para maquinas denominadas Arquitecturas de


palabras de instrucción muy largas. En las maquinas VLIW, muchas operaciones programadas
estáticamente, estrechamente acopladas y de grano fino se ejecutan en paralelo dentro una sola
secuencia de instrucciones. VLIW’s son más extensiones paralelas de varias arquitecturas actuales.

Granularidad (Computación paralela): cantidad de trabajo (o de cálculo) que realiza una operación.
sobrecarga entre múltiples procesadores o elementos de procesamiento.

Estas arquitecturas actuales nunca han roto una barrera fundamental. La aceleración que obtienen
del paralelismo nunca es más que un factor de 2 a 3. No es que no podamos construir más
maquinas paralelas de este tipo; pero hasta la programación de trazas, no supimos cómo generar
códigos para estas. La programación de trazas encuentra suficiente paralelismo en el código
ordinario para justificar el pensamiento sobre un VLIW como altamente paralelo.

En Yale actualmente estamos construyendo una. Nuestra máquina, ELI-512, tiene una palabra de
instrucción horizontal de más de 500 bits y realizará de 10 a 30 operaciones de nivel RISC por ciclo.

Una vez claro que podríamos compilar código para una maquina VLIW, aparecieron algunas
preguntas nuevas y las respuestas se presentan en este documento. ¿Cómo ponemos suficiente
prueba cada ciclo sin hacerlo demasiado grande? ¿Cómo colocamos suficientes referencias de
memoria en cada ciclo sin hacer que la máquina sea demasiado lenta?

¿QUÉ ES VLIW?

Todos quieren usar hardware barato en paralelo para acelerar el cálculo. Un enfoque obvio seria
tomar su computadora favorita RISC, permitir que sea capaz de ejecutar de 10 a 30 operaciones de
nivel RISC por ciclo controlado por una palabra de instrucción muy larga. Una VLIW parece un
microcódigo horizontal muy paralelo.

Mas formalmente, las arquitecturas VLIW tienen las siguientes propiedades:

- Hay una unidad de control que emite una sola instrucción larga por ciclo.
- Cada operación consiste en muchas operaciones independientes estrechamente
acopladas.
- Cada operación requiere un número pequeño y predecible de ciclos para ejecutar.

Las operaciones pueden ser segmentadas. Estas propiedades distinguen los VLIW’s de los
multiprocesadores (con grandes tareas asíncronas) y las máquinas de flujos de datos (sin un solo
flujo de control y sin el acoplamiento ajustado). Los VLIW’s no tienen los regulares requerimientos
de un procesador vectorial o procesador de matriz real.

Se han construido muchas maquinas como esta, pero todas han alcanzado un tope muy bajo en el
grado de paralelismo que proporcionan. Además de los microcodigos horizontales, estas máquinas
incluyen el CDC 6600 y sus muchos sucesores, como la porción escalar del CRAY-1; el tramo IBM y
360/91; y los Stanford MIPS. No es sorprendente que no ofrezcan mucho paralelismo. Los
experimentos y la experiencia indicaron que solo un factor de aceleración de 2 a 3 del paralelismo
estaba disponible dentro de los bloques básicos. (un bloque básico de código no tiene ningún salto
excepto al comienzo y no salta excepto al final.)

Nadie sabía cómo encontrar el paralelismo más allá de los saltos condicionales, y evidentemente
nadie los miraba. Parecía obvio que no podía poner operaciones de diferentes bloques básico en la
misma instrucción. No había manera de saber de antemano sobre el flujo de control. ¿Cómo
sabrías si querías que fueran ejecutados juntos?

De vez en cuando, las personas han construido máquinas VLIW mucho más paralelas para fines
especiales. Pero estas han sido codificadas a mano. La codificación manual de máquinas de
instrucciones largas es una tarea horrible, como cualquiera que haya escrito un microcodigo le
podría decir. Los arreglos de código son intuitivos y casi imposibles de seguir. Los procesadores de
propósito especial pueden salir con la codificación manual porque solo necesitan unas pocas líneas
de código.

Estamos hablando de un orden de magnitud más de paralelismo; obviamente podemos olvidarnos


de la codificación manual. pero, ¿de dónde viene el paralelismo?

No de bloques básicos Los experimentos mostraron que el paralelismo dentro de los bloques
básicos es muy limitado. Pero una técnica radical de compactación global nueva llamada
programación de trazas puede encontrar grandes grados de paralelismo más allá de los límites de
los bloques básicos.

La programación de trazas no funciona en algunos códigos, pero funcionará en la mayoría del


código científico general. Y funciona de una manera que hace posible construir un compilador que
genera un código altamente paralelo.

Los experimentos realizados con la programación de trazas en mente verifican la existencia de


grandes cantidades de paralelismo más allá de los bloques básicos.

¿POR QUÉ NO MÁQUINAS VECTORIALES?

Las máquinas vectoriales parecen ofrecer mucho más paralelismo que el factor 2 o 3 que ofrece el
actual VLIW. Aunque las máquinas de vectores tienen su lugar, no creemos que tengan muchas
posibilidades de éxito en el código científico de propósito general. Son crucificadamente difíciles
de programar, y aceleran solo los bucles internos, no el resto del código.

Para programar una máquina vectorial, el compilador o el codificador manual deben tomar las
estructuras de datos en el código que se ajustan casi exactamente a la estructura integrada en el
hardware. Para programar una máquina vectorial, el compilador o el codificador manual deben
tomar las estructuras de datos en el código que se ajustan casi exactamente a la estructura
integrada en el hardware. Eso es difícil de hacer en primer lugar, y tan difícil de cambiar. Una
modificación, y el código de bajo nivel debe ser reescrito por un programador muy inteligente y
dedicado que conoce el hardware y, a menudo, las sutilezas del área de aplicación.
A menudo la reescritura no tiene éxito; vuelve a los tableros de dibujo otra vez. Mucha gente
espera que un compilador muy inteligente pueda producir código altamente vectorizado a partir
del código escalar común. Creemos que la vectorización producirá suficiente paralelismo en solo
un pequeño porcentaje de programas.

Y la vectorización solo funciona en bucles internos; el resto del código no tiene aceleración alguna.
Incluso si el 90% del código estuviera en bucles internos, el otro 10% funcionaría a la misma
velocidad que en una máquina secuencial. Incluso si pudiera obtener el 90% para ejecutar en
tiempo cero, el otro 10% limitaría la aceleración a un factor de 10.

PROGRAMACION DE TRAZAS

El compilador VLIW que hemos creado usa una técnica de compactación global reciente llamada
programación de trazas. Esta técnica se desarrolló originalmente para la compactación de
microcódigos, siendo la compactación el proceso de generación de instrucciones muy largas de
alguna fuente secuencial.

El microcódigo horizontal es como las arquitecturas VLIW en su estilo de paralelismo. Difiere en


tener operaciones idiosincrásicas y hardware menos paralelo. Se han desarrollado otras técnicas
además de la programación de trazas para la compactación de microcódigos. Difieren de la
programación de trazas en la tarea de bloques básicos ya compactados y la búsqueda de
movimientos de códigos individuales entre bloques.

Eso podría funcionar para microcódigo horizontal, pero probablemente no funcionará para VLIW.
VLIW tiene mucho más paralelismo con el microcódigo horizontal, y estas técnicas requieren una
búsqueda demasiado costosa para explotarlo.

La programación de trazas reemplaza la compactación de código por bloque con la compactación


de flujos largos de código, posiblemente miles de instrucciones de largo. Este es el truco: haces un
poco de preprocesamiento. Luego, programa las largas transmisiones de código como si fueran
bloques básicos. Luego, deshace los malos efectos de pretender que son bloques básicos. Lo que
obtienes de esto es la capacidad de utilizar técnicas de programación bien conocidas y muy
eficientes confinadas a bloques básicos.

Para bosquejar brevemente, comenzamos con un código sin bucles que no tiene bordes
posteriores. Dado un gráfico de flujo reductible, podemos encontrar el código más interno sin
bucles.
a) Un diagrama de flujo, con cada bloque representa un bloque básico de código. b) Una traza
escogida en el diagrama de flujo. c) La traza se ha programado, pero no se ha vuelto a vincular con
el resto del código. d) Las secciones de código no programado, permiten volver a vincular.

(a) muestra un pequeño diagrama de flujo sin fondo negro. Información dinámica y saltos
predecibles, se usa al momento de la compilación para seleccionar flujos con la mayor
probabilidad de ejecución. Esos flujos que llamaremos "trazas". Escogemos nuestra primera traza
del código ejecutado más frecuentemente. (b) Una traza ha sido seleccionada del diagrama de
flujo.

El preprocesamiento evita que el trazado realice movimientos de código absolutamente errados


entre bloques, unos que bloquearían los valores de variables activas fuera de la traza. Esto se hace
agregando nuevos bordes especiales al gráfico de precedencia de datos creado para la traza. Los
nuevos bordes se dibujan entre las operaciones de prueba que saltan de forma condicional, por lo
que la variable está activa y las operaciones que pueden afectar a la variable. Los bordes se
agregan al gráfico de precedencia de datos y se parecen a todos los otros bordes. El programador,
ninguno de los más sabios, tiene permitido comportarse como si estuviera programando un solo
bloque básico. No presta atención de ningún modo para bloquear fronteras.

Una vez completada la programación, el planificador ha realizado muchos movimientos de código


que no conservarán correctamente los saltos de la secuencia al mundo exterior (o se
reincorporará). Por lo tanto, un postprocesador inserta un nuevo código en las salidas y entradas
del flujo para recuperar el estado correcto de la máquina fuera de la secuencia. Sin esta capacidad,
el paralelismo disponible estaría indebidamente restringido por la necesidad de preservar los
límites del salto.

En la parte (c) de la figura, la traza se ha aislado y en la parte (d) aparece el nuevo código no
compactado en el código que se divide y vuelve a unirse.

Luego buscamos nuestro segundo rastro. De nuevo, observamos el código ejecutado con más
frecuencia, que ahora incluye no solo el código fuente más allá de la primera traza, sino también
cualquier código nuevo que generemos para recuperar divisiones y volver a unir. Compactamos la
segunda traza de la misma manera, posiblemente produciendo un código de recuperación. (En
nuestra implementación actual, hasta ahora, nos ha sorprendido gratamente la pequeña cantidad
de código de recuperación que se genera.) Eventualmente, este proceso llega al código con poca
probabilidad de ejecución, y si es necesario, se usan métodos de compactación más mundanos.
utilizado para no producir código nuevo.
(a) Un cuerpo de bucle, que podría contener un flujo de control arbitrario, y un código de salida al
que salta. (b) El cuerpo del bucle desenrollado k veces. (c) Las trazas se recogen a través del ciclo
desenrollado y está programado. (d) El nuevo bucle programado se vuelve a vincular con el resto
del código.

La programación de trazas proporciona una solución natural para los bucles. Los codificadores de
mano usan la canalización de software para aumentar el paralelismo, reescribiendo un bucle para
hacer piezas de varias iteraciones consecutivas simultáneamente. La programación de trazas se
puede ampliar de forma trivial para realizar un pipeline de software en cualquier bucle.
Simplemente desenrollamos el ciclo para muchas iteraciones. El bucle desenrollado es una
secuencia, todas las pruebas de bucle intermedio son ahora saltos condicionales, y la secuencia se
compacta como se indica anteriormente.

Si bien este método de manejo de bucles puede ser algo menos eficiente que el teóricamente
necesario, puede manejar el flujo de control arbitrario dentro de cada iteración de ciclo anterior,
una gran ventaja al intentar compilar código real. La figura anterior, que generalmente es análoga
a la anterior, muestra cómo se manejan los bucles.
BULLDOG, UN COMPILADOR DE PROGRAMACION DE TRAZAS

Hemos implementado un compilador de programación de trazas en Maclips compilados en un


DEC-2060. Lo llamamos Bulldog para sugerir tenacidad (y evitar que la gente piense que fue escrita
en Harvard). Bulldog tiene 5 módulos principales, como se describe en la figura.

Nuestro primer generador de código es para una máquina VLIW idealizada que toma un solo ciclo
para ejecutar cada una de sus operaciones de nivel RISC (una idealización no demasiado drástica) y
tiene acceso ilimitado a la memoria por ciclo (idealización demasiado drástica). Estamos utilizando
el generador de código para ayudar a depurar los otros módulos del compilador y para medir el
paralelismo disponible. Las operaciones promedio empaquetadas por instrucción son una medida
espúrea de aceleración. En su lugar, dividimos el número de ciclos secuenciales al ejecutar el
código no compilado.

En comparación con el código idealizado, el código ELI real contendrá muchas operaciones
pequeñas incidentales. Si eso implica la misma aceleración, o menos, o más, está sujeto a debate.
Estas operaciones incidentales pueden ralentizar el código secuencial más que el paralelo,
haciendo que la aceleración debido al paralelismo sea aún mayor. Sólo el tiempo lo dirá.
La interfaz que estamos utilizando actualmente genera nuestro código intermedio de nivel RISC,
código de dirección N o NADDR. La entrada es un lenguaje de nivel local FORTRAN, C o Pascal
llamado Tiny-Lisp. Fue algo que creamos rápidamente para darnos la máxima flexibilidad. Nos
resulta fácil escribir código de muestra para él, no tuvimos que escribir un analizador, y podemos
jugar con el compilador fácilmente, lo que ha demostrado ser bastante útil. Un compilador de
subconjuntos FORTRAN '77 en NADDR se escribe y se depura, y consideraremos otros lenguajes
después de eso. Nuestro NADDR de nivel RISC es muy fácil de generar código y aplicar
optimizaciones de compilador estándar.

Tenemos dos generadores de código más que se están escribiendo en este momento . Un
generador ELI-512 completo está bastante avanzado: un subconjunto del mismo se está
interconectando con el selector de trazas y el código de reparación. También estamos escribiendo
el generador de código FPS-164. El FPS-164 es el sucesor del Floating Point Systems AP-120b,
probablemente la máquina de mayor venta que haya tenido el microcódigo horizontal como su
único lenguaje. Existe un compilador de FORTRAN para el FPS-164, pero nuestra experiencia ha
sido que encuentra poco, incluso la pequeña cantidad de paralelismo disponible en esa máquina.
Un compilador que compita con el código de la mano realmente cambiaría la usabilidad potencial
de esa máquina (es muy difícil entregar el código) y demostraría la versatilidad de la programación
de trazas.

MEMORIA ANTI-ALIASING EN BULLDOG

La programación de trazas hace necesario hacer un gran número de movimientos de código para
completar las instrucciones con operaciones que provienen de lugares muy separados en el
programa. Los movimientos del código están restringidos por la precedencia de los datos. Por
ejemplo, supongamos que nuestro programa tiene los siguientes pasos:

(1) Z ≔ A + X
(2) A ≔ Y + V
Nuestros movimientos de código no deben hacer que (2) se programe antes que (1). Por lo tanto,
el programador de traza necesita una traza de prioridad de datos antes de la programación.

Pero, ¿qué sucede cuando A es una referencia de matriz?

( 1 ) Z ≔ A [expr 1]+ X
( 2 ) A [expr 2]≔Y +V
Si (2) puede hacerse antes que (1) es ambiguo. Si se puede garantizar que "expr1" sea diferente de
"expr2".

Si (2) puede hacerse antes que (1) es ambiguo. Si se puede garantizar que "expr1" sea diferente de
"expr2", entonces el movimiento del código es permitido; de otra forma no. Responder a esta
pregunta es el problema de las referencias de memoria anti-aliasing. Con otras formas de
direccionamiento indirecto, como perseguir punteros, el anti-aliasing tiene pocas esperanzas de
éxito. Pero cuando las referencias indirectas son a elementos de matriz, generalmente podemos
decir que son diferentes en el momento de la compilación. Las referencias indirectas en los bucles
internos del código científico casi siempre son elementos de la matriz.

El sistema implementado en el compilador de Bulldog intenta resolver la ecuación "expr1 = expr2".


Usa definiciones de alcance para reducir el rango de cada variable en las expresiones. Podemos
suponer que las variables son números enteros y usar un solucionador de ecuaciones diofánticas
para determinar si podrían ser iguales. El análisis de rango puede ser bastante sofisticado. En el
sistema implementado, las definiciones se propagan lo más posible. Todavía no usamos las
condiciones de las ramas para reducir el rango de valores que una variable podría tomar, pero lo
haremos.

NOTA:

Se llama "ecuación diofántica" o "ecuaciones diofantinas" a cualquier ecuación algebraica, de dos


o más incógnitas, cuyos coeficientes recorren el conjunto de los números enteros, de las que se
buscan soluciones enteras, esto es, que pertenezcan al conjunto de los números enteros. Las
ecuaciones diofánticas tienen la característica de tener soluciones infinitas, siempre y cuando
dichas soluciones pertenezcan al conjunto de los números enteros.

Anti-aliasing ha sido implementado y funciona correctamente (si no rápidamente).


Desafortunadamente, le faltan algunas de sus habilidades, muy pocas, pero suficientes para
desacelerarla gravemente. En este caso, la afirmación realmente se cumple: la cadena es tan
fuerte como su eslabón más débil. Hasta ahora tenemos aceleraciones en el rango de 5 a 10 para
el código práctico que hemos visto. Bien, pero no es lo que queremos. Al examinar los resultados a
mano, queda claro que cuando se suministren las piezas faltantes, la aceleración será
considerable.

UNA MAQUINA PARA EJECUTAR CODIGO DE PROGRAMACION DE TRAZAS

ELI-512 tiene 16 clusters, cada uno contienes un ALU y algo de almacenamiento. Los clústeres
están dispuestos circularmente, cada uno de los cuales se comunica con sus vecinos más cercanos
y algunos se comunican con los clústeres más alejados.

El ELI usa su palabra de instrucción de 500+ bit para iniciar todo lo siguiente en cada ciclo de
instrucción:

- 16 operaciones de ALU. 8 serán operaciones enteras de 32 bits, y 8 se realizarán utilizando ALU


de 64 bits con un repertorio variado, incluidos los cálculos de coma flotante.

- 8 referencias de memoria segmentada.

- 32 accesos de registros.

Muchos movimientos de datos, incluido el operando, se seleccionan para las operaciones


anteriores.

Un salto condicional de múltiples vías basado en varias pruebas independientes. (Con todo esto
sucediendo a la vez, solo un maníaco querría codificar el ELI a mano).
Para llevar a cabo estas operaciones, el ELI tiene 8 M -clusters y 8 F-clusters. Cada M-clúster tiene
dentro de él:

- Un módulo de memoria local (de un tamaño hasta ahora indeterminado).

- Una ALU entera que es probable que pase la mayor parte del tiempo haciendo cálculos de
direcciones. Los repertorios exactos pueden variar de un clúster a otro y no se corregirán hasta
que sintonicemos la arquitectura con el código real.

- Un banco de registro entero multipuerto.

- Una barra transversal de clústeres limitada, con 8 o participantes con temperatura. Algunos de
los participantes serán buses fuera del clúster. Algunas de las conexiones de la barra transversal
no se realizarán.
Cada F-clúster tiene dentro de él:

- Un ALU de punto flotante. Los repertorios de las ALU variarán de un clúster a otro y no se
corregirán hasta que sintonicemos la arquitectura.

- Un banco de registro flotante multipuerto.

- Una barra transversal de clústeres limitada, con 8 o participantes con temperatura. Algunos de
los participantes serán buses fuera del clúster. Algunas de las conexiones de la barra transversal
no se realizarán.

No te dejes engañar por regularidades ocasionales en la estructura. Están ahí para hacer que el
hardware sea más fácil de construir. El compilador no los conoce y no intenta usarlos. Cuando
comencemos a ejecutar el código científico a través del compilador, sin dudas haremos más
ajustes en la arquitectura. Queremos eliminar tantos buses como podamos, y muchas de las
regularidades pueden desaparecer.

Los planes actuales son construir el ELI prototipo de 100KCL Logic, aunque podemos optar por
Shottkey TTL.
PROBLEMAS

Nadie nunca antes había querido construir una máquina de palabras de instrucción de
512 bits de ancho. Tan pronto como comenzamos a considerarlo, descubrimos que hay
dos grandes problemas. ¿Cómo se hacen suficientes pruebas en cada instrucción sin
hacer que la máquina sea demasiado grande? ¿Cómo colocas suficientes referencias de
memoria en cada instrucción sin hacer que la máquina sea demasiado lenta?
La comparación de VLIW con máquinas vectoriales ilustra los problemas que se deben
resolver. VLIW tiene operaciones de grano fino, estrechamente relacionadas, pero
lógicamente no relacionadas, en instrucciones únicas. Las máquinas vectoriales hacen
muchas operaciones de grano fino, estrechamente acopladas y lógicamente relacionadas
a la vez con los elementos de un vector. Las máquinas vectoriales pueden hacer muchas
operaciones paralelas entre pruebas; VLIW no puede. Las máquinas vectoriales pueden
estructurar referencias de memoria a matrices enteras o segmentos de matrices; VLIW no
puede. Hemos argumentado, por supuesto, que las máquinas vectoriales fallan en el
código científico general por otras razones. ¿Cómo obtenemos sus virtudes sin sus
vicios?
Los bloques básicos cortos implicaban una falta de paralelismo local. También implican
una baja proporción de operaciones a pruebas. Si vamos a empacar muchas operaciones
en cada ciclo, es mejor que estemos preparados para realizar más de una prueba por
ciclo. Tenga en cuenta que esto no es un problema para las máquinas operativas
programadas estáticamente de hoy en día, que no empaquetan suficientes operaciones
en cada instrucción para alcanzar esta proporción.
Claramente, necesitamos un mecanismo para saltar a uno de varios lugares indicado por
los resultados de varias pruebas. Pero no cualquier mecanismo de salto multivía servirá.
Muchas máquinas microdimensionables horizontalmente permiten especificar varias
pruebas en cada microinstrucción. Pero los mecanismos para hacer esto son demasiado
inflexibles para ser de gran utilidad aquí. No permiten múltiples pruebas independientes,
sino que ofrecen una selección de pruebas cableadas que pueden realizarse al mismo
tiempo. Algunas máquinas permiten un conjunto específico de bits después del cálculo de
la próxima dirección, lo que permite un salto de "2 ^ n". Esto se usa, por ejemplo, para
implementar una decodificación de código de operación, o alguna otra declaración de
caso de hardware.

Potrebbero piacerti anche