Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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.
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.
- 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.
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.
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.
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.
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.
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
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.
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.
( 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.
NOTA:
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:
- 32 accesos de registros.
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:
- 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.
- 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.
- 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.