Títol: Desarrollo de un compilador de Fortran para CellSs/SMPSs

Anuncio
Títol: Desarrollo de un compilador de Fortran para
CellSs/SMPSs
Volum: 1
Alumne: Luis Martinell Andreu
Director/Ponent: Jesús Labarta Mancho
Departament: AC
Data: 12 de Juny de 2008
DADES DEL PROJECTE
Títol del Projecte: Desarrollo de un compilador de Fortran para CellSs/SMPSs
Nom de l'estudiant: Luis Martinell Andreu
Titulació: Enginyeria Informàtica
Crèdits: 37.5
Director/Ponent: Jesús Labarta Mancho
Departament: AC
MEMBRES DEL TRIBUNAL (nom i signatura)
President: Eduard Ayguadé Parra
Vocal: Lluís Màrquez Villodre
Secretari: Jesús Labarta Mancho
QUALIFICACIÓ
Qualificació numèrica:
Qualificació descriptiva:
Data:
Índice general
1. Introducción
1
2. Motivación
2.1. Computación de alto rendimiento . . . .
2.2. Aplicaciones HPC . . . . . . . . . . . .
2.3. Arquitecturas . . . . . . . . . . . . . .
2.4. Modelos de Programación . . . . . . .
2.4.1. Paralelismo . . . . . . . . . . .
2.4.2. OpenMP . . . . . . . . . . . .
2.4.3. MPI . . . . . . . . . . . . . . .
2.4.4. Otros modelos de programación
2.5. Fortran . . . . . . . . . . . . . . . . . .
2.5.1. Historia de Fortran . . . . . . .
2.5.2. Caracterı́sticas de Fortran . . .
2.5.3. Uso actual de Fortran . . . . . .
2.6. Conclusiones . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
5
7
8
8
9
9
10
11
11
13
14
15
3. Objetivos del proyecto
3.1. Tareas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
17
4. CellSuperScalar/SMPSs
4.1. Modelo de Programación . . . . . . . . . . .
4.2. Arquitectura de una aplicación CellSs/SMPSs
4.3. Runtime de Cell Superscalar . . . . . . . . .
4.4. Runtime de SMP Superscalar . . . . . . . . .
.
.
.
.
21
21
24
25
27
5. Fortran para CellSs/SMPSs
5.1. Definición de la sintaxis del lenguje para fortran . . . . . . . . . . . . . . . . . . . .
5.2. Interacción con la biblioteca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29
29
35
6. Diseño del compilador
6.1. Compiladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2. El proceso de compilación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2.1. Compilación en CellSs . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
39
39
42
44
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
ÍNDICE GENERAL
2
6.2.2. Compilación en SMPSs
6.3. Arquitectura del compilador . .
6.4. Diseño del metadriver . . . . . .
6.5. Diseño interno del compilador .
6.5.1. El Driver . . . . . . . .
6.5.2. El frontend . . . . . . .
6.5.3. El modelo de TL . . . .
6.5.4. Las fases . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
45
45
46
47
49
49
50
51
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
55
55
58
59
59
59
63
63
66
68
69
73
76
78
78
79
80
80
82
85
85
86
93
94
8. Testing y resultados
8.1. Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2. Resultados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95
95
98
7. Desarrollo del compilador
7.1. El metadriver . . . . . . . . . . . . . . . . . . . . . . . . .
7.2. mf95ss . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.1. Driver . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.2. Funcionamiento del frontend . . . . . . . . . . . . .
7.2.2.1. Análisis y estructuras internas del frontend
7.2.3. Modificaciones sobre el frontend original . . . . . .
7.2.3.1. Ampliación de la gramática . . . . . . . .
7.2.3.2. extended attribute . . . . . . . . . . . . .
7.2.4. Modelo de TL . . . . . . . . . . . . . . . . . . . . .
7.2.4.1. Estructuras del frontend . . . . . . . . . .
7.2.4.2. Langconstruct . . . . . . . . . . . . . . .
7.2.4.3. Recorridos sobre el AST . . . . . . . . .
7.2.4.4. Anotaciones . . . . . . . . . . . . . . . .
7.2.4.5. Fases . . . . . . . . . . . . . . . . . . . .
7.2.4.6. Generación de código . . . . . . . . . . .
7.2.5. Modelo de fases . . . . . . . . . . . . . . . . . . .
7.2.5.1. Configuración . . . . . . . . . . . . . . .
7.2.5.2. Pre-análisis . . . . . . . . . . . . . . . .
7.2.5.3. Análisis de tareas . . . . . . . . . . . . .
7.2.5.4. Function router . . . . . . . . . . . . . .
7.2.5.5. Transformación de llamadas a tareas . . .
7.2.5.6. Transformación de directivas . . . . . . .
7.3. Tecnologı́as . . . . . . . . . . . . . . . . . . . . . . . . . .
9. Conclusiones
9.1. Satisfacción de los objetivos
9.2. Planificación del proyecto . .
9.3. Valoración económica . . . .
9.4. Expectativas de futuro . . . .
9.5. Valoración personal . . . . .
10. Agradecimientos
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
101
102
103
103
104
105
ÍNDICE GENERAL
3
Bibliografı́a
107
Glosario
107
Anexos: Manuales de CellSs y SMPSs
111
4
ÍNDICE GENERAL
Índice de figuras
2.1. Targeta perforada con una instrucción Fortran . . . . . . . . . . . . . . . . . . . . .
11
4.1.
4.2.
4.3.
4.4.
4.5.
Multiplicación de matrices a bloques. . . . . . . . . . . . .
Cadenas de tareas en la multiplicación de matrices a bloques.
Adaptadores para invocar a las tareas desde la biblioteca. . .
Arquitectura del Cell/BE. . . . . . . . . . . . . . . . . . . .
Funcionamiento de CellSs. . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
22
23
25
25
26
6.1.
6.2.
6.3.
6.4.
6.5.
6.6.
6.7.
6.8.
6.9.
6.10.
6.11.
Proceso de compilación habitual. . . . . . . . . . . . . . . . . . . . .
Compilación para CellSs/SMPSs. . . . . . . . . . . . . . . . . . . .
Enlazado para CellSs/SMPSs. . . . . . . . . . . . . . . . . . . . . .
Proceso de compilación y empaquetado para CellSs. . . . . . . . . .
Proceso de enlazado para CellSs. . . . . . . . . . . . . . . . . . . . .
Proceso de compilación completo para SMPSs. . . . . . . . . . . . .
Módulos del metadriver para la parte de compilación y empaquetado.
Módulos del metadriver para la parte de enlazado. . . . . . . . . . . .
Proceso de compilación del mf95ss. . . . . . . . . . . . . . . . . . .
Proceso de análisis del frontend. . . . . . . . . . . . . . . . . . . . .
Pipeline de las fases de CellSs/SMPSs. . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42
43
43
44
45
46
47
47
48
50
52
7.1.
7.2.
7.3.
7.4.
7.5.
Tipos de anotación . . . .
Construcciones en el AST
Atributo como etiqueta . .
Clases de LangConstruct .
Functors . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
64
66
67
74
77
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
ÍNDICE DE FIGURAS
Capı́tulo
1
Introducción
En este documento se explica el trabajo realizado durante el tiempo que ha llevado completar este
Proyecto de Final de Carrera de la carrera de Ingenierı́a en Informática cursada en la Facultad de
Informática de Barcelona. El proyecto ha consistido en desarrollar un compilador de Fortran para Cell
Superscalar y SMP Superscalar [4] [6]. También conocidos como CellSs y SMPSs, son dos modelos
de programación desarrollados en el BSC (Barcelona Supercomputing Center)1 .
En los capı́tulos que componen el proyecto se intenta sintetizar el trabajo realizado mediante la
aplicación de los conceptos aprendidos y la práctica adquirida en los dos ciclos de la titulación y en el
periodo que he estado trabajando en el BSC.
El capı́tulo 2 explica las razones por las cuales se ha desarrollado el compilador. En primer lugar
se presenta la disciplina de la informática en la que estoy trabajando actualmente, la computación de
alto rendimiento, 2.1 y 2.2. La sección 2.3 presenta el panorama actual en cuanto a la arquitectura de
computadores en el ámbito de la computación de alto rendimiento y cómo esto genera la necesidad
de investigar nuevos modelos de programación que permitan crear aplicaciones y adaptar las aplicaciones actuales a las nuevas arquitecturas de los supercomputadores 2.4. Posteriormente se presenta
el lenguaje de programación Fortran 2.5, se explica su origen, su historia y el uso que se hace de
él actualmente. En la última sección del capı́tulo 2.6 se extraen conclusiones de lo explicado y se
justifica la necesidad de dedicar tiempo en desarrollar herramientas relacionadas con la creación de
aplicaciones en Fortran.
En el tercer capı́tulo, se definen los objetivos marcados al principio del proyecto. La satisfacción de
estos objetivos significa que el proyecto ha sido exitoso, no sólo desde el punto de vista del desarrollo
de una aplicación útil y práctica, también desde el punto de vista personal ya que permite a uno
demostrarse que los obstáculos encontrados en el camino han podido ser superados gracias a los
conocimientos adquiridos en la carrera, a la práctica acumulada durante estos años, al trabajo realizado
1
ver http://www.bsc.es/
1
CAPÍTULO 1. INTRODUCCIÓN
2
durante el proyecto y, lo más importante, al trabajo realizado en equipo y con un buen ambiente de
trabajo.
El capı́tulo 4 presenta los modelos de programación CellSs y SMPSs. Es necesario conocer la
filosofı́a y el funcionamiento de estos modelos para entender qué hace el compilador y por qué lo
hace. Me gustarı́a destacar aquı́ algo que no se menciona durante la memoria cuando se habla del
trabajo realizado que, sin embargo, es de gran importancia a la hora de valorarlo. Se trata del tiempo
dedicado a la práctica y estudio del modelo de programación. Este trabajo ha hecho posible alcanzar
el nivel de conocimientos sobre el modelo necesario para desarrollar el compilador y, lo que es más,
para entender su funcionamiento interno y adquirir conocimientos sobre las técnicas usadas en él.
El siguiente capı́tulo, el 5, presenta el modelo de programación adaptado a Fortran. Es necesario
definir de manera precisa cómo se va a usar el modelo en este lenguaje y como se integrará con los
elementos que componen CellSs y SMPSs.
Los dos capı́tulos siguientes están dedicados al trabajo de estudio, diseño e implementación del
compilador. El primero, el capı́tulo 6, explica la estructura y funcionamiento del compilador. El segundo, sintetiza el trabajo de desarrollo dando detalles sobre cómo se ha implementado cada parte
del compilador y cómo se han solucionado problemas concretos. El objetivo es explicar el trabajo
realizado de una manera que el lector sea capaz, además, de entenderlo.
El capı́tulo 8 explica cómo se ha comprobado el funcionamiento correcto del compilador y evalúa
la utilidad de crear aplicaciones CellSs/SMPSs en Fortran mediante el análisis del funcionamiento de
éstas.
Finalmente se presentan las conclusiones de la memoria del proyecto (capı́tulo 9) y los agradecimientos. Después de la bibliografı́a se incluyen varios anexos útiles para seguir la memoria en algunos
puntos y probar el software. También se adjunta al documento un CD con la distribución de CellSs y
SMPSs que incluye el compilador desarrollado.
Capı́tulo
2
Motivación
2.1. Computación de alto rendimiento
Esta sección tiene dos propósitos principales. Por un lado el de introducir al lector en el ámbito
de la supercomputación, conocer el estado actual de esta disciplina de la informática y las tendencias
futuras en arquitecturas y modelos de programación. Por otro lado, mostrar al lector las razones por
las que nuevos modelos de programación son necesarios y por qué es importante que estos nuevos
modelos den soporte al lenguaje de programación Fortran.
La computación de alto rendimiento (HPC) nace de la necesidad de efectuar gran cantidad de
cálculos para solucionar problemas de simulación en campos tan diversos como la quı́mica, biologı́a,
bioquı́mica, meteorologı́a, en el área industrial y muchos otros.
Al principio, los problemas más complejos eran resueltos fabricando ordenadores cada vez más
caros y complejos conocidos como mainframes. Con la entrada en la era de la miniaturización se empezó a ver que construir estas máquinas salı́a cada vez más caro y se empezaron a crear computadores
a partir de redes de ordenadores comerciales adaptados a las necesidades de los programas que iban a
ejecutar.
Siguiendo un curso análogo en el campo de los procesadores, la tendencia durante las últimas
décadas era el diseño de procesadores cada vez más complejos. Esta tendencia ha ido aumentando
a medida que la tecnologı́a ha permitido mayor miniaturización e incremento de la frecuencia. Actualmente este modelo de evolución está empezando a tocar techo. Muchos fabricantes optan ya por
aprovechar los últimos avances en miniaturización para incrementar el número de núcleos (cores) en
el chip. Pero muchos factores indican (referencias) que esta tendencia puede ir más allá invirtiendo el
ciclo de evolución actual en el diseño de los procesadores para favorecer el incremento del número de
cores por chip.
3
CAPÍTULO 2. MOTIVACIÓN
4
Dicho de una manera más sencilla, se diseñarán procesadores más sencillos (menos potentes)
y/o se incrementará la especialización: la integración de núcleos con un diseño especı́fico para rendir
mejor con un cierto tipo de aplicaciones. De hecho ya han salido al mercado procesadores que integran
la GPU (Graphics Processing Unit) en el mismo chip y otros, como el Cell/BE que usan procesadores
especializados para la computación intensa. Esto permitirá poner cada vez más procesadores por chip
y mejorar su rendimiento.
Pero la complejidad del diseño de arquitecturas más sofisticadas no es el único factor determinante
para este cambio de tendencia. También es importante tener en cuenta que estas arquitecturas implicaban un incremento del consumo mayor generación tras generación. Entonces gana importancia el
estudio de la relación Potencia de cálculo/Energı́a consumida.
Si se mantiene esta dinámica, según algunos expertos [3] [9], la tendencia será a incrementar de
manera casi exponencial el número de cores, poniendo a nuestro alcance chips con miles de cores
en unos pocos años. Este cambio de tendencia es de gran importancia en general y, concretamente
en el campo de la supercomputación, supone una revolución ya que las aplicaciones que se usan
actualmente en los supercomputadores están diseñadas y compiladas para los procesadores actuales.
Este cambio en la arquitectura de los ordenadores supondrá cambiar la manera de programar las
aplicaciones. Esto quiere decir que están por aparecer nuevos modelos de programación que permitan
sacar el máximo partido de las nuevas arquitecturas.
En las siguientes secciones se analizan con un poco más de profundidad los diferentes campos
implicados en la computación de alto rendimeinto. Con las aplicaciones, se crea la necesidad de tener
supercomputadores más potentes que permitan a los usuarios obtener resultados con más velocidad,
con mayor precisión, usando más informacion o haciendo gran cantidad de cálculos.
Para responder a esta necesidad, los fabricantes investigan y desarrollan las nuevas arquitecturas
teniendo en cuenta los factores más importantes como son potencia de cálculo y consumo energético.
Las más comunes en procesadores de última generación y que están marcando las tendencias actuales,
se presentan en la sección 2.3.
Como ya se ha comentado, con la aparición de los primeros supercomputadores formados por
redes de equipos de propósito general, se empezaron a desarrollar modelos de programación transparentes, que evitaban al usuario programador enredarse con problemas de comunicación y sincronización entre procesos o gestión de memoria. Los modelos más conocidos son OpenMP y MPI. Además,
con la aparición de nuevas arquitecturas y formas de aprovechar los recursos actuales para fabricar
supercomputadores, otros nuevos modelos están apareciendo y aparecerán durante los próximos años.
Estos y otros serán presentados y analizadas sus caracterı́sticas, diferencias y similitudes en la sección
2.4.
La última parte de este capı́tulo está dedicada a Fortan, lenguaje para el cual se ha desarrollado
el compilador de este proyecto. Se explica la historia de Fortran, su uso actual, su relación con la
2.2. APLICACIONES HPC
5
supercomputación y las razones por las que consideramos importante que den herramientas para poder
usar los nuevos modelos de programación en Fortran.
2.2. Aplicaciones HPC
No podrı́amos hablar de supercomputación, evidentemente, sin dedicar tiempo a las aplicaciones.
Desde un punto de vista de interés cientı́fico o industrial resulta muy interesante la construcción y
funcionamiento de supercomputadores y la aparición de nuevas arquitecturas. Pero todo ello viene
motivado por la necesidad de crear aplicaciones que permitan llevar a cabo cálculos de gran complejidad, o con grandes cantidades de datos, en periodos de tiempo razonables.
Desde su aparición, los ordenadores han sido utilizados para efectuar cálculos que a mano eran
mucho más lentos. Los ordenadores nos han ayudado a acelerarlos y a reducir, cuando no eliminar, la
cantidad de errores en los mismos. Hoy en dı́a los ordenadores hacen cálculos y simulaciones que a
nosotros nos costarı́a meses, años, siglos o mucho más hacer a mano.
Muchos de estos cálculos se podrı́an hacer sin problemas en un ordenador personal, sin embargo,
hay otras aplicaciones que requieren de los supercomputadores, por razones que ya se han comentado.
A continuación se enumeran, clasificados por disciplina, algunas de estas aplicaciones:
1. Ciencias Naturales
a) Meteorologı́a: Algoritmos de simulación de fenómenos meteorológicos a escala local y a
escala global.
b) Genética: Simulación evolutiva de genes.
c) Simulación de redes biomoleculares.
d) Dinámicas moleculares
2. Matemáticas
a) Reolución de ecuaciones diferenciales parciales
b) Obtención de valores propios
c) Demostración de teoremas
3. Industria
a) Diseño, cálculo de fuerzas
• Aeronáutica
• Ingenierı́a naval
• Automovilismo
CAPÍTULO 2. MOTIVACIÓN
6
b) Simulación de choque
c) Simulación de aerodinámica
d) Dinámica de fluidos en general
4. Fı́sica
a) Simulación de comportamientos electrónicos
b) Fenomenos astronómicos relacionados con las galaxias y estrellas
Esta lista muestra unos pocos ejemplos de aplicaciones, que gracias a los supercomputadores,
permiten obtener resultados con gran velocidad o de gran calidad. Ası́, se puede acelerar el diseño de
hélices, barcos, medicamentos. Avanzarnos al tiempo en el estudio del calentamiento global o conocer
mejor sus causas.
A pesar de la gran diversidad de aplicaciones y sus diferencias tanto a nivel algorı́tmico como de
volumen de datos operados o estructuras de datos usadas, dichos algoritmos y estructuras de datos se
pueden clasificar y encontrar elementos comunes entre diferentes aplicaciónes. Ası́, se puede definir
una clasificación de los diferentes subproblemas que componen las aplicaciones. A continuación se
muestra las 13 clases que se definen en [3].
1. Dense Linear Algebra
2. Sparse Linear Algebra
3. Spectral Methods
4. N-Body Methods
5. Structured Grids
6. Unstructured Grids
7. Combinational Logic
8. Graph Traversal
9. Dynamic Programming
10. Back-track and Branch+Bound
11. Graphical Models
12. Finite State Machine
2.3. ARQUITECTURAS
7
La mejora de los algoritmos que calculan estos subproblemas para las actuales y futuras arquitecturas, junto con el diseño de arquitecturas adecuadas a las estructuras de cálculo más comunes,
abren la posibilidad de mejorar el funcionamiento de las aplicaciones. Estas mejoras bien pueden ser
en cuanto a la precisión, que permitirı́a obtener resultados más fiables, en cuanto a la velocidad, esto
abrirı́a la posibilidad a ejecutar algunas de estas aplicaciones en tiempo real, en cuanto a la capacidad
de cálculo, el hecho de poder ejecutar aplicacions que hoy dı́a resulta imposible por la gran cantidad
de cálculos que requieren.
2.3. Arquitecturas
En este apartado veremos las diferentes tendencias actuales en cuanto a procesadores, por un lado,
y las arquitecturas más comunes en supercomputación. Durante las últimas décadas la tendencia en
arquitectura de computadores era crear procesadores cada vez más complejos. Desde la aparición de
los primeros procesadores de arquitectura segmentada, se fueron incorporando mejoras como cachés,
predictores de salto cada vez más complejos, ejecución fuera de orden, procesadores superescalares,
multithreading...
Todas estas mejoras, unidas a la capacidad de poner cada vez más transistores por procesador
y aumentar la frecuencia del reloj, implican un proceso de diseño mucho más complejo además de
problemas de consumo y disipación del calor. Como ya se ha comentado en la sección 2.1, la tendencia actual es a la integración de muchos procesadores por chip. Los conocidos actualmente como
multicores (ref GLOSARIO), podrı́an pasar a ser manycores llegando a componerse de decenas, cientos o, quién sabe, miles de procesadores.
Esto convierte cada chip en un pequeño supercomputador como los conocemos actualmente. Pero
esta capacidad para integrar tantos procesadores en un solo chip da lugar a nuevos problemas que,
hasta el momento, no habı́a hecho falta resolver: ¿Cómo interconectar tal cantidad de procesadores en
un chip?
Entre las respuestas a esta pregunta hay varias opciones. La tendencia actual es que sean pequeñas
máquinas SMP (Simmetric Multi-Processor) de manera que todos los núcleos verán la memoria de la
misma manera. Pero muchos arquitectos de computadores auguran que esta solución se convertirá en
el cuello de botella cuando se alcance un numero considerable de porcesadores por chip. Entre las
propuestas alternativas se rescata la idea de las memorias transaccionales [7].
También se abren nuevas posibilidades y campos de investigación y experimentación como las
arquitecturas heterogéneas. ¿Por qué no integrar procesadores diferentes de manera que cada uno
esté especializado para la tarea que se le encargue? En este sentido ya han aparecido algunas propuestas como el Cell/BE o chips con GPU integrada.
CAPÍTULO 2. MOTIVACIÓN
8
2.4. Modelos de Programación
Un modelo de programación es una herramienta (bien puede ser una forma de usar las herramientas propias del sistema operativo o una pieza software especı́fica) que permite el paso de aplicaciones
escritas siguiendo el modelo/filosofı́a natural del programador a aplicaciones adaptadas al hardware
disponible. Hay decenas de modelos de programación diferentes, cada uno de ellos es más o menos
adecuado a diferentes propósitos y arquitecturas. Sin embargo para este documento y el propósito del
proyecto nos interesan los modelos de programación centrados en el dominio de la supercomputación.
Hay que destacar la importancia de los modelos de programación por hacer de puente entre la
visión algorı́tmica y secuencial de los programadores y la realidad paralela de la supercomputación.
Esto es lo que estudiaremos a continuación, el paralelismo.
2.4.1.
Paralelismo
No todos los modelos de programación están orientados a la supercomputación ni su propósito
es el mismo. Los que nos ocupan, tienen como propósito explotar el paralelismo en las aplicaciones.
Pero para comprender esto es necesario entender a grandes rasgos qué significa esto de paralelismo.
En una aplicación, se pueden realizar diversas operaciones sobre datos. Es posible que, en un
cierto momento, algunas operaciones se pudieran realizar sobre ciertos datos en un orden arbitrario.
Independientemente del orden en que se han realizado esas operaciones el resultado es correcto. Esto
significa que si no importa el orden tampoco importarı́a que las operaciones tuvieran lugar al mismo
tiempo. Si podemos disponer de un equipo capaz de ejecutar estas operaciones a la vez, la aplicación
irá previsiblemente más rápido. Esto es paralelismo.
Para poder sacar partido del paralelismo potencial de una aplicación es necesario disponer de
recursos y mecanismos adecuados. Estos mecanismos son los modelos de programación y los recursos
son los supercomputadores y procesadores multi/many core.
Llegados a este punto es un buen momento para introducir el concepto de escalabilidad. La escalabilidad es la propiedad de explotar el paralelismo en mayor grado mediante el incremento de
recursos sin perder eficiencia (relación poténcia de cálculo/cálculo efectivo). Es decir, si tenemos una
aplicación que hace 100.000 operaciones independientes que tardan un tiempo t en realizarse en un determinado procesador, la aplicación tardarı́a 100.000t en ejecutarse. Con un modelo de programación
que permitiera explotar este paralelismo podrı́amos ejecutarla con 2 procesadores y potencialmente
tardarı́a 50.000t para cada mitad, y 50.000t en total, puesto que se ejecutan a la vez. Podrı́amos usar
el doble de procesadores y tardar 25.000t, y ası́ sucesivamente.
Sin embargo, las arquitecturas y los modelos de programación implican un coste. Hay que gestionar el reparto de trabajo entre los procesadores. Además, la escalabilidad depende también de las
2.4. MODELOS DE PROGRAMACIÓN
9
propiedades de la aplicación, no todas las aplicaciones tienen un paralelismo implı́cito claro y hay momentos en que requiere disponer de diversos resultados para una determinada operación. Esto último
se conoce como sincronización.
En los siguientes apartados se explican OpenMP y MPI, los principales modelos de programación
en supercomputación. Se analizan sus principales caracterı́sticas, ventajas y desventajas. También se
presentan otros modelos emergentes, entre ellos algunos desarrollados en el BSC, incluidos CellSs y
SMPSs.
2.4.2.
OpenMP
Orientado a sistemas de memoria compartida, OpenMP es uno de los modelos de programación
más importantes en la actualidad para la supercomputación. OpenMP sigue el modelo fork-join, heredado de los sistemas unix. El usuario (programador) decide qué partes del código esconden paralelismo y se lo indica ası́ al modelo. La aplicación inicia varias hebras - o threads- (fork) y asigna la
ejecución de un trozo de estas partes a cada una de ellas. Una vez terminadas éstas las operaciones de
cada hebra, se sincroniza (join) de manera que la ejecución sigue el curso secuencial de la aplicación
original.
El modelo funciona mediante anotaciones en el código, comentarios que permiten indicar qué partes son paralelizables. OpenMP permite explotar el paralelismo en bucles, dividiéndolos en varios que
operan partes de los datos. También permite indicar secciones (sections) que se corresponden a trozos
de código separados que se pueden ejecutar a la vez. Las últimas versiones de OpenMP incluyen una
forma de paralelismo a nivel de tarea.
OpenMP se ofrece para C, C++ y Fortran. Su sencillez y portabilidad, gracias al soporte de muchos
compiladores, han hecho de él un modelo de programación paralela y para supercomputación de gran
importancia. Sin embargo, entre sus limitaciones está el hecho de ser un modelo para sistemas de
memoria compartida puesto que los grandes supercomputadores están formados por redes de equipos
independientes. Además, en OpenMP el paralelismo es explı́cito, el programador tiene que especificar
qué partes del código pueden ser ejecutadas en paralelo.
2.4.3.
MPI
Message Passing Interface, forman las siglas de este modelo basado en la comunicación entre
procesos. MPI es un estándar que define unas interfaces y bibliotecas que permiten crear aplicaciones
para ser ejecutadas en sistemas distribuidos. Usando las interfaces definidas por MPI, se pueden crear
varios procesos y decidir cómo se envı́an los datos entre ellos, cuándo se sincronizan y qué tipo de
sincronización tienen.
MPI es muy útil para grandes supercomputadores y redes de ordenadores distribuidas porque
CAPÍTULO 2. MOTIVACIÓN
10
ofrece un mecanismo transparente para comunicar procesos que se están ejecutando en varios sistemas
como una sola aplicación. Otra caracterı́stica es que MPI permite la ejecución de aplicaciones en redes
heterogéneas, que tienen sistemas con propiedades diferentes.
La principal desventaja de MPI respecto otros modelos como OpenMP es la mayor dificultad de
uso puesto que el programador se responsabiliza directamente de la sincronización y del reparto del
trabajo entre los procesos.
2.4.4.
Otros modelos de programación
OpenMP y MPI, se caracterizan por ser dos modelos de ámbito muy general. Dirigidos cada uno
a su tipo de sistema (memoria compartida y sistemas distribuidos, respectivamente), no están creados
especı́ficamente para ninguna arquitectura, lenguaje, o aplicación en concreto. Los modelos que se
presentan es este apartado no son tan generales.
GRID Superscalar Desarrollado en el BSC, es un modelo orientado a redes GRID. GRID Superscalar explota el paralelismo a nivel de tarea (task), es decir, el usuario encapsula el código que hace
los cálculos en unidades aisladas conocidas como tareas, cada instancia de una tarea lleva a cabo una
parte de las operaciones finales. Las tareas operan sobre unos ciertos datos de manera que se crean
dependencias entre las tareas.
CellSs y SMPSs
Son los modelos de programación objeto de este proyecto. Herencia de GRID Su-
perscalar, están orientados a explotar el paralelismo a nivel de tarea pero, en este caso, especı́ficamente
para el Cell/BE (en el caso de CellSs) y para arquitecturas SMP (Simmetric Multi-Processor), en el
caso de SMPSs. Serán estudiadas en detalle en el capı́tulo 4.
HPF O High Performance Fortran es una propuesta para incorporar un conjunto de ampliaciones en
Fortran 90 que permitan explotar el paralelismo de manera nativa en el lenguaje.
Datacutter Se trata de un modelo de programación pensado para procesar las grandes cantidades de
datos producidas por herramientas de medición o captación como pueden ser microscopios electrónicos o sincrotrones en redes GRID.
CUDA Cuda es un modelo de programación especı́ficamente diseñado por los ingenieros de nvidia(poner una ref al paper que presente CUDA) para sacar partido de la gran capacidad de cálculo de
las GPUs (Graphics Processing Unit) de las tarjetas nvidia. Esta firma monta equipos de supercomputación (Tesla) basados en tarjetas gráficas de alta gama.
2.5. FORTRAN
11
Figura 2.1: Targeta perforada con una instrucción Fortran
Además de nvidia, otros fabricantes de hardware como intel o IBM, desarrollan modelos de programación o ampliaciones a modelos existentes especı́ficos para su hardware.
2.5. Fortran
Este apartado está dedicado a Fortran, el lenguaje de programación al que va dirigido el compilador desarrollado en este proyecto. El objetivo es conocer mejor el lenguaje, su historia y sus
caracterı́sticas para explicar las razones que nos llevan a desarrollar un compilador para Fortran.
2.5.1.
Historia de Fortran
Fortran o, más bien, FORTRAN, fue el primer lenguaje de programación de alto nivel que tuvo
éxito. Creado por un equipo de IBM liderado por John W. Backus a ,mediados de los años 50. Su
nombre viene de las palabras “Formula Translation” y fue concebido para producir código máquina
eficiente para los ordenadores 704 de IBM.
El lenguaje proporcionaba un nivel de abstracción respecto a la arquitectura del computador que
permitı́a al programador olvidarse de los registros y la memoria. La idea demostró ser muy útil puesto
que resultaba mucho más fácil programar. Ası́, aplicaciones de control de reactores nucleares fueron
escritas de manera rápida por fı́sicos nucleares que poco sabı́an de informática y programación.
De FORTRAN I a Fortran 66 En pocos años se fueron introduciendo notables mejoras a la primera versión, FORTRAN I. La siguiente versión, FORTRAN II (1958), permitió la compilación de
’modules’ por separado y el uso de módulos de código ensamblador.
En 1961 fue publicado Fortran IV, después de saltarse FORTRAN III, se reunieron suficientes
CAPÍTULO 2. MOTIVACIÓN
12
mejoras como para sacar esta nueva versión que mejoró además su independencia de la arquitectura.
En el año 1962 se inició por parte de un comité de la ASA (American Standard Association), la
redacción de un estándar para FORTRAN que desembocarı́a en la versión Fortran 66, primera versión
estándar del lenguaje. Fue entonces cuando empezó a llamársele Fortran.
Fortran 77
La siguiente versión de Fortran y que tuvo una gran extensión fue Fortran 77. Las
principales razones de su gran popularización fueron el gran periodo de tiempo que el estándar estuvo
vigente, por un lado, y la progresiva disminución del coste de los equipos informáticos que permitieron
a pequeñas empresas y universidades acceder a equipos y desarrollar sus propios programas, por el
otro.
Entre las mejoras de Fortran 77 podemos destacar:
• Bucles DO con ı́ndice decreciente.
• Estructuras condicionales con bloques: IF() THEN... ELSE ... ENDIF.
• Bucles DO con pre-test.
• Tipo de datos CHARACTER, permitı́a una representación alfanumerica explı́cita.
• Constantes CHARACTER string: ’ABCD’.
• Finalización del programa principal sin la instrucción STOP.
Fortran 90 Esta renovada versión supuso una revolución en la forma del lenguaje y, a pesar de las
dificultades que aún hoy tienen muchos programadores para adaptarse, incluye muchas mejoras que a
la larga están suponiendo la imposición del nuevo lenguaje respecto a versiones anteriores. La razón
es que se han eliminado algunas caracterı́sticas arcaicas y que limitaban la flexibilidad del lenguaje y
dificultaban la legibilidad del código.
Las mejoras introducidas en esta relativamente reciente versión fueron las siguientes:
• Free format source code: Fortran 77 mantenı́a las restricciones de formato en el código fuente
(Fixed form) originarias de cuando se escribı́an los programas en tarjetas perforadas.
• Estructuras de control actuales (CASE, DO-WHILE).
• Derived Types: Tipos de datos definidos por el usuario, permiten encapsular datos de diversos
tipos en variables para representar un nuevo tipo.
• Mejoras en la notación de los arrays (vectores).
2.5. FORTRAN
13
• Secciones de arrays.
• Operadores entre arrays.
• Memoria dinámica.
• Parámetros con nombre.
• Especificación de la direccionalidad de los parámetros a subrutinas (INTENT).
• Control de la precisión y rango de las variables.
• Módulos.
La siguiente versión, Fortran 95 añadió algunas mejoras menores que completaban la potencia del
lenguaje.
Fortran 2003 Cuando todavı́a no ha sido completamente adoptado Fortran 95, ya ha sido publicado
el estándar 2003, que añade algunas caracterı́sticas como la programación orientada a objetos con
la intención de mantener Fortran actualizado con caracterı́sticas de lenguajes comunes como C++ y
JAVA.
Además, con Fortran 2003 se estandariza la interacción del lenguaje con C. Otra mejora importante es el soporte de excepciones permitiendo hacer aplicaciones más robustas.
Sin embargo, y a pesar de que esta versión de Fortran tiene muchas mejoras atractivas, gran parte de los usuarios están todavı́a estancados en Fortran 77 y el resto utilizan Fortran 90/95. Por ello
muchos proveedores de compiladores se han centrado en mejorar sus compiladores para versiones
anteriores, centrándose cada vez más en Fortran 95 y han integrado algunas de las mejoras más importantes de Fortran 2003 sólo como ampliaciones.
2.5.2.
Caracterı́sticas de Fortran
Fortran fue diseñado por un equipo de programadores cuyo objetivo era reducir el coste de desarrollo de programas para el ejército y la investigación. De modo que fue pensado para poder expresar
fórmulas matemáticas de manera más natural. Los programadores no tenı́an que preocuparse de hacer
los ’loads’ necesarios para hacer unas ciertas operaciones, por ejemplo. Además, los primeros compiladores de Fortran fueron desarrollados con la meta de que los programas generados fueran al menos
tan rápidos como los hechos a mano (en código ensablador).
Una de las caracterı́sticas de Fortran es la gran variedad de intrinsic functions que permiten hacer
muchas y muy diferentes operaciones numéricas y de otros tipos. La importancia de estas funciones
radica en que son traducidas directamente por el compilador como una serie de instrucciones que
CAPÍTULO 2. MOTIVACIÓN
14
realiza la operación especificada. Se ahorra ası́ al programador muchas lı́neas de código y permite que
el código resultante sea óptimo.
Otro de los aspectos que caracteriza Fortran, y se puede considerar la mayor diferencia con los
lenguajes de alto nivel pensados para desarrollar sistemas (C), es la ausencia de punteros. Hasta la
versión Fortran 95, no se dispuso de ningún mecanismo de referenciar variables usando otras y, aunque
en las versiones actuales existe, sus limitaciones los hacen muy poco útiles.
También en este sentido Fortran se distingue de muchos lenguajes de alto nivel por su carencia
de contexto global. En Fortran, cada programa, subrutina o función, es una unidad independiente y
aislada. Esto quiere decir que no hay ninguna entidad superior externa a un programa o subprograma
que indique que existe otra entidad de las anteriores.
En cuanto al estilo, como ya se ha comentado, Fortran todavı́a permite el formato fixed source
form Además, el estándar no distingue entre mayúsculas y minúsculas en el código.
El documento Fortran 90 tutorial [8] es un pequeño tutorial de programación en Fortran que
puede servir como iniciación a los que desconozcan el lenguaje y aporta algunos consejos sobre estilo
e indicaciones sobre cómo programar correctamente aplicaciones en Fortran.
2.5.3.
Uso actual de Fortran
En las últimas décadas Fortran se ha visto desplazado por los nuevos lenguajes de programación
en muchos campos. Sin embargo, hay aún muchos biólogos, quı́micos, matemáticos, etc., que usan
Fortran. Muchas universidades no técnicas explican Fortran como lenguaje para expresar programas
que resuelven problemas de otros ámbitos (no informáticos). Fortran resulta cómodo y fácil de aprender.
La comunidad cientı́fica aporta una parte importante de los usuarios de los supercomputadores.
Ası́, muchas de las aplicaciones de simulación y análisis y sı́ntesis de datos están escritas en Fortran.
De ahı́ la necesidad de que los proveedores de compiladores de Fortran ofrezcan soporte a los modelos
de programación actuales, esos modelos que permiten el funcionamiento adecuado de las aplicaciones
en las arquitecturas actuales.
Tanto OpenMP como MPI se pueden ejecutar sin problemas con aplicaciones escritas en Fortran
con la mayorı́a de compiladores, en el mundo de la HPC. Los proveedores de compiladores e investigadores que desarrollan nuevos modelos de programación mantienen el interés en Fortran ya que el
coste de reescribir muchas de las aplicaciones existentes en otro lenguaje de programación que no sea
Fortran serı́a mucho mayor.
2.6. CONCLUSIONES
15
2.6. Conclusiones
Una de las principales funciones del BSC es ofrecer a la comunidad cientı́fica y industrial servicios
de supcomputación, poniendo a disposición de los usuarios los recursos y mecanismos necesarios
para llevar a cabo sus investigaciones y actividades. Esto también supone la necesidad de innovar y
mejorar dichos servicios. La tecnologı́a, los ordenadores, puede cambiar con mucha más velocidad de
lo que lo hacen las técnicas y programas usados por los cientı́ficos y ingenieros. Por ello es necesario
ofrecer modelos de programación cómodos que permitan a los usuarios mejorar el rendimiento de sus
aplicaciones sin tener que cambiar sus programas notablemente.
En este sentido, pasar de Fortran a otros lenguajes de programación supondrı́a muchos problemas
a muchos usuarios que crean aplicaciones usando módulos ya hechos en Fortran o tienen sus librerı́as
y algoritmos pensados y escritos en Fortran. Es importante, pues, que las novedades en cuanto a
modelos de programación se refiere, que el BSC ofrece, den soporte a Fortran.
16
CAPÍTULO 2. MOTIVACIÓN
Capı́tulo
3
Objetivos del proyecto
CellSs y SMPSs son dos modelos de programación creados por el equipo del BSC. En el caso de
CellSs, el objetivo es ofrecer una manera fácil de programar el Cell/BE (procesador de STI, ref a STI).
Esta necesidad surge dada la complicada arquitectura del Cell/BE. SMPSs es una generalización de la
idea a procesadores multicore de arquitectura SMP, ver 2.3.
El objetivo principal de este proyecto es desarrollar un compilador de Fortran para CellSs/SMPSs,
de manera que los usuarios del BSC, y la comunidad cientı́fica en general, que utilizan Fortran puedan sacar partido de la capacidad de CellSs de aprovechar la gran potencia de cálculo que ofrece la
arquitectura del Cell/BE, en el caso de CellSs, y permitir también que se puedan crear aplicaciones en
este lenguaje para sistemas SMP usando el modelo de programación SMPSs.
A continuación se explican las tareas que hay que completar para alcanzar este objetivo. En esta lista se mencionan conceptos que no han sido explicados todavı́a. En los capı́tulos siguientes se
tratan con profundidad estos aspectos y se justifica, esperamos, la necesidad de completar las tareas
especificadas.
3.1. Tareas
• Definir la sintaxis de CellSs/SMPSs en Fortran: Especificación de cómo se escriben las anotaciones que marcarán las tareas y controlarán la ejecución del programa. Hay que estudiar cuidadosamente los pormenores de Fortran para saber en qué condiciones se han de usar las anotaciones y qué restricciones hay. Por ejemplo si se pueden especificar rangos dentro de arrays
pasados o si está permitido el uso de estructuras Fortran (tipos derivados) como parámetros.
• Estudiar el entorno: Engloba el estudio de los compiladores disponibles para trabajar; el compilador de CellSs/SMPSs para C, que servirá como referencia en el desarrollo del de Fortran,
17
CAPÍTULO 3. OBJETIVOS DEL PROYECTO
18
como filosofı́a de la estructura del compilador se usará de base el compilador “mcxx”, que
divide la compilación posterior al análisis en varias fases. Finalmente, el compilador de Fortran mf95 servirá de base para la parte de análisis del código. Esta sección incluye también el
entorno de trabajo sobre el que se llevará a cabo el proyecto.
• Especificar el comportamiento del compilador: Mediante la escritura manual de ejemplos que
simulen ser código generado por el compilador que se va a implementar.
• Desarrollo del compilador de Fortran para CellSs/SMPSs: Incluye fases de diseño interno e
implementación del compilador. A continuación se muestra una lista de las subfases del desarrollo:
1. Controlador principal de la compilación.
2. Generación del árbol de representación del código reconociendo las anotaciones y tratando
las posibles ambiguidades.
3. Completar con información semántica cada una de las “program units”, unidades mı́nimas
de código en Fortran.
4. Aplicación de las transformaciones de código necesarias para que la “program unit” sea
compilada para CellSs/SMPSs.
5. Generación de los ficheros necesarios para la compilación final (en el caso de CellSs se
han de separar por un lado las “tasks” (tareas) y las funciones/subrutinas a las que éstas
invocan y, por otro lado el resto de program units).
6. Añadir soporte a tipos de datos diferentes de los de C. Durante las etapas anteriores se
asumı́a el uso de tipos equivalentes a los tipos estándar de C. En fortran los tipos varı́an
(en tamaño y semántica) en función del compilador e incluso del propio código a la hora
de declarar una variable.
7. Añadir soporte a tipos derivados.
8. Alinear las variables del usuario, soporte a memoria ALLOCATE, en CellSs.
9. Soporte a extensiones propias del compilador de IBM(XLF) en CellSs. Ver si hay alguna
extensión especı́fica de gfortran (compilador de Fortran de GNU) u otros para SMPSs.
10. Soportar compilación de varios ficheros fuente.
• Pruebas y ejemplos: Elaboración de códigos que demuestren el correcto funcionamiento del
compilador. Comprobar el funcionamiento en casos crı́ticos y con entradas erróneas. Estudiar
el rendimiento del compilador.
• Redacción del manual de usuario de CellSs/SMPSs para Fortran.
• Documentación: Elaboración de la memoria del proyecto.
3.1. TAREAS
19
Hay que destacar que éstas son las tareas que se pensó eran necesarias para cumplir los objetivos
del proyecto en el momento en que éstos fueron definidos. Algunas de ellas han perdido importancia o
han sido replanteadas cuando se ha profundizado en el estudio del modelo y en los problemas técnicos
encontrados durante el desarrollo. En el capı́tulo 9 se analiza el cumplimiento de los objetivos y se
justifican los cambios mencionados con respecto a las tareas definidas.
20
CAPÍTULO 3. OBJETIVOS DEL PROYECTO
Capı́tulo
4
CellSuperScalar/SMPSs
CellSuperScalar/SMPSs, conforman un modelo de programación basado en GridSuperscalar*,
pero pensados para procesadores multicore. El objetivo principal del modelo es facilitar la programación en estas arquitecturas para hacer más cercanos los recursos de la informática en general y de la
supercomputación en concreto, a programadores no especializados.
Gracias a ello, el programador puede desentenderse de los detalles referentes a la arquitectura del
sistema en que ejecutará la aplicación. Esto implica una programación mucho más sencilla puesto que
no tiene que enfrentarse a problemas tales como la gestión de memoria o comunicación entre threads.
4.1. Modelo de Programación
El objetivo de CellSs/SMPSs es proporcionar una forma fácil de expresar aplicaciones con paralelismo de manera secuencial. Esto es, una aplicación o programa que efectúa una serie de cálculos
de uno en uno y en un orden determinado.
El modelo está pensado desde el punto de vista de la encapsulación. Un concepto completamente
adquirido por los programadores expertos y fácil de entender por los que no lo son tanto. La idea
consiste en determinar partes del código que pueden ser ejecutadas de manera simultánea y ponerlas
en una función a la que llamaremos tarea (en inglés TASK).
El programa principal (master thread) registra las tareas en un grafo donde se gestionan las dependencias de datos (ver más adelante) se ejecutan en las otras CPUs (worker threads) en cuanto sea
posible.
Para acabar de comprender la idea puede servir de ayuda verlo con un ejemplo. A continuación se
muestra una operación de multiplicación de matrices a bloques.
21
CAPÍTULO 4. CELLSUPERSCALAR/SMPSS
22
Figura 4.1: Multiplicación de matrices a bloques.
La figura 4.1 muestra una matriz de reales dividida en bloques. Para completar una multiplicación
de matrices usando bloques se aplican las mismas operaciones que en una multiplicación de matrices
normal pero con los bloques como elementos.
A continuación se muestra el código en Fortran que realiza la multiplicación de matrices a bloques.
La subrutina “block madd” se encarga de multiplicar el bloque de A indicado por el de B y lo acumula
en un determinado bloque de C.
subroutine matmul()
...
do ii=1, N, BSIZE
do jj=1, N, BSIZE
do kk=1, N, BSIZE
call block_madd(A(ii,kk), B(kk,jj), C(ii,jj))
end do
end do
end do
end
subroutine block_madd(A, B, C)
integer, parameter :: BSIZE = 32
real :: A(BSIZE), B(BSIZE, C(BSIZE)
integer i, j, k
do i=1, N, BSIZE
do j=1, N, BSIZE
do k=1, N, BSIZE
C(i,j) = A(i,k)*B(k,j) + C(i,j)
end do
end do
end do
end
4.1. MODELO DE PROGRAMACIÓN
23
Figura 4.2: Cadenas de tareas en la multiplicación de matrices a bloques.
En este caso se identificarı́a como tarea la subrutina “block madd:”. Estas tareas se ejecutarán
cuando el runtime considere que están listas y haya recursos (CPUS) disponibles para ello. En ese
momento se proporcionarán los parámetros de la tarea a ejecutar y se invocará a la rutina. Por lo que
a la disponibilidad de los resultados respecta, se comentará más adelante.
En cuanto a los parámetros podemos observar que A y B sólo son accedidos para usar los valores
que contienen. El bloque de C, mientras, usa sus datos tanto para leer datos de los cálculos anteriores
como para escribir los resultados. Esto determina la dirección de los parámetros. A y B se consideran
parámetros de entrada (IN) mientras que C es un parámetro de entrada y salida (INOUT). Si en una
tarea se escribe en un parámetro sin tener en cuenta la información anterior, se considera parámetro
de salida (OUT). Estas direcciones sirven para determinar las dependencias de datos entre tareas.
Como se puede ver, si seguimos la ejecución secuencial del bucle, durante todas las iteraciones
del bucle más interno las variables “ii” y “jj” no varı́an. Esto quiere decir que en todas las llamadas a
“block madd” de esta iteración ii, jj, irán acumulando sus resultados en el mismo bloque de C.
Entonces se genera una cadena de tareas dependientes, figura 4.2. Los datos calculados por una
son usados por la siguiente para hacer sus operaciones. Éste es el grafo de dependecias entre las tareas
para esta aplicación.
Ası́, si disponemos de 4 CPUs, en este caso, las tareas independientes puedes ser ejecutadas simultáneamente. Las bibliotecas de CellSs/ SMPSs se encargan de gestionar qué tareas han sido ya
ejecutadas y cuáles están disponibles. El programador invoca las tareas en el orden en que las hubiera
invocado en una arquitectura con un sólo procesador. El modelo se encarga de o ejecutar una tarea
hasta que todos los datos que ésta tiene como entrada hayan sido calculados y estén disponibles.
Para resumir el funcionamiento de una aplicación CellSs/SMPS, podrı́amos distinguir las siguientes fases de ejecución tı́picas en una aplicación con este modelo.
En primer lugar está la inicialización, como en toda aplicación hay que leer o generar los datos y
preparar las estructuras de datos para la ejecución del algoritmo.
CAPÍTULO 4. CELLSUPERSCALAR/SMPSS
24
Luego empieza la aplicación en sı́, el programa va invocando tareas. En algún momento es posible
que sea necesario establecer puntos de sincronización, esperar a que ciertas tareas terminen para hacer
una operación concreta con los datos producidos por éstas y continuar. El modelo proporciona dos
mecanismos para hacer esto:
• Barrier: espera la finalización de todas las tareas.
• Wait on: el usuario indica qué datos han de estar calculados antes de continuar la ejecución del
programa (las tareas, por supuesto, siguen ejecutandose).
Finalmente termina la ejecución con CellSs/SMPSs, todas las tareas se han ejecutado y los resultados están disponibles para ser escritos, procesados por otra aplicación etc.
CellSs/SMPSs se compone de un compilador “source-to-source” ( transforma código fuente en
código fuente y lo compila con un compilador tradicional) y una biblioteca. El compilador interpreta
las anotaciones en el código que dan indicaciones sobre las tareas y el flujo de ejecución.
La biblioteca gestiona las tareas durante la ejecución. Su funcionamiento es diferente entre CellSs
y SMPSs. Estas diferencias se pueden ver en las secciones 4.3 y 4.4.
4.2. Arquitectura de una aplicación CellSs/SMPSs
Desde el punto de vista del programador, CellSs y SMPSs funcionan de la misma manera. La
única diferencia destacable es que en el Cell/BE la memoria de las SPEs es limitada, cosa que hay que
tener en cuenta a la hora de elegir los tamaños de los parámetros de las tareas.
Para escribir una aplicación CellSs/SMPSs, es necesario indicar mediante anotaciones qué funciones son tareas. Igualmente se usan anotaciones para indicar dónde se inicia la ejecución del program
CellSs o SMPSs y cuándo finaliza.
Las anotaciones sirven además para establecer puntos de sincronización parcial o total sin finalizar
la ejecución de la aplicación CellSs, ası́ se garantiza que unos ciertos datos ya han sido calculados y
están disponibles para ser usados por el master thread.
El compilador transforma las anotaciones en llamadas a la biblioteca del master. La biblioteca
inicializa las listas y el grafo de dependencias y se registran las tareas que hay en la aplicación.
Las llamadas a funciones marcadas como tareas son reemplazadas por invocaciones a la función
“AddTask”, de la biblioteca, que es la que registra la información a cerca de la tarea en el grafo de
dependencias.
Es necesario un mecanismo mediante el cual la biblioteca pueda invocar a las funciones marcadas
como tareas. Para ello se utilizan adaptadores (task adapters, 4.3). Se trata de funciones conocidas por
la biblioteca que se implementan en tiempo de compilación de manera que invoquen a las tareas.
4.3. RUNTIME DE CELL SUPERSCALAR
25
Figura 4.3: Adaptadores para invocar a las tareas desde la biblioteca.
4.3. Runtime de Cell Superscalar
CellSuperScalar está especı́ficamente diseñado para el procesador Cell/BE de IBM (STI, Sony,
Toshiba e IBM). Para comprender cómo funciona una aplicación CellSuperScalar, hay que conocer más o menos el funcionamiento del procesador. El Cell/BE (Cell Broadband Engine, figura 4.4)
está compuesto de una unidad PowerPc (PPE) conectada directamente a la memoria principal. Además
cuenta con 8 unidades SIMD (SPE) con 256KB de memoria propia y un bus DMA (Direct Memory
Access) que los interconecta con la memoria principal.
Figura 4.4: Arquitectura del Cell/BE.
La figura 4.5 muestra un sencillo esquema del funcionamiento de CellSuperScalar.
Master thread
Las aplicaciones CellSs están formadas por varios hilos o threads. Por una parte está el master
thread. Se ejecuta en la PPE y su función es registrar las tareas añadidas y gestionar las dependencias
de datos usando el grafo de dependencias.
Algunas dependencias de datos se eliminan mediante renombrado (renaming) de parámetros. Esto
CAPÍTULO 4. CELLSUPERSCALAR/SMPSS
26
Figura 4.5: Funcionamiento de CellSs.
permite que tareas que en principio tenı́an una dependencia directa o indirecta (a través de otras
dependencias) se pueden ejecutar a la vez ya que aquella dependencia se ha eliminado haciendo que
utilicen parámetros diferentes.
El master thread también es el encargado de gestionar la sincronización de la aplicación cuando
el usuario pone una directiva para esperar datos (“wait on”), o una barrera (“barrier”).
Helper thread
Aparte del master thread, en la PPE se ejecuta otro hilo, el Helper thread. Su trabajo es el de
decidir cuándo una tarea puede ser ejecutada y controla la ejecución de las tareas en las SPEs. Para
este propósito se utiliza la siguiente polı́tica de planificación: Una tarea puede ser ejecutada si sus
predecesores en el grafo de dependencias han terminado su ejecución.
Para reducir el impacto de tener que transferir los parámetros de memoria principal a las SPEs por
DMA, las tareas que forman ’tiras’ en el grafo de dependencias se ejecutarán en la misma SPE. De
esta manera que los datos producidos por una tarea no necesitan ser transferidos a memoria principal
porque el siguiente en usarlos será la próxima tarea a ejecutar en esa SPE.
También se explota la localidad de datos guardando los resultados de las tareas en la memoria de
las SPEs y ejecutando tareas que usan esos datos en las SPEs que los contienen.
El helper thread también se encarga de la sincronización entre el master y los worker threads
siendo quien informa a las SPEs de las tiras de tareas que tienen que ejecutar, su longitud e información
relacionada con los parámetros de las tareas.
Worker threads
El código correspondiente a las tareas se compila a parte del programa principal. Éste se enlaza
con una biblioteca que contiene el programa que se ejecutará en las SPE (el worker thread). Este
último programa espera a encontrar tareas disponibles para ejecutar en el espacio de sincronización
4.4. RUNTIME DE SMP SUPERSCALAR
27
(memoria de la PPE).
Cuando el programa que se ejecuta en las SPEs encuentra una tarea (o tira de tareas) disponible,
copia los parámetros (de entrada) de la tarea a la memoria de la SPE y se lanza la ejecución del
código del usuario (la tarea propiamente dicha). Posteriormente copiará (usando DMA) la información
generada por la tarea (parámetros de salida) a memoria principal. Luego indicará a la PPE que la
ejecución de aquella(s) tarea(s) ha terminado.
4.4. Runtime de SMP Superscalar
Para las arquitecturas SMP (ver 2.3) las bibliotecas funcionan de una manera diferente a como lo
hacen el el Cell/BE. Estas diferencias justifican que se haga una distinción entre SMPSs y CellSs ya
que el funcionamiento para arquitecturas de memoria compartida es diferente.
Otra de las principales diferencias entre las arquitecturas SMP y el Cell/BE es que la arquitecura es
homogénea, lo cual permite que no haya que compilar las tareas y el programa principal por separado.
Además, al tener memoria compartida no son necesarias las transferencias de parámetros puesto que
todos los worker threads verán la misma memoria.
Una aplicación SMPSs se ejecutará en un sistema usando tantos procesadores/cores como el sistema le permita (o el usuario haya definido). Para conseguirlo se usa la ayuda de una biblioteca que
inicia los threads de cada procesador, conocidos como “worker threads”.
El programa principal se encarga, como en CellSuperScalar, de registrar las tareas que se van
invocando en un grafo para mantener las dependencias. También guarda los datos relativos a las tareas
tales como parámetros, tamaño y dirección de estos. Los worker threads se encargan de esperar tareas
disponibles y cogerlas para ejecutarlas. A diferencia de CellSuperScalar, todos los threads colaboran
para determinar qué tareas están listas para ser ejecutadas.
Cuando el programa principal alcanza un punto de sincronización (barrier o wait on) éste debe
esperar a que ciertas (o todas) las tareas hayan finalizado su ejecución. En ese momento se comporta
como un “worker thread” consumiendo tareas disponibles hasta que la ejecución antes interrumpida
ya pueda continuar. De esta manera, el programa principal colabora en la ejecución de la aplicación.
28
CAPÍTULO 4. CELLSUPERSCALAR/SMPSS
Capı́tulo
5
Fortran para CellSs/SMPSs
Durante todo el documento se ha hablado de anotaciones en el código, transformaciones de código, de funciones marcadas como tareas, de registrar tareas etc. Sin embargo no se ha explicado cómo
se hace esto exactamente. El compilador se encarga de hacer estas transformaciones.
En esta sección se tratan dos aspectos básicos del funcionamiento de CellSs/SMPSs en Fortran.
Por un lado la definición de la sintaxis del lenguaje (en este caso por lenguaje nos referimos a las
anotaciones de CellSs) para Fortran. Y por otra parte se trata el problema de la interacción de las
aplicacies CellSs/SMPSs con la biblioteca. Estas dos partes se corresponden a dos fases del proceso
de compilación de un fichero Fortran.
La primera tiene que ver con la fase de análisis. El compilador ha de comprobar que las anotaciones introducidas por el usuario corresponden a directivas existentes en el lenguaje.
La segunda parte está relacionada con la parte de generación de código (XXX). El compilador
tiene que generar código compatible con las bibliotecas de CellSs. Las bibliotecas están escritas en
C, por lo que se ha definido en ellas una interfaz especı́fica para Fortran que permite acceder a sus
funcionalidades de éstas desde un programa escrito en Fortran.
5.1. Definición de la sintaxis del lenguje para fortran
Ya se ha dicho que las anotaciones son comentarios en el código que un compilador normal pasarı́a
por alto pero que, un compilador especı́fico es capaz de interpretar para cambiar el curso normal de la
compilación mediante transformaciones en el código. Ası́, una aplicación con anotaciones puede ser
compilada normalmente con un compilador comercial o opensource y su comportamiento tendrı́a que
ser el esperado.
Este sistema de modificar el código del usuario para adaptar aplicaciones a arquitecturas multi29
CAPÍTULO 5. FORTRAN PARA CELLSS/SMPSS
30
procesador o multicore no es nuevo, como se explica en la sección 2.4. El modelo de programación
OpenMP lo usa desde ya hace años. Muchos compiladores proporcionan también técnicas para que el
usuario pueda introducir indicaciones sobre cómo debe ser compilada una cierta parte de código.
El compilador de CellSs/SMPSs, entonces, interpreta las anotaciones y genera una versión del
código del usuario modificada mediante la sustitución de las anotaciones por código que realiza las
operaciones deseadas. Ası́ el usuario programador se preocupa de qué hace la aplicación y no de cómo
lo hace.
Muchos programadores, sobre todo si tienen algo de experiencia en el campo de la supercomputación, tienen ya algo de experiencia con las anotaciones OpenMP. Por ello, se decidió en su momento
que las anotaciones para CellSs/SMPSs tendrı́an la misma forma que las anotaciones OpenMP, de
manera que los potenciales usuarios serı́an más receptivos a algo que para ellos ya es conocido.
En cuanto a Fortran, hay que decidir que versión estándar de Fortran usar en CellSs/SMPSs; la
decisión tomada es soportar Fortran 95. La razón básica es que el compilador original, en el que
se ha basado el desarrollo del compilador para CellSs/SMPSs 6.5, soporta esta versión de Fortran.
Pero la decisión no es arbitraria, Fortran 95 es la versión más moderna que soportan la mayorı́a de
compiladores de Fortran.
Esto introduce alguna limitación a los usuarios acostumbrados a programar en versiones anteriores
a Fortran 90. Pero el lenguaje proporciona herramientas muy útiles para aportar información que
CellSs necesita de manera explı́cita (como por ejemplo la dirección de los parámetros). Para conocer
las diferencias entre Fortran 95 y otras versiones ver 2.5.
La diferencia principal, y más destacable en este punto por ser la que puede afectar a más programadores, es el hecho de que a partir de Fortran 90 se eliminan las limitaciones de Fortran 77 y
anteriores en cuanto al formato. Entre otros cambios, se define el formato “free source form” que
sustituye al “fixed source form” con el que es posible hacer un código mejor estructurado ya que
se eliminan restricciones como la de longitud de lı́nea o a partir de qué carácter se puede poner una
instrucción. Esto es una buena noticia ya que ayudará a los programadores a hacer códigos más claros.
Volviendo a las anotaciones, OpenMP define un estándar[1] de cómo éstas deben ser. Se ha tomado
ese estándar para diseñar las anotaciones necesarias para CellSs/SMPSs. La sintaxis de las anotaciones
OpenMP en Fortran (en free source form) tiene el siguiente aspecto:
!$sentinel directive-name [clause[[,] clause] ...]
A continuación se describen los elementos que componen la anotación:
• Comentario (!$): el sı́mbolo del dollar detrás de la marca de comentario Fortran indica al compilador que lo que viene a continuación es una anotación.
• “sentinel”: tres letras que permiten al compilador identificar el tipo de anotación. No sólo
5.1. DEFINICIÓN DE LA SINTAXIS DEL LENGUJE PARA FORTRAN
31
OpenMP usa anotaciones por lo que esta marca permite al compilador saber si lo que sigue
es OpenMP, CellSs, u otras directivas especı́ficas de algún otro compilador.
• “directive-name”: Nombre de la directiva. Son las indicaciones propiamente dichas que se le
dan al compilador.
• “clause”: Cláusulas o modificadores sobre la directiva dada. Permiten aportar detalles o información adicional a la directiva. Puede contener expresiones entre paréntesis.
Ahora que ya conocemos cómo se construyen las anotaciones en Fortran podemos diseñar las
directivas que permitirán al compilador convertir un código con anotaciones en una aplicación CellSs.
Para ello lo primero es saber qué directivas son necesarias, es decir, qué información y dónde
la necesita el compilador para generar código que interactúe con la biblioteca de CellSs o SMPSs
correctamente. Esto no es nuevo, las directivas ya están definidas para el compilador de C, pero no
nos vamos a limitar a copiarlas. La razón es explicar por qué es necesaria cada una de ellas.
Pero antes de explicar las directivas en sı́, primero hace falta definir un sentinel de manera que el
compilador sea capaz de distinguir entre directivas dirigidas al compilador de CellSs/SMPSs o, por
lo contrario, son para otro propósito. El sentinel definido para identificar las directivas de CellSs fue
CSS y, por herencia, SMPSs se quedó con la misma marca.
Una vez el compilador ha detectado que se le ha pasado una directiva para indicarle algo, tiene
que interpretar dicha directiva. Para decidir las directivas es necesario recordar las fases de ejecución
vistas en el capı́tulo anterior.
En primer lugar, está la inicialización, es necesario iniciar los threads que se ejecutarán en los
diferentes procesadores o cores – y el helper thread, en el caso de CellSs - y registrar las tareas. Estos
pasos son necesarios para que pueda comenzar la ejecución de código con tareas correctamente. Ası́,
hay que proveer al lenguaje de una directiva que el compilador sustituya por invocaciones a las rutinas
de inicialización.
!$CSS START
De la misma manera, cuando finaliza la ejecución de la aplicación hay que sincronizar y finalizar
los threads correctamente de manera que se puedan leer los resultados producidos por las tareas. Para
este propósito se utiliza la siguiente directiva:
!$CSS FINISH
El compilador traducirá esta directiva a una llamada a la biblioteca que esperará a la correcta
finalización de todas las tareas y terminará los threads. Después de la ejecución de un finish no se
podrán volver a invocar tareas.
CAPÍTULO 5. FORTRAN PARA CELLSS/SMPSS
32
Para los puntos de sincronización parcial, se definen dos directivas útiles que permiten esperar a
que hayan terminado ciertas o todas las tareas. Se trata de las directivas wait on y barrier
!$CSS BARRIER
Esta directiva se convertirá en una llamada a la librerı́a que esperará a la correcta finalización
de todas las tareas. Ası́, se introduce un punto de sincronización total tras el que se pueden seguir
invocando tareas si se desea.
!$CSS WAIT ON(expression[,expression])
Esta directiva sirve para sincronizar de manera parcial con los workers. expression representa una
variable que ha sido usada como parámetro de salida o entrada/salida de una tarea. La biblioteca
esperará hasta que todas las tareas en las que esa variable figura como parámetro de salida hayan
terminado su ejecución (y devuelto los datos a memoria principal, en el caso de CellSs).
Pero hasta ahora no hemos indicado nada al compilador acerca de las tareas en sı́. Hay dos razones
por las que se ha dejado este punto para el final. La primera es que el funcionamiento de las directivas
que ahora se explicarán es un poco diferente al de las vistas hasta ahora. Además su sintaxis cambia
respecto a la versión de CellSs para C y C++ puesto que se han aprovechado herramientas del propio
lenguaje (Fortran) para aportar información al compilador.
Para facilitar su comprensión hemos dividido la explicación en tres partes. En la primera se explican las directivas originales en el compilador de C/C++. En segundo lugar, se explica el mecanismo
de invocación de tareas, qué ha de realizar el compilador para que se registre una tarea durante la
ejecución del código. Finalmente se explica cómo se especifican las tareas en Fortran y las razones
por las que se toman ciertas decisiones.
Directivas C/C++
CellSs define en C una directiva para indicar que una función es una tarea. Esta directiva permite al
usuario aportar información sobre los parámetros de la tarea. Además ofrece una cláusula que permite
hacer de una función una tarea de alta prioridad, la utilidad de esto se comentará más adelante. A
continuación se muestra la sintaxis de la directiva:
#pragma css task [input(<input parameters>)]optional \
[inout(<inout parameters>)]optional \
[output(<output parameters>)]optional \
[highpriority]optional
<function declaration or definition>
Las cláusulas input, output e inout, sirven para indicar al compilador la lista de parámetros de
entrada, salida o, entrada y salida, respectivamente. En los casos en que la declaración o definición de
5.1. DEFINICIÓN DE LA SINTAXIS DEL LENGUJE PARA FORTRAN
33
la función no aporte información sobre el tamaño de los parámetros (cuando son arrays), se han de
indicar entre corchetes las dimensiones de cada parámetro del que se indica el tamaño.
El compilador se ha de encargar de localizar las funciones marcadas como tareas y registrar sus
datos (la información de los parámetros de cada una de ellas). Luego se recorre el código del programa
principal buscando invocacines a esa función y usando los parámetros reales y los datos de la tarea,
transforma la llamada en código que se encarga de registrar la tarea en el grafo para que sea ejecutada
en el momento que corresponda.
Invocación de tareas
Vista la directiva que hay que usar en C para añadir tareas, conviene explicar por qué es necesaria
toda esa información. CellSs, o SMPSs, necesitan conocer qué funciones son tareas para construir
los task adapters. En el caso de CellSs, además, las tareas se compilan por separado del código del
programa principal ya que se ejecutarán en una arquitectura diferente.
Cuando el compilador encuentra una llamada a una función que es una tarea, debe añadir la tarea a
las colas y registrar sus parámetros de manera que se complete el grafo de dependencias entre tareas.
Para ello, se invoca a una función de la biblioteca pasándosele información sobre qué tarea ha de
añadir, los parámetros pasados por el usuario en esa invocación y la dirección de cada parámetro.
Además de indicar si la tarea es de alta prioridad o no.
También es necesario conocer el tamaño de los parámetros ya que cuando se hace renombrado de
parámetros se hace una nueva copia del parámetro con el que a partir de ese momento trabajarán las
tareas que usaban el parámetro original.
Especificación de tareas en Fortran
La principal diferencia entre C y Fortran en cuanto a las tareas, es el hecho de que Fortran carece
de contexto global. Esto impide saber si una tarea existe o no aunque la tarea esté definida en el mismo
fichero. La razón es que al no haber contexto, los subprogramas (subrutinas y funciones, o programa
principal) se compilan de manera independiente y como no hay ningún mecanismo para declarar una
subrutina, no se conocen todas las tareas de la aplicación en el momento de compilar un subprograma.
La solución es utilizar las instrucciones que dispone el lenguaje para especificar las interfaces de
subprogramas externos a otro. En Fortran, mediante el uso de la instrucción INTERFACE es posible
indicar una o varias interfaces de funciones o subrutinas externas. Ası́, habrá que indicar, en cada
subprograma que llame a tareas, las interfaces de, al menos todas las tareas que aquel subprograma
invocará directamente. Es decir, invocaciones dentro del propio cuerpo de la función o subrutina.
De esta manera, cada vez que se compile un subprograma se dispondrá de la información necesaria
CAPÍTULO 5. FORTRAN PARA CELLSS/SMPSS
34
sobre las tareas invocadas desde éste y será posible transformar correctamente las llamadas. Pero para
que funcione hace falta poner una anotación que indique que aquella interfaz corresponde a una tarea.
Estas anotaciones tendrı́an el siguiente aspecto
!$CSS TASK [HIGHPRIORITY]
Como se puede observar, esta anotación no tiene cláusulas, como en C, para indicar la dirección
o dimensiones de los parámetros. Fortran 95 tiene las herramientas adecuadas para hacer esto usando
la sintaxis correcta del lenguaje.
Para indicar la dirección de un parámetro hay que usar el atributo INTENT(IN | OUT | INOUT).
Ası́, en la declaración de cada parámetro se deberá indicar la dirección (INTENT). Además, para
indicar los tamaños de los arrays en Fortran, se usan las definiciones de éstos. Para ver esto lo mejor
será poner un ejemplo de interfaz marcada como tarea:
subroutine a()
interface
!$CSS TASK
subroutine task1(a,b,c)
integer, intent(in) :: a
integer, intent(inout) :: b, c(100,100)
end interface
...
call task1(x,y,z)
...
end subroutine
Pero esto no sirve al compilador para determinar qué subrutinas son tareas y cuáles no lo son. Por
ello también es necesario indicarlas mediante la misma anotación en la definición de la tarea (donde
está implementada).
!$CSS TASK
subroutine task1(a,b,c)
integer, intent(in) :: a
integer, intent(inout) :: b, c(100,100)
...
end subroutine
5.2. INTERACCIÓN CON LA BIBLIOTECA
35
5.2. Interacción con la biblioteca
En esta sección se plantea el problema la comunicación entre la biblioteca de CellSs/SMPSs,
escrita en C++, y las aplicaciones Fortran. En primer lugar se trata este problema. Luego, se muestran
las interfaces de la biblioteca definidas para Fortran.
La interacción entre C/C++ y Fortran es un tema complicado y que ha dado problemas desde que
se empezó a usar Fortran en entornos con bibliotecas escritas en C/C++. Se trata de poder invocar funciones escritas en C/C++ desde código en Fortran y viceversa y de compartir la información necersaria
para el buen funcionamiento de la aplicación CellSs/SMPSs.
Algunos compiladores ofrecen mecanismos para conseguir una interacción aceptable entre estos
dos lenguajes. Sin embargo, no todos los compiladores funcionan de la misma manera ası́ que si se
desea que el compilador sea portable, la mejor solución es utilizar siempre los recursos que el lenguaje
estándar nos proporciona.
El paso de parámetros en Fortran es diferente que en C o C++. Cuando en Fortran se pasa una
variable a una función o subrutina, ésta recibe una referencia a esa variable o a una sección de memoria
donde se aloja el valor pasado, en caso que sea resultado de la evaluación de una expresión o del
acceso a una sección de un array. En cualquier caso, el paso de parámetros siempre acaba siendo por
referencia.
C/C++, a diferencia, contempla el paso de parámetros por valor, de hecho, es comunmente usado.
Éste es el caso de la biblioteca de CellSs/SMPSs, las interfaces definidas hasta el momento hacı́an
uso de paso de parámetros por valor ası́ que ha sido necesario escribir una nueva versión con paso de
parámetros por referencia.
Siguiendo el orden de explicación de las secciones anteriores presentaremos la interfaz de la biblioteca escrita para Fortran. Lo primero es la inicialización, la biblioteca tiene varios pasos de inicialización que deben ser ejecutados para que la aplicación pueda empezar correctamente.
void css_fortran_pre_init();
void css_fortran_init();
void css_registerTask(char const *name, function_adapter adapter);
Las funciones css fortran pre init y css fortran init sirven para inicializar las estructuras internas
de la biblioteca, iniciar los threads que harán de workers, cargar configuraciones etc.
La función css registerTask1 , se encarga de registrar las tareas de la aplicación en el sistema.
Recibe como parámetros el nombre de la función y, en el caso de SMPSs, el task adapter.
1
Esta función no tiene el infijo fortran puesto que es común para aplicaciones en C y Fortran. Esto es porque no es el
compilador de Fortran el que se encarga de poner esta llamada en el código
CAPÍTULO 5. FORTRAN PARA CELLSS/SMPSS
36
Las directivas de sincronización barrier y wait on se transforman en llamadas a as siguientes
funciones de la biblioteca, respectivamente:
void css_fortran_barrier();
void css_fortran_wait_on(uint32_t *variableCount, void **addresses);
Los parámetros de la función css fortran wait on sirven para indicar las variables que se deben
esperar. El primero, variable Count representa el número de direcciones que contiene el segundo,
addresses. Cada dirección corresponde a cada una de las variables que haya añadido el programador
en la directiva.
Para la finalización la biblioteca proporciona la siguiente interfaz:
void css_fortran_finish()
Finalmente, la interfaz para añadir tareas es un poco más sofisticada:
typedef struct {
char direction;
char scalar;
char dimensions;
size_t size;
void *address;
css_parameter_bounds *bounds;
} css_parameter_t;
void css_fortran_add_task(
css_uint32_t *functionId, uint32_t *highPriority,
uint32_t *parameterCount, css_parameter_t *parameters);
En primer lugar, la llamada a css fortran add task, recibe el identificador asignado a la tarea que
el usuario habı́a invocado originalmente en el código. Esta invocación se sustituye por una llamada a
la función descrita.
Además, recibe una referencia a una variable que indica la prioridad de la tarea (si es highpriority
o no). El siguiente parámetro (parameterCount) indica el número de parámetros que tiene la tarea.
El último parámetro es una lista de elementos del tipo css parameter t, definido encima. Esta
estructura contiene la descripción de un parámetro consistente en los siguientes campos:
• direction: Determina si el parámetro es de entrada (in), salida(out) o entrada/salida (inout),
valores 1, 2 y 3 respectivamente.
5.2. INTERACCIÓN CON LA BIBLIOTECA
37
• scalar: Si vale 0, el parámetro será tratado como un array. En caso contrario será tratado como
una variable escalar.
• dimensions: Este campo está en desuso actualmente.
• size: Indica el tamaño total del parámetro en bytes.
• address: Dirección donde esá almacenado.
• bounds: Actualmente en desuso.
El compilador deberá completar esta estructrua tantas veces como parámetros tenga la llamada
para cada llamada a una tarea que se encuentre en el código. Ası́, en lugar de ejecutarse la llamada a
la tarea se ejecutará la función a la que llamaremos addTask.
38
CAPÍTULO 5. FORTRAN PARA CELLSS/SMPSS
Capı́tulo
6
Diseño del compilador
En este capı́tulo se explica la estructura y funcionamiento del compilador. Hay que destacar la importancia de los capı́tulos anteriores que aportan los conceptos necesarios para explicar las decisiones
tomadas en el momento de decidir cómo será el compilador.
En primer lugar se explica qué tipo de compilador se ha elegido y por qué. Luego se explica el
proceso de compilación de una aplicación (bien sea en C o en Fortran), esto determinará los pasos que
ha de realizar el compilador y nos ayudará a definir su estructura.
El compilador de CellSs/SMPSs está compuesto de varias partes. No todas las partes que se explican han sido desarrolladas ı́ntegramente en este proyecto por dos razones. En primer lugar la infraestructura de compilación es común para C y Fortran y el compilador de C ya estaba desarrollado.
Otras partes están implementadas basándose en software ya existente aunque las modificaciones han
sido muy significativas. En cada momento se indicará qué partes de código son comunes o no se han
modificado respecto al software original.
Finalmente, se presenta el diseño en sı́ del compilador. Se muestra qué componentes forman la
aplicación y cómo se integran estos componentes.
6.1. Compiladores
La primera decisión a tomar es qué tipo de compilador utilizar para crear una aplicación CellSs o
SMPSuoperscalar. Para ello analizaremos los diferentes tipos de compilador. Veremos las caracterı́sticas y utilidad de cada uno de ellos.
El objetivo de este capı́tulo es explicar el diseño del compilador, conocer qué partes lo forman y
la estructura que tiene. Esto se hará usando como guı́a el proceso de compilación que se explica en el
segundo apartado. Aquı́ no se explica en profundidad el funcionamiento interno de los compiladores.
39
CAPÍTULO 6. DISEÑO DEL COMPILADOR
40
Este tema se tratará en el siguiente capı́tulo, dedicado al desarrollo del compilador.
Como ya se ha comentado en los capı́tulos anteriores, para el compilador de CellSs o SMPSs se ha
elegido un compilador del tipo source-to-source. Se ha explicado también la razón básica por la que
se ha tomado esta decisión; las aplicaciones CellSs/SMPSs se hacen utilizando anotaciones OpenMP.
Un compilador es una aplicación que sirve para traducir programas entre diferentes lenguajes. El
cometido habitual de un compilador es dar como salida una versión del código entrado que pueda
ser ejecutado. Hay otras aplicaciones para las que los compiladores son usados (navegadores web,
procesadores de texto...) pero se alejan de lo que en el ámbito del proyecto interesa lo que se conoce
comunmente como compilador para hacer programas. En esta sección nos centraremos más en los
compiladores convencionales para acabar explicando por encima el funcionamiento de los compiladores source-to-source.
Las primeas aplicaciones que hacı́an algo parecido a la compilación eran ensambladores que ayudaban a los programadores a no tener que memorizar qué significaban los bits de cada instrucción
del lenguaje de una máquina. Actualmente los compiladores se utilizan para compilar muy diversas
aplicaciones y con diferentes objetivos. Según estos objetivos se pueden distinguir diferentes tipos de
compilador:
• Compiladores multi-pasada convencionales: Los tradicionales compiladores que de un lengaje
de alto nivel (C, C++, Fortran...) generan código máquina para una determinada arquitectura.
• Compiladores source-to-source: Compiladores que transforman código de un lenguaje de alto
nivel en código del mismo u otro lenguaje de alto nivel. Usados para optimizaciones en HPC
(OpenMP) y otras aplicaciones.
• Just in time compilers: Usados para lenguajes interpretados, el compilador de la aplicación
genera un código conocido como bytecode y un segundo compilador lo interpreta y ejecuta en
al momento.
• Otros: Optimizadores, hardware compilers...
En los compiladores convencionales podemos distinguir dos conceptos acerca del proceso de compilación. Por un lado está la multi-pasada, que se refiere a la necesidad de recorrer más de una vez el
código para poder completar el análisis correctamente.
El otro concepto es el de multi-fase. Los compiladores dividen el proceso en varias fases de manera
que cada fase lleva a cabo parte del proceso. Esto ayuda a dividir el trabajo de creación del compilador
y acelerar su desarrollo. Además permite mejorar la calidad del mismo ya que es más fácil hacer
testing sobre piezas de software más pequeñas.
Las principales fases se agrupan, por lo general, en dos bloques. En primer lugar tenemos un conjunto de fases conocido como front-end que se encarga de traducir el código a un lenguaje intermedio.
6.1. COMPILADORES
41
El front-end se suele dividir en las siguientes fases:
• Análisis del léxico: Se divide el código en pequeñas porciones conocidas como tokens que
pueden representar instrucciones, palabras clave, números, texto, operadores de expresiones...
• Análisis sintáctico: Se extrae la estructura sintáctica del texto siguiendo un conjunto de reglas
gramaticales (definidas para cada lenguaje). La estructura se suele guardar en un árbol, conocido
como AST (Abstract syntax tree).
• Análisis semántico: Del análisis sintáctico se obtiene el AST que permite ver una estructura
global del código. Pero a la hora de comprobar, por ejemplo, que una expresión es correcta,
deberı́amos volver a recorrer todo el árbol hasta encontrar las declaraciones de las variables
para saber sus tipos. Resulta más sencillo tener una estructura que almacene esa información
contextual de manera organizada y ası́ poder acceder a ella de una forma más directa.
El otro conjunto de fases conocido como back-end es el que se encarga de la generación de código.
En él se distinguen, por lo general, tres fases: análisis, optimización y generación de código.
El front-end y el back-end se comunican mediante lo que se conoce como un lenguaje de representación intermedia. Si bien puede tratarse de las propias estructuras de datos producidas por el
front-end, normalmente se trata de una representación textual en un lenguaje que puede ser común
para varios lenguajes de programación. Esto es, el compilador produce la misma representacion para
un fichero en C que para uno en Fortran, por ejemplo, y a partir de entonces la compilación se hace
exactamente igual.
Este esquema sólo es orientativo y dada la variedad y complejidad de los compiladores actuales,
no se puede considerar un estudio riguroso de la estructura de los compiladores. Sin embargo, es muy
útil para nuestro propósito. Las mayores diferencias entre el esquema presentado y el compilador de
Fortran que se ha desarrollado se hallan en la segunda parte, lo que hemos presentado como back-end.
Los compiladores source-to-source comparten con los compiladores convencionales gran parte del
front-end. A la hora de procesar la información analizada, estos compiladores hacen las modificaciones necesarias y reescriben el programa en un fichero que será compilado después por un compilador
convencional. Esto proporciona un nivel de abstracción más ya que en el diseño del compilador no
hay que preocuparse de la generación de código máquina. Este hecho, a su vez, aporta una mayor portabilidad respecto a otras soluciones ya que el código máquina generado dependerá del compilador
seleccionado en la configuración.
Los compiladores source-to-source más comunes son los de OpenMP. Su funcionamiento, como el
nuestro hará, se basa en interpretar las anotaciones en el código fuente. Durante el análisis (front-end)
el compilador detecta estas anotaciones y en las fases posteriores se eliminan y se usa la información
aportada en ellas para modificar ciertas partes del código o generar código nuevo.
CAPÍTULO 6. DISEÑO DEL COMPILADOR
42
6.2. El proceso de compilación
Para crear una aplicación CellSs o SMPSs es necesario seguir una serie de pasos. En primer
lugar hay que escribir el código de la aplicación. En la sección 8 se muestran un par de ejemplos de
aplicaciones que usan el modelo de programación.
El proceso de compilación es, en lo esencial, independiente del lenguaje de programación. Compilar una aplicación CellSs o SMPSs en C o C++ se hace siguiendo los mismos pasos que para compilar
una aplicación en Fortran. Por esto, la infraestructura de compilación de CellSs/SMPSs es común para
C, C++ y Fortran.
Una vez escrita la aplicación, el programador sólo tiene que compilarla ejecutando el compilador de CellSs/SMPSs con los parámetros adecuados. Los requisitos y opciones para esto se pueden
consultar en el manual de CellSs y SMPSs anexo a esta memoria.
Figura 6.1: Proceso de compilación habitual.
En la figura 6.1 se muestra el proceso común de compilación de aplicaciones, desde el punto de vista del usuario-programador. Muchos de los compiladores de lenguajes no interpretados de
GNU/Linux siguen este esquema.
Primero se pasa el fichero fuente al compilador que lo procesa para crear un objeto compilado.
Algunos compiladores de Fortran hacen un paso de pre-procesado internamente que permite preparar
el código para facilitar la compilación o integrar los módulos. Posteriormente, se enlaza (link) el objeto
con los objetos y bibliotecas necesarios para crear el ejecutable final.
La idea es, básicamente, que el usuario interactúe con el compilador de CellSs/SMPSs de esta
misma manera. Esto le abstrae de tener que pensar en cómo se forma la aplicación.
Una opción de muchos compiladores es crear la aplicación directamente con los objetos que genera de los ficheros pasados. El compilador de CellSs/SMPSs, también ofrecerá esta opción. Ası́, el
proceso de compilación tendrá el aspecto que se muestra en las figuras 6.2 y 6.3. En primer lugar se
ejecuta el compilador de Cell/SMP Superscalar y se genera un objeto empaquetado que contiene el
6.2. EL PROCESO DE COMPILACIÓN
43
programa compilado y un descriptor de las tareas de la aplicación.
En un segundo paso se desempaqueta dicho objeto y se generan los task adapters a partir de
la información de las tareas almacenada en el objeto. Además se genera un poco de código en C
necesario para ayudar a la aplicación a entenderse con las bibliotecas. Posteriormente se compila todo
y se enlazan los objetos con las bibliotecas generando finalmente un ejecutable CellSs/SMPSs.
Figura 6.2: Compilación para CellSs/SMPSs.
Figura 6.3: Enlazado para CellSs/SMPSs.
Internamente el proceso de compilación de una aplicación con este compilador source-to-source
es un poco diferente a los convencionales. El compilador de nuestro modelo varı́a su comportamiento
en función de si se trata de una aplicación CellSs o SMPSs. Aun ası́, su comportamiento desde el
punto de vista del usuario no cambia.
CAPÍTULO 6. DISEÑO DEL COMPILADOR
44
6.2.1.
Compilación en CellSs
A la hora de compilar uno o varios ficheros para formar una aplicación CellSs, hay que tener en
cuenta la naturaleza heterogénea del procesador Cell/Be. Esto implica que, por debajo, el compilador
deberá compilar parte del código por separado. Concretamente, se ha de compilar el programa principal (que formará, junto con la biblioteca del master, el master thread) con un compilador que genere
código para la PPE (PowerPC Element) del Cell. Mientras, el código correspondiente a las tareas
marcadas por el usuario se ha de compilar con el compilador de las SPE. Como se puede observar en
la figura 6.4, el compilador genera dos ficheros de código fuente.
Figura 6.4: Proceso de compilación y empaquetado para CellSs.
En el paso etiquetado como compiler es donde se realizan las transformaciones de código, para
ello se invoca dos veces el compilador; una con un perfil de configuración que generará el código de
la parte del master y otra con un perfil que generará el código de los adaptadores de las tareas. Posteriormente se compilan los ficheros generados con los compiladores que corresponda y se empaquetan
en un objeto en el que, además, se añade información sobre las tareas.
En la segunda prarte de la compilación, ver figura 6.5, para CellSs también hay un aspecto que
comentar. A la hora de generar el código de los adaptadores hay que generar también un array de
funciones que contendrá la lista de adaptadores de manera que la biblioteca de los workers sea capaz
de hacer la llamada a las tareas.
Finalmente se enlaza todo. Este proceso también difiere de lo normal en CellSs porque la aplicación incluye código máquina para dos arquitecturas en el mismo ejecutable. Para hacer que esto
funcione se utilizan las herramientas del kit de desarrollo de IBM (Cell/BE SDK). Se crea una aplicación de SPE con las tareas y la biblioteca de los workers. Esto se empaqueta en un objeto de PPU y
se enlaza con la aplicación final.
6.3. ARQUITECTURA DEL COMPILADOR
45
Figura 6.5: Proceso de enlazado para CellSs.
6.2.2.
Compilación en SMPSs
En SMPSs, el proceso de compilación es más parecido al ejemplo mostrado en la introducción.
El hecho de ser una arquitectura de memoria compartida facilita la compilación puesto que no es
necesario compilar por separado las tareas del resto de la aplicación. En la figura 6.6 se muestra el
proceso de compilación completo para aplicaciones SMPSs.
6.3. Arquitectura del compilador
Como se ha explicado en la introducción, el compilador de Fortran funciona sobre la misma
infraestructura que el de C/C++. Ésta se conoce como metadriver. El metadriver es el encargado
de llevar a cabo el proceso de compilación de manera genérica. Es decir, independientemente de el
lenguaje, permite compilar y enlazar (link) programas CellSs/SMPSs.
Internamente, determina el lenguaje en que están escritos los programas y, utilizando el compilador adecuado (el compilador de Fortran o C/C++ para CellSs/SMPSs), se generan los objetos sin
empaquetar.
Este compilador es el que se encarga de hacer las transformaciones de código y compilar el código
transformado con el compilador que corresponda (en nuestro caso se tratarı́a del compilador de Fortran
que se haya configurado para la distribución de CellSs/SMPSs).
CAPÍTULO 6. DISEÑO DEL COMPILADOR
46
Figura 6.6: Proceso de compilación completo para SMPSs.
6.4. Diseño del metadriver
El metadriver estaba originalmente pensado para la compilación de programas en C y C++. Sin
embargo, su estructura es muy genérica y, no sin algunas ampliaciones que se explicarán más adelante,
se adapta bien a las necesidades para compilar una aplicación en Fortran.
Siguiendo el proceso de compilación explicado anteriormente, presentamos los módulos que componen el metadriver y la función de cada uno de ellos. Cabe destacar que las transformaciones de
código son las mismas en CellSs y SMPSs, salvo que para CellSs se compilan las tareas por separado.
Por esta razón, el compilador es el mismo. Ası́ mismo, algunos módulos del metadriver son comunes
mientras otros varı́an ligeramente en función de si se trata de CellSs o SMPSs.
El metadriver se compone, en primer lugar, de un módulo conocido como Driver que gestiona el
proceso de compilación. Su función es determinar el tipo de ficheros a compilar, procesar las opciones
de configuración y las opciones pasadas por el usuario. El otro componente principal es el empaquetador, integrado en el Driver, se encarga de guardar los objetos compilados y los descriptores de tareas
en un sólo objeto.
Centrándonos en la primera parte del proceso de creación de una aplicación, figura 6.7, la com-
6.5. DISEÑO INTERNO DEL COMPILADOR
47
Figura 6.7: Módulos del metadriver para la parte de compilación y empaquetado.
pilación y empaquetado de los programas, el módulo principal es el compilador en sı́ (mf95ss). Éste
funciona como una aplicación independiente que es usada por el metadriver. Hay un módulo encargado de esto.
La figura 6.8, muestra los módulos que intervienen en el proceso de enlazado. Desde el desempaquetado del objeto generado en la compilación hasta la generación del código de los adaptadores y su
compilación.
Figura 6.8: Módulos del metadriver para la parte de enlazado.
6.5. Diseño interno del compilador
En esta sección entendemos por compilador al componente etiquetado como mf95ss en apartados
anterioes. Se trara de la parte principal del proyecto. Su cometido es llevar a cabo las transformaciones
de código descritas en la sección 5.
El compilador de Fortran para CellSs/SMPSs está basado en la estructura del mcxx (compila-
CAPÍTULO 6. DISEÑO DEL COMPILADOR
48
dor usado para C/C++). Se trata de un compilador muy versátil del tipo source-to-source pensado
originalmente para transformaciones de código para OpenMP y otros modelos de programación.
De la misma manera que con el proceso de compilación visto, para compilar un fichero con el
mf95ss (es decir, llevar a cabo las transformaciones de código y compilar el código resultante), se
sigue un proceso gestionado por un driver.
La figura 6.9 muestra dicho proceso. En ella se pueden observar los diferentes componentes que
intervienen en él. El funcionamiento del compilador consiste en ejecutar, en primer lugar una fase de
análisis (frontend) donde se llevan a cabo las siguientes tareas:
Figura 6.9: Proceso de compilación del mf95ss.
• Análisis sintáctico: se lee el código y se construye una estructura representativa de la estructura
del programa (Abstract Syntax Tree).
• Análisis sintáctico de las directivas (anotaciones): De la misma manera que hay una gramática que determina la estructura correcta de un programa, las directivas tienen una estructura
determinada.
• Análisis semántico: se completan las estructuras de datos que guardan la información semántica
(sı́mbolos, funciones, tipos...) del programa.
Hay que destacar dos detalles en este punto. El primero es que puesto que el objetivo del compilador no es generar código máquina, no se analiza el código en busca de errores (type checking). Se
analiza suponiendo que es correcto y el compilador que se use posteriormente se encargará de verificar
esto.
El segundo es que en las fases de análisis se realizan otros pasos de procesado del árbol de representación (AST) que se explicarán en el capı́tulo siguiente.
Una vez hecho el análisis, se ejecutan una serie de pasos, conocidos como fases del compilador,
que llevarán a cabo las transformaciones de código. Las fases se ayudan de una capa de software
6.5. DISEÑO INTERNO DEL COMPILADOR
49
llamada TL (Translation Language) que se encarga de ofrecer una interfaz usable a través de la cual
obtener información de la representación del programa (AST e información semántica) y modificar el
código del mismo.
El modelo de TL es una representación abstracta de los componentes que puede haber en el código de un programa (expresiones, llamdas a funciones, subrutinas, instrucciones, declaraciones...).
Además incluye herramientas que facilitan la movilidad a través del código que se está compilando a la hora de escribir las fases del compilador, éstas son los componentes del compilador interno
(mf95ss) encargados de realizar las transformaciones de código.
En las siguientes subsecciones se comenta en detalle el diseño interno de cada uno de los módulos
o partes presentados. Aquı́ también se explica el código original en que están basados los diferentes
elementos, cuáles han sido usados directamente y cuáles han tendido que ser modificados o reescritos.
6.5.1.
El Driver
Este elemento es el controlador principal del proceso de compilación de un fichero Fortran. Como
tal es necesario que se representen de algún modo los ficheros que puede recibir (que es capaz de
procesar el compilador), las opciones de compilación y configuración.
El mf95ss, de la misma manera que el mcxx, es un compilador configurable, las fases se pueden
establecer mediante la modificación de un fichero de configuración, cosa que permite poder usar el
mismo compilador para diferentes propósitos.
El driver del compilador es una versión adaptada del driver del mcxx. Todas las estructuras de
representación de ficheros y opciones han tenido que ser adaptadas a Fortran.
6.5.2.
El frontend
El frontend es la parte del compilador encargada de analizar el código del programa que se va
a compilar. Aquı́ se presenta la estructura del frontend del compilador de Fortran, sus módulos funcionales y sus estructuras de datos. En el capı́tulo de desarrollo se tratarán los problemas concretos
relacionados con Fortran, las gramáticas, tablas de sı́mbolos etc.
Esta parte de la aplicación es también una adaptación de software ya existente, todas las gramáticas
de Fortran están definidas en el estándar de Fortran 95 [2]. El compilador mf95, contenı́a un conjunto
de módulos que aislados del resto de la aplicación forman el frontend de nuestro compilador.A pesar
de que se ha aprovechado código ya existente, se han hecho modificaciones muy significativas sobre
el código original puesto que el sistema de compilación es completamente diferente al de la aplicación
original. Los módulos mencionados se pueden observar en la figura 6.10
Cada módulo lleva a cabo una parte del proceso de análisis. En primer lugar, el módulo etiquetado
CAPÍTULO 6. DISEÑO DEL COMPILADOR
50
Figura 6.10: Proceso de análisis del frontend.
como analyzer se encarga de leer el código y analizar su estructura. El siguiente módulo, de loopnesting, se encarga de arreglar la estructura del árbol de sintaxis (AST) ya que si no las estructuras
anidadas se analizan de manera incorrecta, esto se explicará en el siguiente capı́tulo.
Los dos módulos modules y resolve use statements permiten generar los módulos pre-compilados,
por un lado, y hacer accesible a las program units la información contextual de los módulos, por el
otro.
El bloque denominado buildsymtab se encarga de completar una tabla de sı́mbolos con la información contextual (variables declaradas, tipos, funciones, interfaces...) de cada program unit (unidad
básica en aplicaciones Fortran). Esta información permite, por ejemplo, obtener el tipo de un sı́mbolo
al que se hace referencia dentro de una expresión.
El módulo semantic se encarga de corregir ambigüedades introducidas en el análisis del código.
Esto es por causa de la naturaleza arcaica de Fortran.
Además, el frontend proporciona herramientas para volver a obtener el código fuete a partir del
árbol y para obtener información interna del compilador durante la ejecución (muy útil para localizar
y corregir errores durante el desarrollo).
En el capı́tulo de desarrollo se explica el funcionamiento de cada una de estas partes y más detalles
sobre el análisis de código Fortran.
6.5.3.
El modelo de TL
Esta parte del compilador proporciona una interfaz agradable para programar las fases de transformación de código. Hay que tener en cuenta que las estructuras de datos que representan el código
y las tablas de sı́mbolos son muy grandes y complejas de manejar.
El conjunto de clases conocido como TL (Translation Language) da forma a estas estructuras y
permite, mediante un relativamente reducido número de clases y métodos diferentes, que se pueda
recorrer el código, consultar información, analizarlo y modificarlo.
Por las caracterı́sticas de la aplicación, es más adecuado implementar esta parte en C++ ya que
con orientación a objetos se consigue una mayor abstracción.
6.5. DISEÑO INTERNO DEL COMPILADOR
51
En el bloque TL se pueden distinguir cinco conjuntos de clases según su función. En la sección
7.2.4 se explican con más detalles:
• Estructuras del frontend: Conjunto de clases que representan las estructuras de datos con información del código que se está compilando. Permiten consultar y modificar la información
generada por el frontend de manera fácil.
• Fortran: Conjunto de clases que representan las construcciones propias del lenguaje. Permiten
trabajar con pártes del código de manera más clara.
• Traverse: Conjunto de clases que permite realizar recorridos en los AST en busca de directivas
o ciertas construcciones en el código.
• Fases: Conjunto de clases que controlan las fases, su configuración, definen la superclase que
se usa para implementar las fases y herramientas para su uso.
• Generación de código: Clase que permite la generación de código desde las fases, el código
se genera de manera textual en las fases y posteriormente se obtiene un AST que puede ser
incorporado al código.
No está de más dejar claro que aquı́ cuando se habla de fases nos referimos a la superclase que da
forma a una fase. Define su comportamiento pero no implementa transformaciones de código.
6.5.4.
Las fases
Las fases de compilación permiten programar de manera sencilla y esquemática las transformaciones de código para un propósito u otro. En este caso, el propósito es el de hacer las transformaciones
necesarias para generar una aplicación CellSs o SMPSs. Esto se hace mediante una serie de pasos
que, utilizando las herramientas proporcionadas por la capa de TL, realizan una parte del proceso. Los
diferentes pasos se organizan por tareas diferentes de manera que se pueda abstraer una de la otra y
programarlas con más facilidad e independencia.
Para comunicarse entre ellas, las fases, usan un objeto que contiene toda la información necesaria
durante el recorrido (pipeline) de la compilación a través de las fases. Esto último se hace aplicando
el patrón de diseño conocido como DTO (Data Transfer Object). El DTO proporciona un medio para
propagar datos de una fase a otra en el que se pueden ir añadiendo campos y obtener objetos de
diferentes tipos o clases.
Las fases de compilación para CellSs y SMPSs son las mismas aunque, mediante opciones de configuración, se comportan de manera diferente para responder adecuadamente en función de qué modelo de programación se haya decidido usar.
CAPÍTULO 6. DISEÑO DEL COMPILADOR
52
Figura 6.11: Pipeline de las fases de CellSs/SMPSs.
La figura 6.11 muestra el proceso de compilación de un fichero CellSs/SMPSs. A continuación se
describe la función de cada fase.
• Configuración: Su función es registrar la información necesaria para la compilación en el DTO.
Para ello se consultan las variables que se pasa al proceso de compilación desde la lı́nea de
comandos.
• Pre-análisis: Recorre el código localizando y marcando todas las funciones y subrutinas.
• Análisis de tareas: Localiza las funciones que son tareas o se ha marcado que se ejecutarán en
un lado u otro (PPE o SPE) - en el caso de CellSs.
• Function router: Hace un recorrido a través de las funciones y subrutinas y las llamadas entre
ellas para determinar cómo se comporta la aplicación. Es importante porque será necesario
saber si una función es invocada por una tarea, por ejemplo, y ser compilada por separado (en
CellSs).
• Transformación de las llamadas a tareas: Recorre todos los subprogramas comprobando si invocan tareas (esto se sabe porque la especificación exige definir una interfaz por cada tarea
que se invoca desde un subprograma). En caso afirmativo realiza la sustitución de la llamada
por una llamada a la función de la biblioteca addTask con los parámetros adecuados. Aquı́ se
genera además el fichero con las descripciones de las tareas para que el metadriver genere los
adaptadores (task adapters).
• Transformación de las directivas (anotaciones): Esta fase elimina todas las directivas del código
sustituyéndolas, si es necesario, por las instrucciones que corresponda.
Una vez ejecutadas las fases el driver toma el control de nuevo y escribe en un fichero el código
modificado que se ha generado y lo compila usando un compilador de Fortran convencional.
Las fases de CellSs y SMPSs son similares para C/C++ y Fortran. La configuración, pre-análisis
y el recirrido de funciones son prácticamente iguales. De la misma manera, la transformación de
directivas sólo difiere en el código generado y en detalles menores que se comentarán más adelante.
Sin embargo, el análisis de tareas y las transformaciones de las llamadas cambian sustancialmente
puesto que son los que llevan a cabo análisis y transformaciones más dependientes del tipo de lenguaje
6.5. DISEÑO INTERNO DEL COMPILADOR
53
de programación. El hecho de que Fortran carezca de un contexto global, simplifica el análisis puesto
que no hay que comprobar declaraciones. En cuanto a las transformaciones de las llamadas, en C/C++
el recorrido es bastante más sencillo mientras que en Fortran se complica por el hecho de tener que
recorrer las interfaces para todas las funciones o subrutinas que puedan invocar tareas, registrar las
tareas que se declaran y, finalmente, localizar las llamadas y realizar las sustituciones.
La programación de las fases deja clara la importancia del nivel inferior de software sobre el que
se apoyan. Una buena definición de las construcciones Fortran ayuda a que la programación sea clara
y sencilla.
54
CAPÍTULO 6. DISEÑO DEL COMPILADOR
Capı́tulo
7
Desarrollo del compilador
En este capı́tulo entraremos en los detalles del desarrollo del compilador para hacer comprender su
funcionamiento interno, mostrar cómo se integran las diferentes piezas que lo componen y, finalemnte,
conocer las tecnologı́as usadas en el desarrollo del compilador.
7.1. El metadriver
El metadriver es la parte visible del compilador de cara al usuario-programador. Su función es
relaizar todos los pasos necesarios para crear una aplicación CellSs/SMPSs. En el capı́tulo dedicado
al diseño 6.4 se explican los componentes que forman el metadriver y su función. Puesto que este
programa ya existı́a y se trata de algo externo al compilador, no entraremos en más detalles acerca de
su implementación. Sin embargo sı́ explicaremos las modificaciones que se han tenido que llevar a
cabo en el metadriver para que sea capaz de compilar aplicaciones en Fortran.
Para ello seguiremos los pasos del proceso de compilación desde el punto de vista del metadriver,
mencionaremos los elementos que intervienen en cada uno de los pasos y qué modificaciones se han
hecho en cada uno de ellos.
En primer lugar, cuando se ejecuta el metadriver se procesan las opciones pasadas al compilador
por la lı́nea de comandos. Para ello se utiliza el módulo ParameterParser. En este módulo se lee la
lista de opciones y se guarda la información pasada en un objeto que será usado para consultar la
configuración durante el resto del proceso de compilación (se trata de la clase CompilationOptions).
En el ParameterParser se han hecho dos modificaciones. La primera es añadir las extensiones de
los ficheros Fortran de manera que cuando se pase al compilador un fichero con extensión .f90, por
ejemplo, sepa distinguirlo de uno con la extensión de C o C++.
La otra modificación es la incorporación de tres nuevas opciones que permiten al usuario pasar
55
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
56
parámetros al compilador de Fortran nativo a través del compilador que hace las transformaciones
de código (mf95ssen el caso de Fortran). Estas opciones (-WPPEf,.. -WSPEf,.. -Wf,..) especı́ficas para
fortran, permiten distinguir entre opciones para el compilador de Fortran y opciones para el compilador de C/C++. Para ver más información sobre las opciones del compilador se pueden consultar los
manuales de CellSs y SMPSs adjuntos en los anexos 1 y 2.
En cuanto a la clase CompilationOptions se han añadido más campos en la lista de opciones de
configuración del metadriver donde se guardará la siguiente información:
• fortranSourceFiles: Lista de ficheros Fortran pasados al compilador.
• fortranPpuCompiler: Compilador nativo de la PPE, para CellSs.
• fortranPpuLinkerLibs: Bibliotecas necesarias para enlazar una aplicacion CellSs en la PPE, en
Fortran.
• fortranSpuCompiler: Compilador nativo de la SPE, para CellSs.
• fortranSpuLinkerLibs: Bibliotecas necesarias para enlazar el objeto de la SPE, en CellSs.
• fortranCompiler: Compilador nativo (SMPSs).
• fortranLinkerLibs: Bibliotecas necesarias para enlazar una aplicación escrita en Fortran (SMPSs).
• fortranSupportsSizeof : Determina si el compilador de Fortran reconoce la función SIZEOF.
• fortranUnderscore: Determina cómo son generados los sı́mbolos de los objetos en Fortran (algunos compiladores añaden un o dos a los sı́mbolos al generar el objeto).
• fortranSmpArchBytes: Determina si las direcciones de memoria de la arquitectura para la que
se está compilando son de 32 o 64 bits.
• IntSize: Determina el tamaño de un entero en C para la arquitectura actual. Necesario para crear
aplicaciones SMPSs en Fortran.
Estos campos se completan en el siguiente paso de la ejecución del metadriver donde se carga
el fichero de configuración. Este fichero tendrá definidas todas las opciones necesarias para crear
aplicaciones CellSs o SMPSs, según sea el caso, en C/C++ y Fortran. Se generará en el momento de
la configuración de la distribución de CellSs/SMPSs teniendo en cuenta las caracterı́sticas del sistema
para el que se quieren ejecutar las aplicaciones creadas usando el compilador.
Después de la carga de la configuración empieza la ejecución del proceso de compilación en sı́. En
función de si se compila para CellSs o SMPSs, intervienen diferentes módulos del metadriver Pero
su funcionamiento es similar. Para hacerlo más comprensible explicaremos las modificaciones que
7.1. EL METADRIVER
57
han sido necesarias en la compilación y enlazado de aplicaciones SMPSs y luego mencionaremos las
diferencias con la compilacion en CellSs.
A la hora de crear una aplicación SMPSs, podemos distinguir dos fases, la compilación y el
enlazado. La compilación implica la generación de uno o varios objetos empaquetados. Cada objeto
empaquetado tendrá el objeto resultado de compilar un fichero fuente, y la descripción de las tareas
que se han encontrado en el fichero fuente. En el enlazado se desempaquetarán el o los objetos y se
creará la aplicación. Este objeto empaquetado también ha sido modificado, para la compilación en
Fortran, añadiendo el número de parámetros que tienen las tareas que se encuentran en un código
Fortran. Esto es, porque en Fortran hay que generar los task adapters, cosa que no pasa en C ya que
los genera directamente el compilador mcxx.
Empecemos desde el principio, para crear una aplicación SMPSs, como hemos visto en el capı́tulo
de diseño (6.2), hay que ejecutar el compilador de CellSs/SMPSs, el mf95ss en el caso de Fortran, con
el perfil1 de compilación SMPSs. Este perfil indica que se generará un sólo objeto, correspondiente
al código del usuario con las anotaciones transformadas y compilado, y un fichero manifest con la
información de las tareas.
La clase SMPCompiler se encarga de invocar al compilador de SMPSs que corresponda dependiendo de si el fichero a compilar en ese momento es Fortran o C/C++. Ası́, la modificación que se
ha hecho permite distinguir el lenguaje del fichero fuente e invocar al mcxx, en el caso de C/C++ o al
mf95ss si se trata de Fortran. De las opciones del metadriver se obtienen los parámetros que hay que
pasar al compilador de SMPSs para que la compilación sea correcta. Esto se hará para cada fichero
fuente que se desee compilar.
Posteriormente se procede a empaquetar el fichero compilado con la descripción de las tareas,
que se encuentran en el manifest. Si se va a empaquetar un fichero C compilado, se añade la lista de
nombres de las tareas en el objeto y el fichero compilado. En el caso de Fortran, se añade para cada
tarea el número de parámetros que tiene.
Ahora, tendremos un objeto empaquetado para cada fichero fuente que se le haya pasado al metadriver. Es, entonces, el momento de proceder al enlazado. La clase SMPLinker es la encargada de
ello.
En primer lugar hay que desempaquetar los ficheros que se van a enlazar y leer la información
añadida sobre las tareas. En este proceso se generan dos listas de tareas, las que tienen el adaptador
generado (han aparecido en un objeto compilado de un fichero en C), y las que no. Aquı́ conviene
hacer notar que las tareas podrı́an estar escritas en C y usadas desde Fortran o viceversa. Siempre que
una tarea aparezca de alguna manera en un fichero en C (usada o creada), su task adapter será creado
y no será necesario, en este caso, que el metadriver se comporte diferente que como lo hacı́a cuando
sólo compilaba C/C++.
1
Un perfil corresponde a una configuración del compilador mf95ss
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
58
En el caso de tareas que sólo aparecen en objetos empaquetados creados a partir de código en Fortran, los adaptadores no existen de manera que habrá que generarlos. La clase SMPCodeGenerator es
la encargada de generar código en C para completar la integración de la aplicación con la biblioteca.
Esta clase tiene dos funciones: GenerateTaskAdapters, a la que se le pasa la lista de tareas que no
tienen los adaptadores generados y el número de parámetros de cada una de ellas, y GenerateTaskRegistration que generará la función task registration cssgenerated a la que invocan las aplicaciones
SMPSs para que se registren las tareas en la biblioteca; también se generarán aquı́ los identificadores
de las tareas.
Una vez generados los ficheros con las inicializaciones, los identificadores de las tareas y los
task adapters, se compilan y se enlazan con los objetos desempaquetados para obtener la aplicación
SMPSs.
En el caso de CellSs, las modificaciones hechas para que se puedan compilar y enlazar ficheros
en Fortran son equivalentes a las hechas en SMPSs con la diferencia de que las tareas y los task
adapters se generan aparte y se compilan con un compilador de SPE. Además, en CellSs es necesario
otro mecanismo para hacer llegar los task adapters a la biblioteca del worker. Ası́, la clase CellCodeGenerator, encargada de generar el código auxiliar para CellSs, también genera un array con las
direcciones de los adaptadores que la biblioteca del worker conoce. De esta manera se establece el
mecanismo mediante el cual la biblioteca del worker será capaz de invocar a las tareas a través de los
task adapters.
Otra diferencia esencial entre CellSs y SMPSs, por lo que al metadriver respecta, es que cuando
se compila para CellSs, se ejecutará el mcxx o el mf95ss - según corresponda - dos veces. Una vez
con perfil de PPE, para generar el objeto de PPE y el manifest y otra con perfil de SPE para generar la
parte de código que se ejecutará en las SPE, tareas y procedimientos invocados por las tareas.
7.2. mf95ss
Como se explica en el capı́tulo anterior, el mf95ss está compuesto por dos partes principales adaptadas de dos compiladores diferentes. La primera parte, el frontend se ha extraı́do del compilador
de Fortran mf95. Se corresponde a la parte de análisis de código. La segunda parte está sacada del
compilador de C/C++ mcxx y es la que proporciona la infraestructura para la generación de código.
En el capı́tulo de dedicado al diseño del compilador se ha explicado la estructura del mf95ss. En esta
sección conoceremos a fondo las partes que lo componen.
Esta parte es la más densa en cuanto al trabajo del proyecto se refiere. La implementación del
compilador no sólo implica la integración de dos partes tan diferentes: también implica añadir todas
las funcionalidades necesarias para obtener la información del código que se está compilando que
interesa para compilar una aplicación CellSs/SMPSs.
7.2. MF95SS
7.2.1.
59
Driver
El compilador mcxx está concebido como un compilador autónomo que hace transformaciones de
código y compila ficheros. También es capaz de enlazarlos para crear aplicaciones. Para CellSs/SMPSs
en C/C++ se usa como parte del proceso de compilación de manera que su funcionamiento queda escondido a ojos del usuario-programador. Pero, internamente, este compilador lleva a cabo un proceso
de compilación similar al que se ha explicado en el capı́tulo de diseño.
A la hora de implementar el mf95ss se ha tomado como punto de partida el driver del mcxx ya
que incorpora los pasos necesarios para iniciar e invocar las fases de compilación. Una de las caracterı́sticas más interesantes del mcxx que también ha heredado el compilador de Fortran, es que puede
leer varios perfiles de configuración de un fichero. Esto permite que se pueda ejecutar el compilador
con diferentes conjuntos de fases dependiendo del perfil que se cargue. Ası́, es posible usar el mismo
compilador para crear aplicaciones CellSs y SMPSs.
El proceso de compilación implementado en este driver es, en lo esencial, igual para C que para
Fortran de manera que las modificaciones se reducen a adaptar la configuración del compilador para
que funcione con ficheros Fortran, en lugar de con ficheros C/C++, por un lado, y a adaptar el proceso
de compilación a el frontend de Fortran. También se ha cambiado, lógicamente, el conjunto de opciones que se pueden pasar al compilador ya que hay muchas opcioes especı́ficas de C que no interesan
y otras que interesa añadir para configurar la compilación en Fortran.
7.2.2.
Funcionamiento del frontend
Junto con el bloque de TL, el frontend es una de las partes a las que más tiempo del desarrollo
se ha dedicado. Como se ha explicado anteriormente, el frontend está basado en el código de análisis
del compilador mf95. Se trata de un compilador pensado para aplicaciones OpenMP que funciona
mediante un sistema de generación de código diferente al de nuestro compilador.
En primer lugar veremos las estructuras que el frontend usa para analizar el código y cómo se
construyen. Con algunos ejemplos se introduce el funcionamiento del proceso de análisis y algunos
conceptos sobre las gramáticas que rigen Fortran.
La segunda parte presenta las modificaciones que se han hecho en este bloque (el frontend) durante
la etapa de desarrollo del compilador.
7.2.2.1. Análisis y estructuras internas del frontend
Para poder analizar correctamente programas escritos en Fortran, lo primero que hay que hacer es
conocer las reglas que determinan cómo es un programa correcto en Fortran. En nuestro caso, se trata
del estándar de Fortran 95. Este estándar, a grandes rasgos, define las palabras clave del lenguaje y
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
60
qué combinaciones de ellas tienen sentido.
No vamos a desarrollar todo el estándar de Fortran 95 en este apartado porque no tiene sentido
y porque el frontend original ya estaba escrito y hay partes que no son de mucha relevancia en el
desarrollo del compilador de CellSs/SMPSs. Sin embargo intentaremos ver las partes clave de cómo
se forman y analizan las principales construcciones del lenguaje para que se pueda seguir el hilo de
las explicaciones que seguirán en las siguientes secciones.
Una aplicacion Fortran se estructura en un conjunto de construcciones denominadas program
units. Una program unit puede ser una funcion (FUCTION), una subrutina (SUBROUTINE), un módulo (MODULE), un BLOCK DATA SUBPROGRAM, o el programa prinicpal (PROGRAM). Además,
los subprogramas (subrutinas, funciones y programa principal), pueden contener subrutinas y funciones internas. Éstas no pueden contener más programas internos, solo hay un nivel de anidación. A
continuación se muestra un ejemplo con dos program units.
program p
...
contains
subroutine subr(a,b)
end subroutine
end program p
function f(t)
real:: t, f
...
end function
El AST usado para analizar el código de un fichero Fortran, es una estructura enlazada por punteros. La unidad principal es el nodo del AST, que contiene la información referente a la parte del código
a la que representa, enlaces a los nodos hijos, enlace al nodo padre, información sobre el fichero que
se está compilando y un campo denominado atributo extendido que se explica en la sección siguiente.
El código se analiza siguiendo las gramáticas que define el estándar de Fortran 95. La información
sobre el programa se guarda en los nodos del árbol en un formato ascendente (lo primero es lo más
profundo en el árbol.) A continuación se muestra un esquema de las gramáticas que analizan la lista
de program units.
executable_program is program_unit
or executable_program program_unit
program_unit
is
or
or
or
or
function_subprogram
subroutine_subprogram
main_program
module
block_data_subprogram
7.2. MF95SS
61
Una vez analizado el código, hemos obtenido un AST (Abstract syntax tree), con una representación entera de todo el fichero leı́do. A partir de este AST se puede recorrer el código en orden y
siguendo sus estructuras. Sin embargo, Fortran tiene una caracterı́stica que hace que no se puedan
analizar directamente de manera correcta sus códigos. En Fortran las construcciones iterativas (DO)
se pueden especificar mediante etiquetas que identifican la lı́nea donde se acaba el bucle. Como se
muestra en el ejemplo:
..
30
40
DO 40 J = 1, NRHS
DO 30 I = M + 1, N
B( I, J ) = CZERO
CONTINUE
CONTINUE
..
A la hora de analizar el código, supone un problema tener que relacionar el número de la etiqueta
con el que cierra el bucle ya que la gramática, por su naturaleza (Free-context) no es capaz de distinguir
entre los valores de las etiquetas.
La etapa loopnesting recorre todo el código buscando las sentencias DO y, en caso que el bucle
esté cerrado mediante etiquetas, su correspondiente etiqueta. Luego coge el trozo de lista de instrucciones que queda entre las dos sentencias y lo anida de manera que el código va quedando estructurado
en el AST.
Después de la etapa de loopnesting, se ejecutan dos bloques encargados de generar los módulos
e integrarlos en el código allá donde se incluya un use statement, esto es, poner en el código la
sentencia USE module name que permite acceder a los tipos derivados, funciones y subrutinas del
módulo desde la program unti en que se ha incluido. Estas etapas no son de gran interés ya que las
ampliaciones hechas en el compilador no afectan a ellas y el modelo de programación para Fortran
ignora los módulos en la versión actual. Cabe decir que se ha modificado la forma en que se cuelga el
AST del módulo en el use statement para facilitar el hecho de que pueda ser ignorado por las fases.
La siguiente etapa, buildsymtab es, como su nombre indica, la encargada de completar una tabla
de sı́mbolos para cada program unit. Su funcionamiento es sencillo, recorre todas las program unit y
crea una tabla de sı́mbolos para cada una de ellas. Para cada program unit determina su tipo, añade
su sı́mbolo dentro de su propia tabla, determina dónde terminan las declaraciones y empiezan las
sentencias ejecutables y recorre las declaraciones, interfaces y módulos incluidos añadiendo todas las
entidades declaradas a la tabla de sı́mbolos. La figura muestra un esquema del comportamiento de la
etapa:
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
62
Build symtab
- Recorrer program units
- Si es un "main program", "function" o "subroutine":
- iniciar tabla de sı́mbolos.
- build_symtab_body()
- build_symtab_internals()"
- Si es un block data subprogram:
- iniciar tabla de sı́mbolos
- build_symtab_body()
- Si es un module
- iniciar tabla de sı́mbolos
- build_symtab_module()
- build_symtab_body()
- Recorrer instrucciones
- Añadir sı́mbolos declarados a la tabla de sı́mbolos.
- build_symtab_internals()
- Para cada "internal subprogram":
- Añadir el sı́mbolo de la función o
subrutina a al tabla de sı́mbolos del padre.
- iniciar su propia tabla de sı́mbolos y hacer
el mismo proceso que con las "function" o
"subroutine" (teniendo en cuenta que no hay
"internal subprograms").
- build_symtab_module()
- build_symtab_body() de la parte de especificación
- Para cada "function" o "subroutine" del módulo:
- Crear su tabla de sı́mbolos.
- build_symtab_body()
- build_symtab_internal_subprograms()
Las tablas de sı́mbolos se guardan en una superestructura llamada unit list que mantiene la jerarquı́a de program units y sus tablas de sı́mbolos.
Las propiedades de los sı́mbolos añadidos a la tabla dependen de los atributos, y otros modificadores que se hayan escrito en el código. Cuando buildsymtab se encuentra una declaración, analiza
el árbol para ver las propiedades del sı́mbolo que se está declarando y añade esta información en el
sı́mbolo que se añadirá a la tabla de sı́mbolos. Hay tres estructuras de datos symtab entry, type y attributes, encargadas de guardar toda esta información. Estas estructuras se corresponden a las clases
symbol, type y attributes del bloque TL (7.2.4), ahı́ se explican los métodos que permiten acceder a
7.2. MF95SS
63
los diferentes campos de estas estructuras.
La última etapa del frontend es la de análisis semántico. Como se ha comentado en otros capı́tulos, este compilador no hace typechecking. Sin embargo, es necesario resolver las ambigüedades de
algunas construcciones del código cuyo significado puede variar en función de las declaraciones. Concretamente, una expresión como la siguiente: A = F(A), tiene diferente significado en función de cómo
esté declarado F. Si F es una función, el AST deberá tener una forma diferente que si se trata de un
array, por ejemplo. En la etapa de análisis sintáctico es imposible determinar si una cierta expresión
contiene una llamada a una función o un acceso a un array ya que no conocemos cómo está declarado.
El módulo semantic se encarga de corregir este tipo de errores para que el AST sea una representación
correcta de lo que el código quiere expresar.
7.2.3.
Modificaciones sobre el frontend original
Para adaptar el frontend a las necesidades del compilador para CellSs/SMPSs, se han tomado las
diferentes etapas del proceso de análisiss y se han añadido las estructuras de datos y funcionalidades
que permiten la integración con el resto de las partes del compilador. Entre estas modificaciones se
pueden distinguir dos grupos: las que añaden funcionalidad al frontend original, y las necesarias para
que el compilador funcione correctamente con el driver y las fases.
En el grupo de modificaciones que representan extensiones del frontend original hay, principalmente, dos modificaciones en las que centraremos la atención en este apartado. La primera es la
ampliación de la gramática que permite reconocer las anotaciones. La otra es la incorporación de el
campo dinámico llamado extended attribute en el AST. Este atributo extendido permite relacionar los
nodos del AST con diferentes tipos de datos (booleanos, enteros, ASTs...).
7.2.3.1. Ampliación de la gramática
El código original del frontend tiene las herramientas básicas necesarias para analizar programas en Fortran e interpretar anotaciones OpenMP. Sin embargo, el sistema que usa para analizar las
anotaciones no resulta útil porque está pensado exclusivamente para anotaciones OpenMP. Por ello es
necesario sustituir esta parte por una que sea capaz de reconocer nuestras directivas.
A la hora de hacer esto hay varias posibilidades; por un lado podemos añadir las reglas necesarias
para que tanto el análisis del léxico como el sintáctico sean capaces de reconocer las anotaciones. Pero
esta solución implica crear el mismo problema que nos hemos encontrado a la hora de definir nuestras
anotaciones, la gramática que ya estaba hecha para OpenMP, no es adaptable a nuevas extensiones
del lenguaje. Por esta razón, y de la misma manera que lo hace el compilador de C/C++ (mcxx), lo
que vamos a hacer es añadir un sistema de reconocimiento de directivas lo más genérico posible de
manera que las modificaciones en el núcleo del compilador a la hora de hacer posibles ampliaciones
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
64
o darle otros usos al compilador, sea mı́nima en el peor de los casos.
La primera modificación que hay que hacer, pues, en el frontend es añadir las estructuras de datos
y funciones necesarias para la gestión dinámica de diferentes tipos de anotaciones. Esto da soporte
para poder reconocer anotaciones para diferentes propósitos directamente desde las fases (definiendo
la anotación en la fase).
Hay dos tipos de anotaciones que se distinguen por su naturaleza, las directivas y las construcciones (construct). La diferencia entre estos dos tipos está en la manera en que son analizadas. Las
directivas son anotaciones que tienen que ir dentro del código de un program unit y son interpretadas
como una simple instrucción. Las constrcciones, por su parte, funcionan como una superestructura que
contiene un cierto trozo de código bajo su domninio o influencia. En la figura 7.1 se puede observar la
diferencia, al analizar una direciva, el AST obtenido es plano mientras el AST resultado de analizar
una construcción contiene parte del código bajo su domino, como hijo del nodo que representa a la
anotación.
(a) AST de una anotacion de tipo construcción.
(b) AST de una anotación de tipo directiva.
Figura 7.1: Tipos de anotación
Para ampliar la gramática de manera que reconozca las anotaciones, definiremos, en primer lugar,
las producciones que definen una anotación correcta. Las anotaciones que reconoceremos, son un
superconjunto de las definidas en el capı́tulo 5. Su forma se ha tomado de las anotaciones definidas
para C adaptándolas a Fortran de manera que permitirá futuras ampliaciones en la funcionalidad del
compilador.
Ası́, las directivas reconocidas tiene la siguiente forma:
!$sentinel directve [clause[[,] clause]...]
7.2. MF95SS
65
Y la gramática que las reconoce es la siguiente:
pragma_custom_directive
is
!$ sentinel clause_list_opt
clause_list_opt
is
or
[]
clause_list
clause_list
is
or
or
or
clause
clause_list
clause_list, clause
clause_list clause
clause
is
or
or
custom_clause ( expr_list )
custom_clause ( )
custom_clause
Finalmente, es necesario un punto de entrada; Esto es, decidir en qué partes del código podrá haber
una anotación y de qué tipo. Ésta es quizá la parte más dependiente del modelo de programación,
sobretodo para las construcciones. En el caso de las directivas, en Fortran ya está casi todo decidido.
Una directiva sólo puede aparecer, en principio, allá donde puede haber código o declaraciones, en el
cuerpo de la program unit. Es algo que no cambiará aunque se quiera soportar un nuevo modelo de
programación. Para las construcciones, sin embargo, hay que definir qué pueden contener. Serı́a un
trabajo demasiado duro e inútil definir el punto de entrada para todas las posibles construcciones de
manera que parece razonable definir sólo aquellas que vamos a usar.
Definir el punto de entrada consiste en localizar en la gramática que reconoce el lenguaje, las
posiciones en que pueden aparecer las anotaciones e introducir una regla que produzca las anotaciones.
En el caso de las construcciones, y como se ve en la gramática de las anotaciones, es necesario además
un punto de vuelta a la gramática de Fortran, que nos devuelva desde la construcción a lo que contiene
ésta en cada caso.
El hecho de añadir las anotaciones, en el caso de las directivas, no tiene ningún efecto destacable
sobre el resto del frontend ya que simplemente hay que ignorarlas (comportamiento por defecto de
buildsymtab y semantic con sentencias desconocidas). En el caso de las construcciones el tema se
complica un poco más. Las anotaciones que introducen una modificación en la estructura del AST,
hacen que éste sea diferente a lo esperado por las etapas loopnesting y buildsymtab. Éstas esperan
una estructura de program units y interfaces determinada y se encuentran un nodo intermedio que
representa la anotación. En la figura 7.2 se muestra la diferencia entre el AST de una subrutina con
anotación y el de una sin anotación.
El código de las etapas loopnesting y buildsymtab se ha modificado de manera que cuando se
encuentre una anotación del tipo construct, realicen su tarea un nodo más abajo. Ası́, la anotación no
afecta al proceso.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
66
Figura 7.2: La construcción (gris oscuro) se interpone en el AST entre el nodo de la lista y la subrutina
(gris claro).
7.2.3.2. extended attribute
Para poder manejar con comodidad el AST desde el nivel de TL es muy útil disponer de un mecanismo para moverse por él y obtener información acerca de sus nodos. El mecanismo que se ha
usado para hacer esto en nuestro compilador se conoce como extended attribute y permite, por ejemplo, identificar diferentes partes del código (declaraciones, funciones y subrutinas, expresiones...),
localizar las anotaciones y relacionar los nodos del árbol con la tabla de sı́mbolos que contiene la
información contextual relacionada con ellos.
Los nodos del AST cuentan con un campo extensible capaz de almacenar uno o varios “atributos
extendidos”. Cada “atributo extendido” tiene un nombre y un tipo determinado. El compilador mcxx
aporta el conjunto de clases que permiten añadir un atributo a un nodo y consultar la presencia de un
atributo en un determinado nodo. A continuación se muestran las funciones que permiten definir y
buscar atributos en los nodos:
// To set an attribute in an AST node
ASTAttrSet(AST a, char* attribute_tag, type type_t, tl_type_t attrib);
// To get an attribute from an AST node
//
This function returns the attribute in the case it has
//
been found in the extended data field of an AST.
//
The schema contains the list of existing tag names.
tl_type_t ext_struct_get_field(ext_schema_t ast_extensible_schema,
extended_data data,
char* name,
char *found);
7.2. MF95SS
67
En la figura 7.3 se puede ver cómo identificar las program units, la fase de TL define una clase llamada ProgramUnit. Esta clase representa, lógicamente al código que corresponde a una program unit
y todo lo que está bajo su influencia: declaraciones, interfaces, subprogramas internos, instrucciones...
Figura 7.3: En este ejemplo se muestra una representación de un atributo añadido a un nodo del AST.
Ası́, al nodo del AST del que cuelga toda esta información le añadiremos un atributo (o etiqueta)
que indique que se trata de una program unit. Las program units, como se ha comentado en el apartado 7.2.2, definen un contexto dentro del cual se conocen las variables, parámetros, interfaces, etc.
definidos en ellas. Esta información contextual puede interesar obtenerla a partir de cualquier nodo
interno a la program unit. Si se declara una variable de un cierto tipo, por ejemplo, se guardará esta
información en la tabla de sı́mbolos que corresponda. Pero nos puede interesar obtener el tipo de una
variable a partir del nodo donde hemos encontrado aquella variable.
Para esto lo interesante es poder obtener la tabla de sı́mbolos a partir de cualquier nodo interno
a la program unit. Mediante un atributo podemos incluir el sı́mbolo de la program unit en el nodo
inicial de ésta.
// in file fortran95-buildsymtab.c
...
if (node.type = AST_SUBROUTINE) // a program unit
{
symtab_t* st = new_symbol_table();
ASTAttrSet(node, LANG_IS_PROGRAM_UNIT, tl_type_t, tl_bool(1));
symtab_entry_t * symbol =
build_symtab_subroutine_statement(subroutine_statement, st);
ASTAttrSet(node, LANG_PROGRAM_UNIT_SYMBOL,
tl_type_t, tl_symbol(symbol));
...
}
...
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
68
Otro atributo muy importante es el que sirve para identificar las anotaciones. Usando este atributo
se pueden localizar todas las anotaciones del AST o de una porción del AST de manera que se puede
construir un recorrido que permita procesarlas. Las siguientes instrucciones han sido añadidas en la
etapa semántica del frontend:
static void custom_directive_handler(AST a, symtab_t* st)
{
build_symtab_pragma_custom_line(ASTSon0(a));
ASTAttrSet(a, LANG_IS_CUSTOM_DIRECTIVE, tl_type_t, tl_bool(1));
ASTAttrSet(a, LANG_CUSTOM_LINE, tl_type_t, tl_ast(ASTSon0(a)));
}
La etapa semántica recorre el AST invocando a los handlers definidos para cada tipo de nodo.
En este caso se añaden los atributos necesarios para poder identificar y obtener información de las
directivas.
También interesa identificar otras partes del código del usuario mediante atributos como pueden
ser las expresiones, bloques de interfaces, el punto donde terminan las declaraciones y empieza el
código ejecutable... Todos estos atributos y otros están definidos en el fichero attributes.def y se añaden
a los nodos adecuados en el bloque semántico (buildsymtab y semantic).
7.2.4.
Modelo de TL
Este bloque es un equivalente al bloque TL del compilador mcxx, para el compilador de Fortran.
Debido a la gran diferencia entre C/C++ y Fortran 95, no basta con copiadar las clases directamente ya
que muchas de ellas se basan en conceptos y estructuras propias del lenguaje que se desea compilar.
Las clases de TL son la base del funcionamiento del compilador. Sin ellas habrı́a que programar
directamente los recorridos por el árbol, la generación de código etc. En este apartado veremos en
profundidad el funcionamiento de este bloque por partes según los conjuntos de clases definidos el el
capı́tulo anterior.
El primer paso para proporcionar un sistema que nos permita trabajar cómodamente con las estructuras de bajo nivel del Frontend, es proporcionar una capa de software que permita manejarlas
cómodamente en un modelo de clases comprensible y abstracto. Este modelo también nos permitirá contextualizar algunas funcionalidades que en el frontend se encuentran sueltas de manera que
serán absorbidas por la clase más adecuada según el caso.
En este primer apartado hay dos niveles. El primero es el nivel más cercano a las estructuras
de datos donde se representa el AST y las estructuras del bloque semántico del frontend. El siguiente
nivel es el más abstracto. Aporta una visión más global sobre las construcciones del lenguaje y permite
trabajar con ellas de una manera más cómoda.
7.2. MF95SS
69
7.2.4.1. Estructuras del frontend
La clase AST, prorciona las funciones necesarias para poder moverse con comodidad por los
nodos de un árbol AST, hacer consultas básicas y modificar el árbol introduciendo nuevos AST’s. Las
funciones principales son las siguientes:
• Consulta:
• Obtener extended attribute.
• Obtener texto del AST
• Obtener tabla de sı́mbolos
• Obtener program unit
• Obtener lı́nea
• Recorridos por el árbol:
• Obtener iterador
• Obtener subárboles
• Modificación:
• Añadir nodos
• Añadir listas
• Eliminar nodos
• Reemplazar texto
El conjunto de clases que permiten trabajar con la información semántica producida por el frontend en la fase buildsymtab está centrado en la tabla de sı́mbolos. La tabla de sı́mbolos, TL::Symtab,
contiene los sı́mbolos relacionados con un cierto contexto, en nuestro caso una program unit o, como
máximo, de un contexto interno a una program unit.
La clase está compuesta básicamente por cuatro métodos que permiten acceder a la información
de una tabla de sı́mbolos.
• get_access_spec(): Permite conocer si la tabla de sı́mbolos es de ámbito público o privado.
• query_symbol(): Devuelve un sı́mbolo si se encuentra el nombre en la tabla de sı́mbolos
o en una tabla de nivel superior (esto es, si el subprograma del que esta tabla representa el
contexto es un subprograma interno de otra program unit o una interfaz).
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
70
• query_in_current_scope(): Devuleve un sı́mbolo si se encuentra el nombre en la tabla
de sı́mbolos.
• query_common(): Devuelve el sı́mbolo de una variable common si ésta se encuentra en la
tabla de sı́mbolos.
Los sı́mbolos contienen información sobre una entidad del código. Ésta bien puede ser una variable, una función o subrutina, una interfaz de una función o subrutina, un parámetro de una función,
una constante, un tipo, etc. La información almacenada depende del tipo de entidad que se trate. La
clase TL::Symbol aporta una interfaz para trabajar con la información de un sı́mbolo. Para un sı́mbolo
se pueden hacer las siguientes consultas en función de el tipo de sı́mbolo que sea:
• Identificador:
• get_type(): Devuelve el tipo del sı́mbolo.
• get_attributes(): Devuelve los atributos que determinan ciertas caracterı́sticas del
sı́mbolo.
• Tipo derivado:
• derived_is_sequence(): Determina si el tipo derivado está declarado como secuencia.
• derived_is_private(): Determina si el tipo derivado está declarado como private
• derived_get_num_components(): Devuelve el número de componentes del tipo
derivado.
• derived_get_component_type(): Devuelve el tipo de un determinado componente.
• derived_get_component_attributes(): Devuelve los atributos asociados a un
determinado componente.
• Program unit:
• get_program_unit_symtab(): Devuelve la tabla de sı́mbolos de la program unit.
• procedure_is_recursive(): Devuelve cierto si el subprograma (en caso que el
sı́mbolo represente una función o subrutina), es recursivo.
• procedure_get_num_parameters(): Devuelve el número de parámetros de la
función o subrutina, si es el caso.
• procedure_get_dummy_argument(): Devuelve el sı́mbolo de un parámetro por
posición.
7.2. MF95SS
71
• procedure_get_dummy_argument_index(): Devuelve el ı́ndice que determina
la posición de un parámetro, según su nombre.
• function_get_result(): Devuelve el sı́mbolo que representa el resultado de la
función, si es el caso.
• procedure_is_pure(): Fortran define unas restricciones que determinan un tipo de
subprogramas (funciones o subrutinas) cuando se definien como pure, en estos casos, esta
función devuelve cierto.
Como se ha visto, un sı́mbolo, si es una variable, una función, subrutina, módulo etc. tiene asociados unos atributos. La clase Attributes permite consultarlos mediante los métodos que se enumeran
a continuación (Algunos métodos han sido omitidos por ser de poca relevancia o ser extensiones del
lenguaje que no se usan en el contexto del proyecto):
• is_allocatable(): El sı́mbolo tiene su valor alojado en memoria dinámica.
• is_pointer(): El sı́mbolo es una referencia a otra variable asociada.
• is_target(): El sı́mbolo es una variable que puede ser asociada con un pointer.
• is_parameter(): El sı́mbolo es una constante.
• is_optional(): Determina si el sı́mbolo es un parámetro opcional (sólo en el caso que sea
parámetro -dummy argument- de una función).
• is_save(): Atributo para indicar que una variable de una función o subrutina mantiene su
valor entre ejecuciones.
• is_external(): Se trata de una función externa a la program unit en que está declarada.
• is_intrinsic(): Indica que una función o subrutina es intrı́nsica del lenguaje (funciones
incluidas por defecto en Fortran).
• get_kind_array(): Devuelve el tipo de especificación de array, si se trata de una variable
o el tipo de lo que devuelve una función con al menos una dimensión.
• get_rank(): Devuelve el número de dimensiones de la variable, 0 si es escalar, 1 o más si es
un array.
• get_dimension_lower_bound(): Permite obtener el AST con la expresión que determina el lı́mite inferior de una determinada dimensión.
• get_dimension_upper_bound(): Permite obtener el AST con la expresión que determina el lı́mite superior de una determinada dimensión.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
72
• get_tree_array_spec(): Permite obtener el AST completo que define la especificación
del array (cómo están definidas sus dimensiones).
• has_char_length(): Permite conocer si se ha especificado un tamaño en carácteres para
los valores del tipo de la variable en este sı́mbolo.
• get_tree_char_length(): Permite obtener el AST de la especificación que se menciona
en el método anterior.
• is_in_common(): Permite conocer si se trata de una variable incluida en un common (se
alojará en memoria de datos y no en la pila).
• get_in_common(): Devuelve el nombre del common en que se ha incluido la variable.
• is_automatic(): Se trata de una array, no parámetro de función o subrutina, cuyas dimensiones dependen de un parámetro.
• is_dummy(): El sı́mbolo es de un parámetro de la función o subrutina donde está declarado.
• is_use_associated(): Se trata de un sı́mbolo que declarado en un módulo que ha sido
incluido (use associated) en una program unit
• get_module_name(): Devuelve el nombre del módulo, si el sı́mbolo es un módulo.
• is_renamed(): Indica si el nombre de la variable se ha cambiado al incluir el módulo.
• get_original_name(): Devuelve el nombre original de la variable en el módulo.
• get_intent(): Devuelve la dirección del sı́mbolo (cuando es un parámetro de una función),
puede ser IN, OUT o INOUT.
• get_access(): Determina si el sı́mbolo es de carácter PRIVATE o PUBLIC.
La clase Type, última de este bloque, encapsula los métodos necesarios para obtener información
del tipo de una variable: saber de que tipo es y obtener el nombre del tipo. Además, si se trata de un
tipo derivado podemos obtener la siguiente información:
• derived_type_get_symbol(): Devuelve el sı́mbolo que describe el tipo derivado.
• derived_type_get_symbol_name(): Devuelve el nombre del sı́mbolo que describe el
tipo derivado.
• derived_is_sequence(): Devuelve cierto si el tipo derivado está declarado como sequence.
7.2. MF95SS
73
• derived_is_private(): Devuelve cierto si el tipo derivado tiene el atributo private (sólo
puede ser accedido desde dentro del módulo en que se encuentra).
• derived_get_num_components(): Devuelve el número de componentes (campos) del
derived type.
• derived_get_component_name(): Devuelve el nombre de un determinado componente.
• derived_get_component_type(): Devuelve el tipo de un determinado componente.
• derived_get_component_attributes(): Devuelve los atributos de un determinado
componente.
Otro método interesante de esta clase es el método get_declaration(), que permite obtener
el texto correspondiente a una declaración de una variable con ese tipo.
7.2.4.2. Langconstruct
Langconstruct es el nombre del fichero que agrupa la jerarquı́a de clases relacionadas con las construcciones de Fortran 95. Como ya se ha comentado, estas clases aportan una visión más estructurada
del código. A la hora de escribir una fase, ver 7.2.5, se puede trabajar con objetos que representan
partes del código y sobre los que se pueden aplicar operaciones para obtener información procesada
o encapsulada en objetos del modelo de TL.
Las clases de esta jerarquı́a se han definido a partir de las necesidades del compilador que se
está desarrollando. Nunca hay que perder de vista que el principal objetivo del proyecto es desarrollar
un compilador de Fortran para CellSs y SMPSs. Por ello, no se ha extendido completamente el modelo
a todos los recovecos de Fortran.
La imagen muestra las clases que forman la estructura de langconstructs. Posteriormente se explica por cada una de ellas lo que representa y sus métodos.
La clase LangConstruct, la superclase de este modelo, define las caracterı́sticas principales que
tienen los langconstructs. Todos las las subclases tienen un AST asociado que contiene el nodo que da
acceso a toda la información que se considera perteneciente a la construcción. Si, por ejemplo tomamos la construcción Expression el AST de una expresión contendrá la lista completa de operadores y
variables que la forman. Un langconstruct tiene además un objeto del tipo predicate del que se habla
más adelante, que sirve para comprobar si un node de AST determinado está representado por ése
langconstruct o no.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
74
Figura 7.4: Diagrama de clases de la parte LangConstruct de TL.
Los métodos comunes de los langconstrcut son los siguientes:
• get_ast(): Devuelve el AST del langconstruct.
• get_symtab(): Busca la tabla de sı́mbolos relacionada con ése langconstruct, si ésta existe.
• get_program_unit(): Busca el ProgramUnit que contiene la construcción.
• get_top_level_program_unit(): Busca la TopLevelProgramUnit que contiene la construcción.
Las primeras clases que vamos a ver son las que hacen referencia a las program units. Aunque
estructuralmente sean iguales, las program units se distinguen en función de su posición en el código.
En algun momento puede interesar recorrer todas aquellas que no son internas a otras. Por ello se
define, por un lado, la clase ProgamUnit, que permite manejar la información referente a una program
unit en general. Y, por el otro, dos clases hermanas que sirven para distinguir los dos casos descritos
y permiten igualmente obtener la ProgramUnit general.
La clase ProgramUnit tiene una serie de métodos destinados a determinar el tipo de ProgramUnit
de que se trata:
• is_main_program(): Programa principal.
• is_function_subprogram(): Función.
• is_subroutine_subprogram(): Subrutina.
• is_module(): Se trata de un módulo.
• is_block_data_subprogram(): Bloque de inicialización de datos common.
7.2. MF95SS
75
• is_internal_subprogram(): Función o subrutina interna a otra o al programa principal.
Otro conjunto de métodos permite obtener información adicional sobre la progam unit:
• get_body(): Devuelve el cuerpo de la program unit, si se trata de una subrutina, función o
programa principal.
• get_internals(): Devuelve la lista de subprogramas internos, en los mismos casos que en
la anterior.
• get_subprogram_name(): Igual que antes, en estos casos devuelve el nombre del suboprograma en cuestión.
• get_program_unit_symbol(): Devuelve el sı́mbolo de la program unit (Toda program
unit tiene un sı́mbolo asociado que tiene información sobre ella -parámetros, etc., si es el casoy su tabla de sı́mbolos).
Hay otras tres clases relacionadas estrechamente con las program units. En primer lugar está la
clase ProgramUnitBody. Un objeto de esta clase se puede obtener de una program unit y su AST
corresponde al cuerpo, exclusivamente la lista de código, que hay dentro de una program unit. Tiene, además, una función que permite acceder al punto del AST donde terminan las declaraciones y
empiecan las sentencias ejecutables.
La segunda clase es SubprogramInterface que contiene el AST de una interfaz que describe el tipo
de subprograma y parámetros de una función o subrutina externa a la program unit donde se declara
la interfaz. Esta clase tiene un método que permite obtener el sı́mbolo de dicha función o subrutina y
ası́ concer sus caracterı́sticas y las de sus parámetros.
Finalmente, tenemos la clase SubprogramCall, es una clase que representa cualquier tipo de llamada a subprograma, bien sea función o subrutina. De ella se puede obtener la información referente
a la llamada mediante los siguientes métodos:
• get_called_symbol_name(): Permite obtener directamente el nombre de la función o
subrutina invocada.
• get_called_symbol(): Devuelve el Sı́mbolo de la función o subrutina de la llamada.
• get_parameters(): Devuelve la lista de parámetros pasados a la función o subrutina.
• get_parameter(): Devuelve, por posición, un determinado parámetro de la invocación.
Hay todavı́a dos clases importantes que no hemos comentado. Son necesarias, en el caso del
compilador de CellSs/SMPSs, ya que nos permitirán manejar las expresiones de los parámetros en las
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
76
llamadas a funciones, por un lado, y modificar las expresiones que definen las dimensiones de una
array, para calcular su tamaño.
La clase Expression puede encapsular cualquier AST que sea una expresion o parte de ella. Ası́,
una variable es una expresión, una suma de dos operandos es una expresión, una llamada a una función
es una expresión, etc. De una expresión nos puede interesar saber si es una variable. Una variable
será cualquier expresión, ahora en el sentido más amplio de la palabra expresión, que empieze con el
nombre de una variable, aunque sea para acceder a un campo de un tipo derivado o a un elemento de
un array.
Además en la clase expresión se puede preguntar, en caso que la expresión sea un acceso a una
variable, si el acceso que se hace es a zonas de memoria no consecutivas. Esto es porque Fortran
permite acceder a, por ejemplo, todos los elementos pares de un array de manera que se crea un nuevo
array para esa expresión con los elementos mencionados. Esta información es muy útil ya que el
modelo de programación espera que se le pase a las tareas un array no temporal. Esto se explica con
un poco más de detalle en la sección dedicada a las fases.
La otra clase que hemos mencionado es VariableReference, esta clase representa el identificador de
una variable a la que se hace referencia en una expresión. La diferencia entre este identificador y otros
que se pueden encontrar en una expresión es que un VariableReference es un sı́mbolo declarado en
su tabla de sı́mbolos correspondiente mientras que otro identificador cualquiera podrı́a ser un campo
de un tipo derivado; a la hora de reemplazar identificadores éste no nos interesa por razones que se
explican, como antes, en la sección que trata las fases 7.2.5.
7.2.4.3. Recorridos sobre el AST
Este apartado está dedicado a explicar el mecanismo que permite recorrer el AST. A la hora de
analizar el código nos puede interesar buscar todas aquellas partes que tienen una cierta propiedad
en común como, por ejemplo, las funciones y subrutinas. Aquı́ es donde entra en juego el atributo
extendido que hemos presentado antes. Dado un nodo cualquiera, el frontend tiene una función que
permite consultar la presencia de un determinado atributo.
A partir de aquı́ no resultarı́a difı́cil implementar un recorrido por todo el árbol buscando un determinado atributo y llevando a cabo las acciones que interesen en un cierto momento de la compilación.
Sin embargo, durante el proceso de compilación se pueden hacer muchos recorridos buscando diferentes atributos que nos permitan identificar ciertas partes del código para hacer modificaciones o
obtener información útil para la compilación. Podemos querer recorrer las anotaciones, las llamadas
a subprogramas y muchas otras cosas más. Por ello interesa una forma de poder hacer todos estos
recorridos sin tener que implementarlos cada vez.
En TL se proporciona un conjunto de clases que definen un recorrido genérico de manera que a la
hora de programar las fases sólo nos hemos de preocupar de qué partes del código queremos localizar
7.2. MF95SS
77
y qué acciones hay que llevar a cabo en cada momento.
El primer concepto que hay que introducir es el de functor. El functor generaliza la idea de función. Representa una entidad que puede ser invocada con un parámetro de un cierto tipo. El functor
está programado en C++ mediante plantillas (templates) que permiten su uso con diferentes tipos de
datos o clases. Una especialización de Functor es el predicate. Se trata de un functor que siempre
devuelve un booleano.
La clase DepthTraverse es la encargada de hacer un recorrido en profundidad sobre un AST. El
funcionamiento es sencillo, la clase tiene una lista donde se relaciona un tipo de functors llamados
TraverseASTFunctor con otros conocidos como TraverseFunctors. Los primeros sirven para decidir
si un nodo del AST cumple una cierta condición y cómo se comportará el recorrido. Ası́, se puede
decidir si el recorrido sigue bajando en este nodo o, por lo contrario acaba aunque el nodo tenga más
hijos (RECURSE). En cuanto a la condición, si ésta se cumple, entonces el TraverseFunctor asociado
será invocado pasándole el nodo del AST.
La gracia del mecanismo reside en que los functors descritos anteriormente son personalizables.
De este modo se puede definir un TraverseASTFunctor cuya condición para invocar al TraverseFunctor sea, por ejemplo, que el nodo que está visitando el recorrido tenga el atributo LANG IS PROGRAM UNIT,
que determina si es el nodo inicial de una program unit. Entonces, se invocará una función sobrecargada en el objeto de una subclase de TraverseFunctor que está asociado al TraverseASTFunctor
mencionado. En la figura 7.5 se muestra gráficamente este comportamiento.
Figura 7.5: Para cada recorrido se puede definir uno o varios emphTraverseASTFunctor y sus correspondientes TraverseFunctor de manera que éstos sean invocados cada vez que el emphTraverseASTFunctor asociado devuelva cierto para un nodo del AST.
El comportamiento real del recorrido es un poco más sofisticado ya que la clase genérica TraverseFunctor define dos funciones a sobrecargar. Una se invocará, si se cumple la condición, antes de
continuar con el recorrido (en preorden), la otra se invocará después (en postorden). Esto puede ser
interesante en función de qué se esté recorriendo y qué se desee hacer.
El último elemento que falta por definir es el Signal. Éste sirve para establecer una relación causaconsecuencia. Es decir, representa el concepto de evento. Cuando se invoca un signal se provoca
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
78
un evento y se invocan todos los functors asociados a este signal. Este sistema se usa en las fases
dedicadas a recorrer anotaciones, esto se explica en el apartado 7.2.4.5.
7.2.4.4. Anotaciones
Hasta ahora se han explicado las clases que permiten interactuar con las diferentes estructuras del
código a compilar pero no se ha hablado de las anotaciones. Dos clases son las encargadas de permitir
la interacción con las anotaciones. En primer lugar tenemos la clase que encapsula la anotación en sı́,
PragmaCustomConstruct.
La clase PragmaCustomConstruct hereda su nombre del mcxx puesto que el formato de las anotaciones en Fortran es prácticamente igual al de las anotaciones en C/C++. Esta clase permite obtener el
pragma de la anotación (lo que hemos presentado como sentinel), obtener la directiva, que es el nombre de la anotación (siguiendo el ejemplo de CellSs/SMPSs serı́an task, barrier, etc.), y determinar si
se trata de una directiva o una construcción.
Una anotación, además de la directiva, tiene una lista de cláusulas con determinados parámetros
o argumentos. Desde la directiva se pueden obtener las cláusulas de una anotación. La clase PragmaCustomClause encapsula una cláusula y de ella se pueden obtener los argumentos bien sea como
una lista de expresiones o como una lista de strings de manera que se puedan interpretar de forma
personalizada.
Como comentario no está de más aclarar el doble uso que se hace de la palabra directiva. Normalmente nos referimos a directiva, en contraste con construcción a un tipo de anotación. Pero cuando
se habla de una directiva en general, nos referimos a ambas, construcciones incluidas. Esto es porque
la construcción se considera un tipo concreto de directiva.
7.2.4.5. Fases
El nivel de TL proporciona la base para construir el proceso de compilación. Además de dar herramientas para manejar el AST, la tabla de sı́mbolos y las anotaciones, y herramientas para recorrer
la información del código, proporciona un prototipo para implementar las fases. La clase CompilerPhase es el esquema de una fase. Los elementos básicos que componen una fase son los parámetros y
el DTO. Desde una fase se pueden registrar un conjunto de parámetros que ésta intentará recibir del
driver (son parámetros pasados al compilador externamente), de manera que hay un mecanismo para
poder configurar el comportamiento de las fases desde la lı́nea de comandos.
El DTO (Data Transfer Object), permite registrar información en un objeto que se va pasando
entre las diferentes fases para que éstas puedan compartir información tal como el AST, opciones
de configuración e información calculada por las fases que puede ser usada en fases posteriores. En
resumen, implementa el método de comunicación entre las fases. La implementación del DTO no
7.2. MF95SS
79
tiene muchos secretos y su comportamiento y utilidad se puede ver claramente en el uso que se hace
de él en las fases de CellSs/SMPSs.
Las fases definen un método genérico run que es el que se invoca cuando se dice que la fase es
ejecutada. A la hora de escribir una fase hay que redefinir este método con el comportamiento deseado.
En este bloque también se define una fase especı́fica para recorrer las anotaciones del código.
La clase PragmaCustomCompilerPhase implementa un recorrido en el que los nodos cumplen la
condición de ejecución de su functor cuando éstos contienen una de las directivas (construcciones
incluidas) registradas en una lista. En este caso cuando se cumpla la condición se generará un eveto
(signal) que invocará a todos los functors conectados. Ası́, quién redefine esta clase para escribir una
fase de compilación deberá definir un functor para cada directiva que previamente haya registrado en
la fase (estas dos cosas deberán implementarse en el método de creación de la fase).
Las fases implementadas para el compilador pueden servir de ejemplos clarificadores para entender los conceptos explicados en relación a los recorridos y el mecanismo de definición e invocación
de functors.
7.2.4.6. Generación de código
Durante todo el capı́tulo hemos visto un conjunto de clases que permite, en global, escribir fases
con recorridos sobre el AST y consultar información acerca del AST, los sı́mbolos y tablas de sı́mbolos
etc. También se ha explicado cómo sustituir o modificar partes del AST, pero todo esto serı́a inútil si
no disponemos de ningún método para generar nuestro propio código en una fase.
La clase Source es la encargada de proporcionarnos este método. Se trata de una de las partes
más interesantes que incluye el mcxx. Esta clase es una implementación de una clase de tipo stream
que permite concatenar texto mediante un operador (<<). Ası́, podemos declarar un objeto del tipo
Source e ir añadiendo nuestras lı́neas de códgo en forma de strings. Además permite la conversión
automática de números enteros a strings. También permite concatenar unos con otros aunque no estén
todavı́a definidos ya que el texto final se obtiene recursivamente en el momento en que se pida, como
se puede observar en el ejemplo.
string create_program_unit()
{
Source program;
Source declarations;
Source instructions;
program <<
<<
<<
<<
"PROGRAM P" << endl
declarations
instructions
"end program p";
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
80
generate_declarations(declarations);
generate_instructions(instructions);
return program.get_source();
}
La clave de esta clase está en que, usando los métodos de parsing del frontend se puede obtener el
AST correspondiente al código escrito de manera que se puede añadir al AST original mediante concatenación o sustitución de nodos lista o nodos, respectivamente. Esto se hace en diferentes funciones
dependiendo del tipo de código que se quiera analizar. Si, por ejemplo, se ha escrito una expresión,
se usará el método que analiza expresiones. Éste tiene un punto de entrada en el parser que empieza
la gramática directamente por una producción de expresiones. De la misma manera si se quiere analizar una lista de instrucciones se usará un método diferente o, en otro caso, si se desea analizar una
program unit.
7.2.5.
Modelo de fases
Las fases del compilador son las que definen su comportamiento final. Mediante diferentes conjuntos de fases podrı́amos implementar compiladores que hicieran transformaciones para muy diferentes
propósitos, en este caso para CellSs y SMPSs. En este apartado se explica cómo se han implementado
las fases del compilador siguiento el orden de ejecución de éstas, que se explica en el capı́tulo 6.
7.2.5.1. Configuración
En primer lugar está la fase de configuración, que lee los parámetros pasados por lı́nea de comandos (por el driver) y los pasa mediante el DTO al resto de las fases. Estas opciones permiten modificar
el comportamiento de las fases para adaptarlo según las necesidades (SMPSs o CellSs, caracterı́sticas
de la arquitectura, opciones de configuración, etc.). A continuación se describe cada una de ellas:
• generate task side: El código generado incluirá las tareas y aquellas funciones o subrutinas a
las que llamen las tareas. Esto es necesario para CellSs puesto que, como se explica en 6.2, el
compilador se ejecuta dos veces, una con perfil de PPE y otra con perfil de SPE de manera que
genere el código correspondiente y use el compilador que toque en cada caso.
• generate non task side: De la misma manera, con este parámetro se indica a las fases que deben
generar el código para el programa principal y el resto de subprogramas que no sean tareas ni
sean invocados por las tareas.
• manifest name: El manifest, es el fichero que genera el compilador con las descripciones de
las tareas. Esta información se guardará en el objeto generado por el metadriver y, posterior-
7.2. MF95SS
81
mente, se recuperará a la hora de enlazar todos los objetos que compongan una aplicación
CellSs/SMPSs y generar los task adapters y código adicional de interacción con la biblioteca.
• supports sizeof : El estándar de Fortran 95, no especifica ningún método para obtener el tamaño de una variable. Las aplicaciones CellSs y SMPSs necesitan conocer los tamaños de los
parámetros para poder hacer renombrado de parámetros y, en el caso de CellSs, transferencias
de los mismos. Sin embargo sı́ hay un mecanismo, que puede ser usado con este propósito.
La sentencia INQUIRE (IOLENGTH=VAR TAMAÑO) VARIABLE, permite obtener el tamaño
que ocuparı́a una variable a la hora de ser escrita en un fichero. Pero no en todos los casos el
resultado es correcto. Los compiladores que proveen una extensión no estándar, SIZEOF(...),
para obtener el tamaño de las variables, en ocasiones no dan el mismo valor usando este recurso
que usando INQUIRE. Mediante esta variable se puede indicar al compilador cuál de los dos
métodos debe usar el código generado.
• arch bytes: La interacción entre las aplicaciones CellSs/SMPSs y las bibliotecas es dependiente
de la arquitectura en que se ejecute la aplicación puesto que se pasan referencias a variables del
programa principal a la biblioteca. Las referencias son direcciones de memoria que, según la
arquitectura, pueden ser, por lo común 4 bytes o 8 bytes (arquitecturas de 32 o 64 bits). Con este
parámetro se modifica en la generación de código el tamaño de los campos donde se almacena
la dirección de los parámetros para pasarlos a la biblioteca en la llamada addTask.
• int size: El tamaño de un entero en C/C++, puede variar dependiendo de la arquitectura pero esta
variación no tiene por qué ser igual a la de las direcciones de memoria. Esto quiere decir que no
podemos suponer que un entero vaya a ocupar una cierta cantidad de bytes en una arquitectura
de 32 bits y otra en una de 64 bits siempre. Por ello también es necesario conocer el tamaño
de un entero ya que, por ejemplo, los identificadores de tareas que se usan en el addTask para
indicar a qué tarea se está llamando, son enteros que asigna el metadriver mediante un objeto
escrito en C.
El procedimiento para pasar los parámetros a las fases es el siguiente: en primer lugar, la función
creadora de la fase registra los parámetros en el driver para que éste sea capaz de pasárselos a la fase
en el momento de su ejecución. Para ello, el driver inicializa las fases en uno de los primeros pasos
de su ejecución. A partir de entonces, los parámetros pasados ya están registrados en la fase.
Posteriormente, durante la ejecución de la fase es cuando está disponible el objeto DTO que
será pasado de una fase a otra y entonces es el momento en que la fase de configuración añadirá los
parámetros al DTO.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
82
7.2.5.2. Pre-análisis
Para compilar un fichero con CellSs/SMPSs es necesario conocer la estructura de la aplicación de
manera que podamos determinar dónde están las tareas, y generar un mapa que permita saber cómo
se invocan entre sı́ las funciones y subrutinas del programa. Esto permitirá generar cada uno de los
lados en el momento que corresponda (task side o non task side).
La fase de pre-análisis localiza las funciones y subrutinas del código y incluye en una lista los
subprogramas a los que invoca cada una y en otra lista los subprogramas por los que son invocadas.
En el código del ejemplo que sigue se muestra el funcionamiento de la rutina run de la fase, que define
su comportamiento.
void TL::PreAnalysis::run(DTO &dto)
{
FunctionMap *original_function_map = new FunctionMap();
original_function_map->initialize();
RefPtr<FunctionMap> function_map_ref(original_function_map);
dto.set_object("superscalar_function_table",
function_map_ref);
FunctionMap function_map = dto["superscalar_function_table"];
AST_t fortran_root_ast = dto["ast"];
FortranRoot root(fortran_root_ast);
AST_t ast = root.get_program_unit_list();
// traverse inits
PredicateBool<LANG_IS_TOP_LEVEL_PROGRAM_UNIT> prog_unit_pred;
TraverseASTPredicate program_unit_traverser(prog_unit_pred,
AST_t::NON_RECURSIVE);
DepthTraverse previous_traverse;
DepthTraverse depth_traverse;
// previously get the existing functions names
PreviousHandler previous_handler(function_map);
previous_traverse.add_functor(program_unit_traverser,
previous_handler);
previous_traverse.traverse(ast);
ProgramUnitHandler program_unit_handler(function_map);
depth_traverse.add_functor(program_unit_traverser,
program_unit_handler);
depth_traverse.traverse(ast);
}
7.2. MF95SS
83
En primer lugar se crea el objeto FunctionMap que contendrá la lista de subprogramas y la información descrita anteriormente. Después se accede al DTO para obtener el AST que ha pasado el
driver después de analizar el código. De este AST se obtiene la lista de program units dentro de la cual
vamos a buscar las funciones, subrutinas y programa principal, si lo hay, y añadirlos a el FunctionMap
con la información sobre las rutinas a las que invocan.
El recorrido se hace mediante un objeto de la clase Traverse. A éste hay que añadirle un TraverseASTFunctor, en este caso es el TraverseASTPredicate que identificará los nodos con el atributo
LANG IS TOP LEVEL PROGRAM UNIT. Después se le añade un TraverseFunctor que en este caso
hemos llamado PreviousHandler. A continuación se puede ver su código:
class PreviousHandler : public TraverseFunctor
{
private:
FunctionMap _function_map;
public:
PreviousHandler(FunctionMap function_map)
: _function_map(function_map)
{
}
virtual void preorder(AST_t node){}
virtual void postorder(AST_t node)
{
TopLevelProgramUnit tl_prog_unit(node);
ProgramUnit prog_unit = tl_prog_unit.get_program_unit();
if (!prog_unit.is_subroutine_subprogram() &&
!prog_unit.is_function_subprogram() &&
!prog_unit.is_main_program()) return;
std::string funct_name = prog_unit.get_subprogram_name();
bool existed = _function_map.contains(funct_name);
// autocreate
FunctionInfo &function_info = _function_map[funct_name];
//
// Check if the function was already defined
//
if (existed)
{
std::cerr << " Error: the program unit ’"
<< function_name << "’ already exists.";
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
84
function_info._has_errors = true;
PreAnalysis::fail();
return;
}
//
// Handle the function definition itself
//
function_info._name = function_name;
}
}
De esta manera, el recorrido ejecutará la rutina postorder que se ha implementado cada vez que se
encuentre un nodo en la lista de subrutinas con la etiqueta (extended attribute) LANG IS TOP LEVEL PROGRAM UNI
Lo que hace esta rutina es comprobar si se trata de un subprograma o programa principal, y registrarlo
en el FunctionMap.
Después, como se muestra en el primer código, se hace otro recorrido en el que, para cada subprograma se busca a todas las subrutinas o funciones que invoca para registrar todos los invocados
e invocadores de cada subprograma en el FunctionMap. Esto se hace de la misma manera que el
recorrido anterior, mediante un TraverseFunctor llamado ProgramUnitHandler. Dentro de la rutina
postorder de éste se ha implementado un recorrido que busca las llamadas a subprogramas:
void postorder(AST_t node)
{
...
DepthTraverse depth_traverse;
PredicateBool<LANG_IS_SUBPROGRAM_CALL>
subprogram_call_predicate;
SubprogramCallHandler
subprogram_call_handler(function_info, _function_map);
depth_traverse.add_predicate
(subprogram_call_predicate, subprogram_call_handler);
depth_traverse.traverse(node);
...
}
El SubprogramCallHandler registrará, para el subprograma actual, cada vez que en éste haya
una llamada en el FunctionMap. En la entrada del subprograma llamado se registrará al actual como
invocador y en la del subprograma actual se registrará el llamado como invocado.
7.2. MF95SS
85
7.2.5.3. Análisis de tareas
La fase TaskAnalysis se distingue de la anterior en que no se trata de una fase normal sino que es
una PragmaCustomCompilerPhase. Esto es, una fase que recorre las anotaciones del código. Este tipo
de fases, se encargan de registrar una determinada anotación de manera que en el driver se registra
qué anotaciones existen y el frontend comprueba que una anotación esté registrada cuando las analiza.
Esto se hace en el momento de la inicialización de la fase (ver las dos primeras lı́neas de la función
creadora):
TaskAnalysis() : PragmaCustomCompilerPhase("CSS")
{
register_construct("TASK");
register_construct("TARGET");
on_directive_post["TASK"]
.connect(functor(&TaskAnalysis::process_task, *this));
on_directive_post["TARGET"]
.connect(functor(&TaskAnalysis::process_target, *this));
}
Las dos lı́neas siguientes se encargan de conectar un functor con el recorrido de la fase de manera
que cuando se encuentre una anotación, se comprobará que ésta es, en el primer caso, !$CSS TASK y
si lo es se ejecutará el functor asociado. En este caso el functor es creado en la misma lı́nea usando
una función de la propia fase. En esta función estará el comportamiento de la fase.
El método run de esta fase simplemente invoca al método tipo definido en la superclase (la PragmaCustomCompilerPhase) que es el que se encarga de hacer el recorrido.
El comportamiento de la fase, implementado en las funciones process task y process target, registra en la definición de la rutina en el FunctionMap si se trata de una tarea task side o, por lo contrario
non task side.
7.2.5.4. Function router
En la sección anterior se han explicado muchos detalles de los recorridos con el propósito de hacer
comprensible su funcionamiento. También se ve la conexión entre las clases de LangConstruct y los
recorridos, a través del nodo de AST que recibe la función que define el comportamiento del recorrido.
En esta sección no se introduce ninguna novedad respecto al uso de las herramientas descritas en
el apartado dedicado a TL de manera que no aporta nada nuevo ver muchos detalles del código de la
fase function router.
La función de la fase, es determinar qué parte de los program units son tareas o funciones y
subrutinas invocadas desde tareas (task side) y qué parte no.
86
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
7.2.5.5. Transformación de llamadas a tareas
Ésta es quizá la fase más complicada del proceso de compilación y la parte donde tienen lugar
las transformaciones de código más significativas del compilador de CellSs/SMPSs. En esta sección
prestaremos más atención al uso que se hace de los elementos de LangConstruct, estructuras del
frontend representadas en TL y a la generación de código. Esto nos ayudará a comprender mejor el
funcionamiento global del compilador y a comprender las transformaciones que hay que hacer.
A grandes rasgos, la etapa recorre todos los subprogramas que ni son tareas ni son invocadas
por tareas (non task side). En cada una de ellas busca las interfaces con anotación de tarea donde
se indica qué subrutinas son tareas y se aporta información sobre sus parámetros (ver sección 5.1).
Estas interfaces se registran en una lista de tareas. Luego se buscan todas las llamadas a subrutinas del
código del subprograma y se sustituyen por la llamada a addTask que corresponda.
Finalmente se crea un fichero, el manifest, que incluye el nombre de todas las tareas vistas y el
número de parámetros. Éste, recordemos, será usado por el metadriver para generar los adaptadores.
Además, y según si se está compilando para CellSs o para SMPSs se filtrarán los subprogramas que
contendrá el fichero de salida del compilador. Ası́, en el caso de CellSs, y dependiendo del perfil con
el que se esté ejecutando, se eliminarán todos los subprogramas del lado de las tareas task side (perfil
PPE) o, por lo contrario, se eliminarán los que no estén relacionados con las tareas non task side (perfil
SPE). En el caso de SMPSs se dejarán pasar todos los subprogramas del fichero.
Para explicar los detalles de la fase nos centraremos en la función que se ejecuta cuando se recorren
los subprogramas de la aplicación (el recorrido más externo). Lo primero que hace esta función es
comprobar que el subprograma actual no es una tarea consultando el FunctionMap recibido en el DTO
de fases anteriores. Si, efectivamente, no se trata de una tarea, el primer paso es buscar las interfaces
para conocer a qué tareas puede invocar este subprograma. Esto se hace mediante un recorrido por
todo el subprograma buscando interfaces que tengan anotación. Para este recorrido hemos escrito un
Predicate que filtrará los nodos sólo aceptando los que son interfaces de subrutinas:
class TaskPredicate : public PredicateBool<LANG_IS_PRAGMA_CUSTOM_CONSTRUCT>
{
virtual bool operator()(AST_t& ast) const
{
if (!PredicateBool<LANG_IS_PRAGMA_CUSTOM_CONSTRUCT>::operator()(ast))
{
return false;
}
PragmaCustomConstruct construct(ast);
return construct.get_directive() == "TASK";
}
};
7.2. MF95SS
87
De hecho, como se puede observar, en ningún momento hay referencia a interfaces. La razón es
que por cómo hemos definido el lenguaje, dentro de un subprograma la anotación !$CSS TASK sólo
está permitida en interfaces de subrutinas. El Predicate mostrado simplemente busca los nodos con
una anotación del tipo construcción de tarea.
Para cada tarea, necesitaremos, a la hora de transformar el código, la información sobre sus
parámetros y la información añadida en la anotación (si es de alta prioridad o no). Usando el sı́mbolo
de la tarea podremos obtener toda la información sobre los parámetros. En el siguiente extracto de
código se puede ver una versión simplificada de lo que hace el compilador para obtener el sı́mbolo
usando el nodo de la interfaz obtenida y las clases vistas en el apartado de TL:
void TaskCallsTransformations::TaskHandler::postorder(AST_t node)
{
...
PragmaCustomConstruct construct(node);
SubprogramInterface interface(construct.get_content());
Symbol if_symbol = interface.get_interface_symbol();
...
high_priority = construct.get_clause("HIGHPRIORITY").is_defined();
...
}
La segunda parte muestra cómo obtener una cláusula de una anotación. Una vez tenemos toda la
información de todas las tareas en una lista (se ha ejecutado recorrido), estamos en condiciones de
buscar las llamadas a función y realizar las sustituciones.
Para ello volveremos al contexto del subprograma en que nos hemos situado antes. Ahora un nuevo
recorrido se hace sobre las llamadas a subrutina donde, para cada llamada, lo primero es buscar en la
lista si el sı́mbolo invocado coincide con alguna tarea de la lista. En caso afirmativo se trata de una
llamada a tarea y deberá ser sustituı́da por una llamada a la bibioteca. Para explicar la sustitución de
una manera sencilla pondremos otro extracto del código, en este caso se muestran los pasos necesarios
para crear la llamada, obtener el AST e intercambiar el AST correspondiente a la llamada original por
el actual:
void TaskCallsTransforms::SubroutineCallHandler::postorder(AST_t node)
{
...
SubprogramCall call(node);
// de la lista de tareas
TaskDescriptor &task_descriptor = _task_set[symbol];
...
Source add_task;
add_task << "CALL css_fortran_add_task("
<< task_name << "_id__cssgenerated, "
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
88
<<
<<
<<
<<
(task_descriptor._high_priority ? "1" : "0") << ", "
symbol.procedure_get_num_parameters() << ", "
"params__cssgenerated"
")\n";
...
AST_t tree = add_task.parse_statement(node);
node.replace_in_list(tree);
Aquı́ se puede observar el uso de la clase Source para escribir código en Fortran y, usando la función parse statement() obtener el AST por el que es reemplazada la llamada en la lista de instrucciones
en la que se encuentra.
Antes de proseguir con la explicación de lo que hace la etapa en este punto, es conveniente recordar qué información es necesario pasar a la biblioteca para que sea capaz de ejecutar una tarea.
En el ejemplo donde se crea el código que llama a css fortran add task (addTask) se añaden dos
parámetros que no se puede deducir de dónde vienen a simple vista. Se trata del que se escribirá como
task name id cssgenerated y el params cssgenerated. El primero representa el identificador de la
tarea, que será una variable global generada por el metadriver en tiempo de enlazado; generará uno
para cada tarea. El segundo es una estructura que contiene la lista de parámetros que se pasan a la tarea
en la llamada original con la información adicional requerida por la biblioteca. Ambos son variables
declaradas en los subprogramas que invocan tareas.
Lista de parámetros
La estructura params cssgenerated es la parte más complicada de esta fase. Primero veremos
cómo está declarada, esto nos ayudará a ver su correspondencia con la interfaz de la biblioteca y
entender qué es necesario saber sobre la tarea para completarla. A continuación se muestra la declaración de la estructura y el parámetro concreto que se pasa a los addTask de un subprograma
(params cssgenerated):
TYPE PARAMETER
SEQUENCE
INTEGER(1) :: DIRECTION
INTEGER(1) :: SCALAR
INTEGER(1) :: DIMENSIONS
INTEGER(1) :: PADDING(" << (_arch_bytes - 3) <<")
INTEGER("<< _arch_bytes << ") :: SIZE
INTEGER("<< _arch_bytes << ") :: ADDRESS
INTEGER("<< _arch_bytes << ") :: BOUNDS_P
END TYPE
TYPE(PARAMETER) :: params__cssgenerated(NUM_PARS)
7.2. MF95SS
89
La variable arch bytes, en función de la arquitectura para la que se haya configurado el compilador, determina el tamaño de una dirección de memoria, que es lo que se pasará en el campo ADDRESS,
el parámetro de la tarea. En el resto de campos que aparece dicha variable se usa básicamente para
hacer coincidir el tamaño y la organización de los campos de la estructura de Fortran con los que
define la biblioteca de CellSs/SMPSs. No todos los campos de esta estructura se usan en la versión
actual de CellSs/SMPSs. Sólo nos iteresan la dirección del parámetro, si es un escalar (no un array),
el tamaño de la variable y su dirección.
El array declarado al final será la lista de parámetros que se le pasará finalmente a la biblioteca
en el addTask. NUM PARS tomará el valor del máximo número de parámetros que tiene una tarea
invocada desde el subprograma.
Campos de los parámetros
Volviendo a la generación de código del addTask, para cada llamada a tarea será necesario completar la lista de parámetros. Del sı́mbolo de la tarea podemos obtener los parámetros pasados de
manera que podremos recorrerlos. Para cada parámetro habrá que completar los campos que se han
mencionado antes. Los dos primeros son fáciles de obtener, tanto la dirección como el número de
dimensiones del parámetro están explı́citamente en el sı́mbolo de manera que simplemente habrá que
generar el código que los añada a la estructura params cssgenerated en la posición del parámetro
que se esté completando.
A la hora de añadir a la lista la dirección del parámetro el tema se complica un poco. Hay expresiones en Fortran que podrı́an alocatar (reservar) memoria para poner el resultado de la expresión (un
conjunto de partes de un array o los elementos de un campo de una estructura para todas las posiciones
de un array, por ejemplo). Para comprender por qué esto puede ser un problema hay que pensar en
el modelo de programación. Cuando el CellSs/SMPSs se registra una tarea, a la biblioteca se le pasa
un descriptor con las direcciones de los parámetros para que cuando la tarea se vaya a ejecutar pueda
acceder a ellos (en el caso de los arrays y los escalares que son de salida o entrada/salida). Por tanto,
es una mala idea crear un array temporal y pasárselo a una tarea porque no sabemos cuándo dejará de
existir.
Resumiendo, habrá que comprobar si el parámetro alocata memoria cuando se evalúa la expresión, y la compilación será erronea si esto ocurre con un parámetro que sea array o un escalar que
sea resultado de la tarea. En los casos en que sea un escalar de sólo entrada, y también en el caso
especial que el parámetro pasado sea una constante (PARAMETER, no confundir con el parámetro,
este atributo del sı́mbolo quiere decir que es una constante, si existe), se creará una variable de el tipo
correspondiente y se le asignará el valor o la expresión del parámetro. Con esto habremos decidido si
el parámetro es válido y, en el último caso, la variable que hará las veces de parámetro.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
90
Finalmente podemos generar el código necesario para obtener la dirección del parámetro y asignarla en el campo correspondiente. Esto lo haremos con la instrucción LOC(..), una extensión no
estándar de Fortran pero que incluyen casi todos los compiladores de Fortran modernos. LOC devuelve un entero con la dirección de memoria de la variable que se le pasa. Es la única manera que
tenemos para obtener la dirección de memoria de manera que podamos asignarla a una variable (en
este caso a un campo de una estructura en un array).
Cálculo del tamaño de un parámetro
Ahora sólo nos queda calcular el tamaño del parámetro. En el caso de los escalares es fácil, usando
la función SIZEOF en el caso que esté soportada o, por lo contrario la llamada INQUIRE(..), podemos
obtener el tamaño de una variable, el compilador nativo se encargará de calcular este valor. En el
caso de los arrays es más complicado, el tamaño de los arrays dependerá del número de elementos
que tengan. Éste se define en la interfaz de la tarea. La sintaxis para especificar las dimensiónes de un
array en Fortran (con el método explı́cito de definir la dimensión de un array, que es el único soportado
por el compilador para CellSs/SMPSs), es como la siguiente:
REAL :: A(1:100,1:100)
En este ejemplo tenemos un array de dos dimensiones en el que la primera dimension va de 1
(lı́mite inferior) a 100 (lı́mite superior) y la segunda igual. Las dimensiones pueden ser expresiones
que dependan de parámetros pasados a la tarea o de constantes definidas en la misma. Está claro que
si generamos código que calcule el tamaño de un parámetro (array) usando las expresiones definidas
en la interfaz de la tarea, no funcionará porque estas expresiones están en un contexto diferente al de
la llamada a la tarea y, algunos sı́mbolos usados en las expresiones pueden no existir en ese contexto.
En el ejemplo que se muestra a continuación se podrá ver con más claridad:
Llamada a la tarea:
...
REAL :: C(10,10)
...
CALL TASK_A(C,d)
...
Interfaz de la tarea:
!$CSS TASK
SUBROUTINE TASK_A(ARG,len)
INTEGER, PARAMETER :: C=10
INTEGER, INTENT(IN) :: len
REAL, INTENT(INOUT) :: ARG(1:C,len)
END SUBROUTINE
7.2. MF95SS
91
La expresión que calcuları́a el número de elementos de una dimensión serı́a del estilo (lim sup −
lim inf + 1). Multiplicando el número de elementos de todas las dimensiones y, finalmente, multiplicándolo por el tamaño de un elemento nos dará el tamaño final del array (tendremos que generar
código que declare una variable auxiliar del tipo del array para conocer, el el código generado, el
tamaño de un sólo elemento). La expresión directa para el ejemplo serı́a:
...
REAL :: AUX_VAR
...
C_SIZE = (C - 1 +1)*(len - 1 + 1)*SIZEOF(AUX_VAR)
El resultado de evaluar esta expresión en el contexto del subprograma desde donde se llama a
la tarea es erróneo ya que C no quiere decir lo mismo que dentro de la tarea y len, de hecho lo
desconocemos.
La solución al problema es llevar a cabo una sustitución en la que todas las constantes que se
encuentren en las expresiones de los lı́mites de un array sean resueltas intercambiadas por su valor,
y todas las referencias a variables que se encuentren sean sustituı́das por el parámetro real que se ha
pasado a la tarea en la llamada original. Ası́, después de aplicar esto la expresión anterior se transforma
en la siguiente:
...
REAL :: AUX_VAR
...
C_SIZE = (10 - 1 +1)*(d - 1 + 1)*SIZEOF(AUX_VAR)
Ahora la expresión puede ser evaluada correctamente en el contexto del subprograma que invoca a
la tarea y podemos generar una expresión a la hora de completar el campo SIZE de los parámetros que
sean arrays para una llamada a addTask. La rutina que hace las sustituciones de variables es interesante
para ver un poco más de código que usa las clases definidas en TL y valorar su utilidad. Una versión
simplificada puede servir como ejemplo para ver esto:
AST_t replace_bound_data_references(AST_t original_expression,
SubprogramCall task_call, Symbol subprogram_symbol)
{
// obtener una copia de la expresión
AST_t new_bound = original_expression.duplicate();
Expression bound_exp(new_bound);
List<VariableRef> ref_list = bound_exp.get_var_refs();
// no hay variables que sustituir
if (ref_list.empty()) return new_bound;
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
92
// Recorremos las Referencias a variables en la expresión
for (it = ref_list.begin(); it != ref_list.end(); it++)
{
// Comprovamos si se trata de un parámetro de la tarea invocada
std::string symbol_name = (*it).get_symbol_name();
int index = subprogram_symbol.get_argument_index(symbol_name);
if (index >= 0) // Es un parámetro
{
// Buscamos el parámetro en la llamada
Expression parameter = task_call.get_parameter(index);
AST_t new_exp_ast = parameter.get_ast().duplicate();
// sustituimos la referencia actual por el parámetro pasado
Source new_expression;
new_exp << "(" << new_exp_ast.prettyprint() << ")";
(*it).get_ast()
.replace_with(new_exp.parse_expr(task_call.get_ast()));
}else{ // Es una referencia a una variable local
// 1. Buscamos el sı́mbolo en la tarea
Symtab st = original_expression.get_symtab();
Symbol symbol = st.query_symbol(symbol_name);
// 2. Comprobamos si es una constante ("PARAMETER")
Attributes attr = symbol.get_attributes();
if (!attr.is_parameter())
{
cout << "Error"
TaskCallsTransformations::fail();
}
// 3. Obtener AST con la inicialización de la constante
AST_t const_def = attr.get_tree_initializer();
// 4. Sustituimos recursivamente los sı́mbolos
//
que puedan aparecer (otras constantes)
AST_t new_const_def;
new_const_def = replace_bound_data_references(const_def,
task_call, subprogram_symbol);
Source new_expr;
new_expr << "(" << new_const_def.prettyprint() << ")";
(*it).get_ast()
7.2. MF95SS
93
.replace_with(new_expr.parse_expr(task_call.get_ast()));
}
}
return new_bound;
}
La función recibe un AST con la expresión original correspondiente a un lı́mite de una dimensión
de un array. También recibe la llamada a la tarea y el sı́mbolo de la función llamada del que obtendremos información acerca de los parámetros. El AST recibido está en el contexto de la tarea por lo
que podemos obtener la tabla de sı́mbolos y ver las definiciones de los sı́mbolos que aparecen en la
expresión.
Con estas últimas transformaciones ya tenemos todo lo necesario para construir la llamada a la biblioteca (addTask). El código real del compilador añade más código que soluciona algunos problemas
técnicos encontrados en el momento de probar las aplicaciones creadas.
7.2.5.6. Transformación de directivas
La última fase es la encargada de eliminar las anotaciones, sustituyéndolas en los casos que sea
necesario, por invocaciones a rutinas de interacción con la biblioteca. Durante las fases previas, en
algun momento, se han usado las anotaciones para conocer las tareas pero en ningún momento se
ha cambiado el código en esos puntos lo que quiere decir que las anotaciones siguen estando ahı́.
Por tanto, esta fase implementa una subclase de PragmaCustomCompilerPhase que recorre todas las
anotaciones definidas para CellSs/SMPSs haciendo las siguientes acciones:
• START: Se sustituye por tres llamadas. Dos de ellas, css fortran pre init() y css fortran init()
corresponden a interfaces de la biblioteca descritas en 5.2. La otra, task registration cssgenerated()
que es invocada entre las dos anteriores, la crea el metadriver cuando conoce todas las tareas de
la aplicación. Su función es registrar en la biblioteca las tareas.
• FINISH: Se sustituye por la llamada a la biblioteca css fortran finish().
• BARRIER: Se sustituye por la llamada a la biblioteca css fortran barrier().
• WAIT: Cuando se encuentra una directiva WAIT es necesario obtener la cláusula ON(...) cuyos
parametros forman la lista de variables que hay que pasar a la biblioteca para que la sincronización tenga lugar. Para ello se completa un array con la lista de direcciones de las variables y se
le pasa a la llamada css fortran wait on(...) de la biblioteca.
• TASK: La anotación es eliminada.
• TARGET: La anotación es eliminada.
CAPÍTULO 7. DESARROLLO DEL COMPILADOR
94
Una vez finalizada esta fase, el código ya tiene hechas todas las modificaciones y puede pasar a
ser compilado por el compilador nativo que se haya configurado. Para ello el driver se encargará de
generar el texto del código modificado, usando el módulo prettyprint del frontend, para escribirlo en
un fichero y lanzar el compilador que generará el objeto de salida del mf95ss.
7.3. Tecnologı́as
Uno de los aspectos importantes a tener en cuenta en todo proyecto de informática son las tecnologı́as que se van a utilizar. Lo común es tratar este asunto en los primeros pasos del proyecto, cuando
se está estudiando el problema y se proponen las alternativas para solucionarlo.
En el caso concreto de este proyecto las tecnologı́as vienen dadas por el propio entorno del proyecto y por las soluciones disponibles para los problemas concreotos que se han planteado durante
el estudio del mismo. Ası́ no tiene sentido hacer un estudio previo para decidir las tecnologı́as y el
entorno de desarrollo que se van a usar.
Por ello hasta ahora hemos prestado la atención justa a este asunto. Pero esto no quiere decir que
no sea necesario explicar las tecnologı́as usadas en el compilador y en su desarrollo.
En primer lugar hablaremos de el entrono de desarrollo, que engloba todas aquellas herramientas
utilizadas para crear el compilador. Se trata de un proyecto de código abierto (opensource y GPL)
destinado a plataformas GNU/Linux. Resulta lógico y conveniente, pues, que se utilice un compilador
de GNU (gcc) y las herramientas autotools para la configuración y compilación de la aplicación.
Para desarrollar la parte de análisis se han utilizado la herramienta lexer y una versión del generador de gramáticas de GNU (bison) adaptada por Roger Ferrer, llamada bison-rofi, para las gramáticas
escritas para el frontend del mf95. Otras herramientas comunes de los sistemas GNU/Linux como
gperf se han usado para la generación automática de listas y mapas.
En cuanto a los lenguajes usados, como ya se ha comentado, la parte del driver y el frontend del
mf95ss se han hecho en C. Las partes de TL, las Fases y el metadriver, están hechas en C++.
En cuanto a las tecnologı́as usadas en el compilador, la más destacable es el uso de bibliotecas
cargadas en tiempo de ejecución para las fases. Esto permite que se puedan cargar diferentes perfiles
de manera dinámica, sin tener que recompilar la aplicación, y ası́ poder tener un compilador más
versátil. Recordemos que las fases son las que llevan a cabo las transformaciones de código.
Capı́tulo
8
Testing y resultados
Para poder evaluar si se han cumplido los objetivos definidos en el proyecto es necesario comprobar el buen funcionamiento del compilador. Esto significa asegurar que cuando se intenta compilar un
programa CellSs o SMPSs correcto el resultado va a ser una aplicación CellSs o SMPSs que funcione.
También es importante evaluar la respuesta del compilador ante entradas erróneas. Sin embargo, el
compilador desarrollado en este proyecto no comprueba completamente que la aplicación pasada sea
correcta. Esto es una tarea que se delega al compilador nativo que se esté utilizando.
Otro aspecto importante es comprobar que las aplicaciones creadas con el compilador de Fortran
para CellSs y SMPSs funcionan de una manera aceptable. No habrá valido la pena el esfuerzo si, por
ejemplo, el código generado por una aplicación implica un gran coste y, por tanto, no se puede sacar
partido del modelo de programación.
En este capı́tulo se tratan estos dos aspectos clave para poder sacar conclusiones del resultado
del proyecto, el compilador. En primer lugar explicaremos las pruebas hechas para comprobar que
el código generado por el compilador es correcto y las aplicaciones creadas funcionan bien. Después analizaremos los resultados de ejecutar aplicaciones escritas en Fortran para comprobar que su
rendimiento sea aceptable.
8.1. Testing
Entre las pruebas realizadas para comprobar el correcto funcionamiento del compilador se pueden
distinguir dos grupos. En primer lugar están aquellas pruebas destinadas a comprobar que el compilador funciona correctamente par crear aplicaciones CellSs/SMPSs a partir de códigos correctos. Las
segundas nos ayudarán a ver cómo responde el compilador ante entradas erróneas.
En cuanto a las pruebas del primer conjunto, es necesario comprobar que se pueden usar todas las
95
CAPÍTULO 8. TESTING Y RESULTADOS
96
funcionalidades del modelo de programación descritas. Para probar esto se han escrito varias aplicaciones utilizando las anotaciones y la sintáxis que se define en este documento. Esto nos ha permitido
comprobar que el código generado por el compilador es el esperado en todos los casos. Hay que
destacar que el programador es responsable de poner las anotaciones START y FINISH una sola vez
y no invocar tareas ni colocar otras anotaciones de manera que puedan ser ejecutadas antes de un
START o después de un FINISH. El programador también se ha de responsabilizar de comprobar que
las funciones que invocan tareas tengan las interfaces de las tareas (con anotación) a las que invocan
definidas. Por lo contrario CellSs/SMPSs no podrá enlazarlas o, como mucho, producirá aplicaciones
que no funcionen.
En este sentido no se han encontrado errores en la generación de código tanto en CellSs como en
SMPSs. En el CD adjunto a este documento hay un directorio “test” donde se encuentran las pruebas
realizadas.
En cuanto a las del segundo grupo, se ha analizado cómo se comporta el compilador ante los
errores más comunes en los códigos de los usuarios de CellSs/SMPSs. Estas pruebas se encuentran en
el directorio “test/errors”.
Dirección de los parámetros
Uno de los errores más comunes que cometen los usuarios de CellSs y SMPSs es olvidarse de
especificar la dirección (INTENT) de los parámetros de las tareas. Con el programa que se muestra a
continuación se ha probado esto y lo que sigue es la salida del compilador al intentar compilar una
aplicación con este error.
program p
implicit none
real :: A
interface
!$CSS TASK
subroutine task(a,b)
real::a, b
end subroutine
end interface
!$CSS START
CALL task(A,2.0)
!$CSS FINISH
end program
8.1. TESTING
97
!$CSS TASK
subroutine task(a,b)
REAL :: a,b
end subroutine
$ cellss-cc -ono_intent no_intent.f90
no_intent.f90:13 Incorrect direction specification.
Número de parámetros incorrecto
A veces ocurre también que el usuario se deja algún parámetro en la invocación a la tarea. Para
probar esto hemos intentado compilar el siguiente código:
program p
integer :: a, b, c
interface
!$CSS TASK
subroutine task_b(a, b)
integer, intent(inout) :: a, b
end subroutine
end interface
!$CSS START
call task_b(a, b, c)
!$CSS FINISH
end program
El error producido por el compilador en este caso es el siguiente:
Index out of bounds when accessing to dummy_arguments[2],
there are 2 elements in the array.
Otros errores
Otros errores comunes, que ya se han mencionado antes, son los descuidos a la hora de marcar las
tareas. En el caso de que el programador se olvide de marcar la subrutina de una tarea, los resultados
varı́an en función de si se está compilando una aplicación CellSs o SMPSs. En el caso del Cell/BE,
la aplicación no podrı́a enlazarse correctamente ya que las tareas han de ser compiladas por separado.
De la misma manera, si el error es que no se ha especificado una interfaz para una cierta tarea invocada
CAPÍTULO 8. TESTING Y RESULTADOS
98
desde una program unit, la aplicación tampoco podrı́a ser enlazada ya que en el programa principal se
harı́a referencia a una subrutina que no ha sido compilada para la PPE.
En el caso de SMPSs, las aplicaciones podrı́an ser compiladas sin problemas pero su comportamiento será anormal. Se trata de un problema difı́cil de controlar de manera que es exclusiva responsabilidad del programador controlar que todas las tareas estén correctamente indicadas.
8.2. Resultados
No sólo es importante comprobar que el código generado sea correcto, también es necesario comprobar si las aplicaciones creadas funcionan bien y evaluar el impacto del código generado en el
rendimiento de la aplicación.
Actualmente no hay muchas aplicaciones CellSs/SMPSs en Fortran puesto que la distribución
acaba de ser publicada. Por esta razón nos centraremos en una aplicación que hemos estado adaptando
en el BSC para CellSs/SMPSs.
La aplicación se llama “argon” y es una simulación del comportamiento de un conjunto de átomos
de argon en una entorno con condiciones periódicas de contorno. Estas aplicaciones son conocidas
como simulaciones de dinámica moleculare (Molecular Dynamics Simulations). Por sus propiedades
se puede explotar el paralelismo de una manera bastante satisfactoria. El algoritmo opera sobre un
conjunto de átomos para los que actualiza su estado (velocidad y posición) en cada paso de la dinámica
(iteración). Para ello ha de tener en cuenta las interacciones entre todas las moléculas del conjunto. El
programa tiene el siguiente aspecto:
!$CSS START
do step=1,niter
do ii=1, N, BSIZE
do jj=1, N, BSIZE
call velocity(BSIZE, ii, jj, x(ii), y(ii), z(ii),
x(jj), y(jj), z(jj),
vx(ii), vy(ii), vz(ii))
enddo
enddo
do jj=1, N, BSIZE
call v_mod(BSIZE, v(jj), vx(jj), vy(jj), vz(jj))
enddo
!$CSS BARRIER
! Reducción, se calcula una media ponderada
8.2. RESULTADOS
99
! de la velocidad de cada átomo
tins=0.e0
do i=1,N
tins=mkg*v(i)**2/3.e0/kb+tins
enddo
tins=tins/N
lam1=sqrt(t/tins)
do ii=1, N, BSIZE
call upd_pos(BSIZE, lam1, vx(ii), vy(ii), vz(ii), x(ii), y(ii), z(ii))
enddo
enddo
!$CSS FINISH
Usando esta aplicación se descubrió un error producido por causa de las optimizaciones introducidas por algunos compiladores de Fortran que asumı́an que si una variable no era usada por ninguna
función o subrutina la podı́an eliminar. Ası́, como las tareas no reciben directamente los parámetros
sino que reciben una referencia a éstos, el compilador no consideraba esto un “uso” de la variable que
se pasaba a las tareas desde el programa principal y las eliminaba.
En cuanto al funcionamiento de la aplicación, a falta de más aplicaciones de más largo recorrido
(las que tenemos por ahora son pocas y de pruebas), no se pueden dar medidas muy significativas
para confirmar que las aplicaciones en Fortran para CellSs y SMPSs funcionan a la perfección. Sı́ se
puede, en cambio, estudiar el impacto del código generado por el compilador en el funcionamiento de
las aplicaciones comparándolo con el funcionamiento de aplicaciones ya probadas en C.
El punto crı́tico en este sentido es el momento en que se completa la estructura de parámetros y se
invoca a la rutina AddTask. El tiempo que se tarda en ejecutar esta rutina es uno de los puntos crı́ticos
en CellSs/SMPSs. Por esta razon nos interesa medir el impacto del código generado por el compilador
de Fortran a la hora de invocar tareas. Para este propóstito se ha creado un programa de pruebas que
se puede encontrar en el CD adjunto a la memoria en la carpeta /test.
El programa de pruebas simplemente ejecuta un bucle que invoca muchas veces la misma tarea.
Con esta sencilla prueba vamos a comparar el tiempo que el programa está añadiendo tareas. Para ello
usaremos una traza paraver y mediremos cuánto tiempo se ejecuta el código del usuario (esto incluye
el código añadido por el compilador) entre la anotación START y la anotación FINISH. También
usaremos como referencia orientativa el tiempo que tarda la rutina AddTask en ejecutarse. Hay dos
versiones del programa, una en C y otra en Fortran.
En la tabla 8.1 se muestran los resultados de esta prueba. Se ha hecho la misma prueba tanto para
CellSs como para SMPSs. Se puede apreciar una diferencia de un 4.8 %, no es un resultado malo pero
tampoco es demasiado bueno. En el caso de SMPSs la diferencia es un poco mayor (sobre el 12 %). El
tiempo de AddTask, que nos sirve de referencia, deberı́a ser el mismo tanto para C como para Fortran
puesto que las dos aplicaciones crean el mismo número de tareas y éstas son idénticas. Esto nos puede
CAPÍTULO 8. TESTING Y RESULTADOS
100
ayudar a hacer una idea de los errores en la medida que se pueden producir.
A la hora de evaluar los resultados hay que tener en cuenta que el compilador de Fortran genera
código adicional para, como se ha comentado antes, evitar que las optimizaciones del compilador
eliminen los parámetros pasados a las tareas. Se ha hecho una prueba eliminando este código pero,
como era de esperar, los resultados no cambian. La razón es que el código añadido se reduce a una
asignación y una sentencia condicional que nunca se ejecuta. Ası́, compilando como lo hemos hecho
(activando los flags de optimización -O3) el código añadido tiene un efecto muy pequeño.
Estos resultados no son catastróficos ya que una variación del orden del 10 % en el AddTask no
supone un gran problema. Sin embargo sı́ que deben ser objeto de estudio y hay que intentar reducir
estos valores. No significa que el código generado por el compilador sea incorrecto pero sı́ que debe
ser mejor optimizado de manera que habrá que estudiar de qué manera se puede hacer esto.
Código del usuario
AddTask
Total
CellSs
C
Fortran
1.77 µs 1.86 µs
3.15 µs 3.07 µs
4.92 µs 4.93 µs
SMPSs
C
Fortran
1.81 µs 2.06 µs
5.60 µs 5.37 µs
7.41 µs 7.43 µs
Cuadro 8.1: Análisis del impacto del código añadido por el compilador (tiempos en microsegundos).
A pesar de lo comentado antes, se ha probado alguna aplicación sencilla como la multiplicación
de matrices en CellSs. Los resultados muestran un funcionamiento peor que el de la misma aplicación
escrita en C aunque la aplicación escala correctamente. Una hipótesis es que se deba a la alineación
de los parámetros. Esto añade un factor constante y justificarı́a estos resultados.
Capı́tulo
9
Conclusiones
En este capı́tulo se evalúa el trabajo realizado según diversos criterios. En primer lugar se valora
la satisfacción de los objetivos marcados al principio del proyecto. Lo segundo es evaluar la planificación del proyecto comparando las horas de trabajo definidas al principio del proyecto con las horas
reales dedicadas. Teniendo en cuenta el tiempo dedicado al proyecto y los recursos utilizados para
su realización podemos evaluar el coste económico del proyecto en la siguiente sección. También se
presentan algunas ideas orientativas sobre por dónde irá el trabajo futuro por lo que al compilador
respecta. Y, finalmente una valoración personal de lo que ha sido el proyecto para mı́.
9.1. Satisfacción de los objetivos
En el capı́tulo dedicado a los objetivos del proyecto (capı́tulo 3). Se define un objetivo principal
que es desarrollar un compilador de Fortran para CellSs/SMPSs que permita aprovechar los recursos
de las arquitecturas multicore o multiprocesador (Cell/BE, SMP) usando dichos modelos de programación. En este sentido se pude considerar que el objetivo ha sido cumplido puesto que, usando el
compilador desarrollado, se pueden crear aplicaciones tanto CellSs como SMPSs que permiten aprovechar la potencia de cálculo de las arquitecturas para las que estos modelos de programación están
diseñados. El compilado desarrollado se distribuye actualmente con la versión 2.0 de CellSs y SMPSs
1.
En cuanto a las tareas definidas para cumplir este objetivo, es necesario justificar qué tareas han
sido modificadas y/o priorizadas y por qué razón. En el momento de definir las tareas necesarias para
cumplir los objetivos del proyecto se consideró necesario que el compilador soportara extensiones de
otros compiladores (como el XL Fortran de IBM o gfortran, de GNU). Algunas extensiones eran ya
soportadas por el compilador original (como la llamada LOC). Pero muchas no han sido añadidas ya
1
ver http://www.bsc.es/cellsuperscalar y http://www.bsc.es/smpsuperscalar
101
CAPÍTULO 9. CONCLUSIONES
102
que su ausencia no impedı́a que el compilador fuera capaz de crear aplicaciones CellSs/SMPSs. Este
asunto es complicado puesto que cada fabricante define sus propias extensiones al lenguaje. Muchas
de ellas coinciden pero otras no y, por ello, es necesario un estudio riguroso antes de añadirlas todas
al compilador de CellSs/SMPSs.
También se habla del alineamiento de variables en el caso de CellSs. La biblioteca del worker
actual soporta variables no alineadas como parámetros. A pesar de esto es necesario estudiar cómo se
puede conseguir el alineamiento en Fortran para mejorar el funcionamiento de muchas aplicaciones.
Hay que tener en cuenta que los compiladores de Fortran no suelen ofrecer extensiones con este
proósito (alineación de variables).
9.2. Planificación del proyecto
En la tabla 9.1 se muestra una comparativa entre la planificación original y el tiempo final que se
ha dedicado a cada tarea para completar el proyecto. A parte del incremento en horas, que se explica
a continuación, hay que explicar también que el retraso en el tiempo viene dado por un cambio en la
filosofı́a de funcionamiento del compilador. Si bien a mediados de Febrero ya habı́a una versión del
compilador que funcionaba correctamente para CellSs, la integración con el metadriver (de manera
que la compilación para programas en C o en Fortran es la misma) retrasó la fase de desarrollo del
compilador final. De la misma manera la entrega de una versión conjunta del sistema (CellSs/SMPSs)
para C y Fortran implicó un nuevo retraso hasta tener completada una versión final.
Tarea
Definición de la sintaxis
Estudio del entorno
Diseño del compilador
Desarrollo del compilador
Pruebas y ejemplos
Documentación
Total
Planificación inicial
40 horas
80 horas
80 horas
240 horas
80 horas
80 horas
600 horas
Planificación final
40 horas
110 horas
90 horas
280 horas
100 horas
80 horas
700 horas
Cuadro 9.1: Comparativa entre la planificación inicial y la real
La diferencia entre las horas de trabajo planificadas y las horas que finalmente se han dedicado al
proyecto es considerable y no puede quedar sin justificación. La idea original era usar el compilador
mf95 como base del compilador de Fortran para CellSs/SMPSs. Esto implica que cuando se planificó no se tuvo en cuenta el coste de estudiar y adaptar el compilador mcxx, que era de C/C++. Esto
primero ha repercutido en el estudio del entrono y las fases de diseño y, sobre todo, desarrollo.
Con todo, el compilador ya ha sido publicado y se están empezando a desarrollar aplicaciones en
Fortran para CellSs/SMPSs. Ası́ y todo con algunas ampliaciones se podrı́a mejorar el funcionamiento
de las aplicaciones. En la última sección de este capı́tulo se ven estas ampliaciones.
9.3. VALORACIÓN ECONÓMICA
103
9.3. Valoración económica
En esta sección se hace una estimación del coste que implicarı́a llevar a cabo este proyecto en
condiciones normales. Esto es, con un equipo de desarrollo dedicado y necesitando adquirir o comprar
como servicios los recursos que se han usado para desarrollar el compilador.
Con todo, el coste de desarrollar el compilador suma 22900e. De estos, el 93 % corresponde a
los recursos humanos (tiempo dedicado) teniendo en cuenta el sueldo medio actual de analistas y
programadores (tabla 9.2). El resto corresponde a los recursos usados en el desarrollo del proyecto:
hardware y software. En este caso se valoran los recursos reales que se han usado, se resumen en las
tablas 9.3 y 9.4 hay que tener en cuenta que los recursos de supercomputación son gratuitos ya que
los cede el centro (BSC-CNS) para propósitos de investigación.
Perfil
Analista
Programador
Sueldo
42e/h
26e/h
Dedicación (horas)
200 horas
500 horas
Total
8400e
13000e
Cuadro 9.2: Recursos humanos
Recurso
Ordenador sobremesa oficina
Material Oficina
Total
PrecioSueldo
1200e
300e
1500e
Cuadro 9.3: Recursos materiales
Recurso
Cell/BE QS20 blade
MareNostrum nodes
Altix in BSC machine
Horas de CPU
30 horas
10 horas
20 horas
Cuadro 9.4: Recursos de supercomputación
En cuanto al software utilizado sólo se ha usado una aplicación de pago (paraver) cuya licencia
comercial sale sobre los 1000e. Todo lo demás corresponde a software libre o gratuito y no se ha
contratado ningún mantenimiento externo.
9.4. Expectativas de futuro
En este apartado se tratan las posibles y probables ampliaciones al software desarrollado en el proyecto. Como se ha comentado ya una de ellas es dar soporte a las extensiones más usadas de los compiladores tradicionales para que los usuarios puedan compilar sus aplicaciones para CellSs/SMPSs sin
tener que renunciar a algunas funciones u optimizaciones que les permiten los compiladores.
CAPÍTULO 9. CONCLUSIONES
104
Otra ampliación, en el caso de SMPSs, es una mejora en el modelo de programación que ya se
está desarrollando en la versión de C/C++. La idea consiste en permitir pasar secciones de arrays a
las tareas aunque éstas no sean contiguas en memoria. Esto permitirı́a una programación más natural
y mejorar el funcionamiento de algunas aplicaciones.
También hay que estudiar a fondo el rendimineto de las aplicaciones escritas en Fortran para
CellSs/SMPSs y ver si es posible algún cambio para mejorar la interacción con la biblioteca (probablemente implicarı́a un cambio de filosofı́a o el desarrollo de una versión de la biblioteca optimizada
para las propiedades de Fortran).
Hay que destacar un aspecto importante del compilador desarrollado en este proyecto y es el hecho de que se trata de una herramienta muy versátil. De la misma manera que el mcxx, el mf95ss,
aplicación desarrollada e integrada en el compilador final, se caracteriza por estar dotado de un sistema de bibliotecas dinámicas e interfaces sencillas que permite crear compiladores source-to-source
personalizados. Ası́, ya no solo será relativamente sencillo añadir posibles ampliaciones en los modelos de programación tratados en el proyecto, sinó que puede ser usado potencialmente para escribir
compiladores que generen aplicaciones para otros modelos de programación.
9.5. Valoración personal
En cuanto al aspecto personal, en el proyecto he aprendido a aplicar los conceptos aprendidos en
la carrera de una forma natural. Después de acabar las asignaturas de la carrera la sensación es de
haber estudiado muchas cosas que ya se te han olvidado. Sin embargo, cuando empiezas a trabajar en
algo descubres que entiendes por qué estás haciendo las cosas y esto es algo que no hubieras sabido
hacer sin haber pasado por la FIB.
En cuanto al trabajo en equipo, ha sido una buena experiencia el hecho de poder hacer el proyecto
de final de carrera en un entorno de trabajo puesto que, además de aprender a coordinarte con otras
personas, ayuda a aprender a explicarse cuando uno tiene un problema y a abrir la mente a soluciones
que no te habı́as planteado para problemas concretos.
Capı́tulo
10
Agradecimientos
En primer lugar he de agradecer a Rosa Ma Badia la confianza depositada en mı́ y, sobre todo
la paciencia que ha tendido conmigo durante estos meses. De la misma manera le agradezco a Jesús
Labarta su apoyo y paciencia.
También me gustarı́a agradece el compañerismo de Josep Ma Pérez, Pieter Bellens, Isaac Jurado
y Josep R. Herrero por su disposición a ayudar en todo momento. Además, he de estar agradecido a
todos los compañeros del despacho y del BSC y que me han ayudado en tantas cosas. Especialmente
he de mencionar a Roger Ferrer, que ha estado siempre dispuesto a responder todas mis dudas sobre
el compilador de C.
Gracias a mis padres y hermanos por aguantarme siempre, por adueñarme de vuestros portátiles y
por animarme en todo momento.
Gracias especiales a Berta por su paciencia, ya sabes que la ley de Hofstadter [5] siempre se
cumple (“Siempre tomará más tiempo del previsto, aunque se tenga en cuenta la ley de Hoftadter”).
Gracias a todos mis compañeros y amigos de la FIB por estar ahı́ siempre para apoyarme.
105
106
CAPÍTULO 10. AGRADECIMIENTOS
Bibliografı́a
[1] Openmp Api User’s Guide. iUniverse.com, Incorporated, 2004.
[2] Jeanne C. Adams, Brian T. Smith, Jeanne T. Martin, Walter S. Brainerd, and Jerrold L. Wagener.
FORTRAN 95 Handbook. MIT Press, Cambridge, MA, USA, 1997.
[3] Krste Asanovic, Ras Bodik, Bryan Christopher Catanzaro, Joseph James Gebis, Parry Husbands,
Kurt Keutzer, David A. Patterson, William Lester Plishker, John Shalf, Samuel Webb Williams,
and Katherine A. Yelick. The landscape of parallel computing research: A view from berkeley.
Technical Report UCB/EECS-2006-183, EECS Department, University of California, Berkeley,
Dec 2006.
[4] P. Bellens, J.M. Perez, R.M. Badia, and J. Labarta. Cellss: a programming model for the cell be
architecture. Supercomputing, 2006. SC ’06. Proceedings of the ACM/IEEE SC 2006 Conference,
pages 5–5, Nov. 2006.
[5] Douglas Hofstadter. Gödel, Escher, Bach: an Eternal Golden Braid. Basic Books, 1979.
[6] Jesus Labarta Josep M. Perez, Rosa M. Badia. A flexible and portable programming model for
smp and multi-cores. Technical report, BSC-UPC, Jun. 2007.
[7] A. McDonald, B.D. Carlstrom, J. Chung, C.C. Minh, H. Chafi, C. Kozyrakis, and K. Olukotun.
Transactional memory: The hardware-software interface. Micro, IEEE, 27(1):67–76, Jan.-Feb.
2007.
[8] Michael Metcalf. Fortran 90 Tutorial. 1995.
[9] Rob Schreiber. Manycores in the future. Technical report, hp labs, 2007.
107
108
BIBLIOGRAFÍA
Glosario
array Referente a las variables de un programa que tienen más de un elemento dispuestos de manera
contigua en memoria. 12
escalar Variable que es un sólo elemento de su tipo. Array con cero dimensiones. 37
GRID Red compuesta por ordenadores convencionales que permite explotar el paralelismo de aplicaciones mediante su división en trabajos. 10
HPC High Performance Computing. Computación de alto rendimiento. 3
intrinsic functions Funciones pre-definidas en Fortran. 13
mainframe Grandes computadoras utilizadas clásicamente para cálculos con grandes cantidades de
datos en bancos grandes proveedores de servicios o en investigación. 3
manycore Se usa esta palabra para designar chips que integran muchos nucleos. 7
multicore Chip con varios núcleos o procesadores integrados. 7
PPE PowerPc Element. Procesador principal del Cell/BE. 25
runtime Comportamiento de un programa durante su ejecución. 23
SIMD Simple Instruction Multiple Data. Técnica para explotar paralelismo a nivel de instrucción en
que los procesadores ejecutan instrucciones sobre tipos de datos múltiples (vector). 25
sincronización En el contexto de multiprocesos es el mecanismo que utilizan los diferentes procesos de un sistema para poder compartir datos y esperarse unos a otros sin que se produzcan
conflictos. 32
109
Glosario
110
SPE Synergistic Processing Element. Unidad SIMD del Cell/BE. 25
thread En procesadores de tiempo compartido se pueden ejecutar a la vez diferentes partes o subprogramas de una aplicación. 9
token Unidad básica usada por los compiladores para analizar la estructura sintáctica del código. Se
podrı́a entender como una “palabra” en el lenguaje de los compilaodres. 41
Anexos: Manuales de CellSs y SMPSs
111
Cell Superscalar (CellSs) User’s Manual
Version 2.0
Barcelona Supercomputing Center
May 2008
i
Cell Superscalar User’s Manual
Contents
1
Introduction
1
2
Installation
2.1 Compilation requirements
2.2 Compilation . . . . . . . .
2.3 Runtime requirements . . .
2.4 User environment . . . . .
.
.
.
.
1
1
2
3
3
.
.
.
.
.
.
.
.
.
.
.
4
4
4
4
6
6
7
8
8
9
10
11
4
Compiling
4.1 Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
13
14
5
Setting the environment and executing
5.1 Setting the number of SPUs and executing . . . . . . . . . . . . . . . . . . . . . . .
15
15
6
Programming examples
6.1 Matrix mutlitply . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
16
7
CellSs internals
18
8
Advanced features
8.1 Using paraver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Configuration file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
20
22
9
CellSs SPU memory functionality
9.1 Dynamic memory allocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.2 DMA accesses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.3 Strided Memory Access . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
23
23
24
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Programming with CellSs
3.1 C Programming . . . . . . . . . . .
3.1.1 Task selection . . . . . . . .
3.1.2 Specifying a task . . . . . .
3.1.3 Scheduling a task . . . . . .
3.1.4 Waiting on data . . . . . . .
3.1.5 Mixed SPU and PPU code .
3.2 Fortran Programming . . . . . . . .
3.2.1 Task selection . . . . . . . .
3.2.2 Specifying a task . . . . . .
3.2.3 Waiting on data . . . . . . .
3.2.4 Fortran compiler restrictions
References
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
ii
Barcelona Supercomputing Center
List of Figures
1
2
3
CellSs runtime behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
One dimensional memory access pattern. . . . . . . . . . . . . . . . . . . . . . . .
Two dimensional memory access pattern. . . . . . . . . . . . . . . . . . . . . . . .
19
25
25
Cell Superscalar User’s Manual
1
1
Introduction
The Cell Broadband Engine (Cell/BE) is an heterogeneous multi-core architecture with nine cores.
The first generation of the Cell BE includes a 64-bit multi-threaded PowerPC processor element (PPE)
and eight synergistic processor elements (SPEs), connected by an internal high bandwidth Element
Interconnect Bus (EIB). The PPE has two levels of on-chip cache and also supports IBMs VMX to
accelerate multimedia applications by using VMX SIMD units.
This document is the user manual of the Cell Superscalar (CellSs) framework, which is based on a
source-to-source compiler and a runtime library. The programming model allows programmers to
write sequential applications and the framework is able to exploit the existing concurrency and to use
the different components of the Cell/BE (PPE and SPEs) by means of an automatic parallelization at
execution time. The requirements we place on the programmer are that the application is composed
of coarse grain functions (for example, by applying blocking) and that these functions do not have
collateral effects (only local variables and parameters are accessed). These functions are identified
by annotations (somehow similar to the OpenMP ones), and the runtime will try to parallelize the
execution of the annotated functions (also called tasks).
The source-to-source compiler separates the annotated functions from the main code and the library
provides a manager program to be run in the SPEs that is able to call the annotated code. However,
an annotation before a function does not indicate that this is a parallel region (as it does in OpenMP).
To be able to exploit the parallelism, the CellSs runtime builds a data dependency graph where each
node represents an instance of an annotated function and edges between nodes denote data dependencies. From this graph, the runtime is able to schedule for execution independent nodes to different
SPEs at the same time. All data transfers required for the computations in the SPEs are automatically performed by the runtime. Techniques imported from the computer architecture area like the
data dependency analysis, data renaming and data locality exploitation are applied to increase the
performance of the application.
While OpenMP explicitly specifies what is parallel and what is not, with CellSs what is specified
are functions whose invocations could be run in parallel, depending on the data dependencies. The
runtime will find the data dependencies and will determine, based on them, which functions can be run
in parallel with others and which not. Therefore, CellSs provides programmers with a more flexible
programming model with an adaptive parallelism level depending on the application input data and
the number of available cores.
2
Installation
Cell Superscalar is distributed in source code form and must be compiled and installed before using it. The runtime library source code is distributed under the LGPL license and the rest of the
code is distributed under the GPL license. It can be downloaded from the CellSs web page at
http://www.bsc.es/cellsuperscalar .
2.1
Compilation requirements
The CellSs compilation process requires the following system components:
2
Barcelona Supercomputing Center
• CBE SDK 3.0
• IBM XL Fortran multicore acceleration for Linux on System p. V11.11
• GNU make
Additionally, if you change the source code you may require:
• automake
• autoconf ≥ 2.60
• libtool
• rofi-bison2
• GNU flex
2.2
Compilation
To compile and install CellSs please follow the following steps:
1. Decompress the source tarball.
tar -xvzf CellSS-2.0.tar.gz
2. Enter into the source directory.
cd CellSS-2.0
3. If necessary, check that you have set the PATH and LD LIBRARY PATH environment variables
to point to the CBE SDK installation.
4. Run the configure script, specifying the installation directory as the prefix argument. The configure script also accepts the following optional parameters:
• --with-cellsdk=prefix Specifies the CBE SDK installation path.
More information can be obtained by running ./configure --help.
./configure --prefix=/opt/CellSS
There are also some environment variables that affect the configuration behaviour.
• PPU C compiler may be specified with the PPUCC variable.
• PPU C compiler flags may be given with the PPUCFLAGS variable.
• PPU C++ compiler may be specified with the PPUCXX variable.
• PPU C++ compiler flags may be given with the PPUCXXFLAGS variable.
1
2
See http://www.alphaworks.ibm.com/tech/cellfortran
Available at http://www.bsc.es/plantillaH.php?cat id=351
Cell Superscalar User’s Manual
3
• PPU Fortran compiler may be specified with the PPUFC variable.
• PPU Fortran compiler flags may be given with PPUFCFLAGS.
• SPU C compiler may be specified with the SPUCC variable.
• SPU C compiler flags may be given with the SPUCFLAGS variable.
• SPU C++ compiler may be specified with the SPUCXX variable.
• SPU C++ compiler flags may be given with the SPUCXXFLAGS variable.
• SPU Fortran compiler may be specified with the SPUFC variable.
• SPU Fortran compiler flags may be given with SPUFCFLAGS.
5. Run make.
make
6. Run make install.
make install
2.3
Runtime requirements
The CellSs runtime requires the following system components:
• CBE SDK 3.0
2.4
User environment
If the CBE SDK resides on a non standard directory, then the user must set the LD LIBRARY PATH
and PATH accordingly.
If CellSs has not been installed into a system directory, then the user must set the following environment variables:
1. The PATH environment variable must contain the bin subdirectory of the installation.
export PATH=$PATH:/opt/CellSS/bin
2. The LD LIBRARY PATH environment variable must contain the lib subdirectory from the installation.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/CellSS/lib
4
3
Barcelona Supercomputing Center
Programming with CellSs
CellSs applications are based on the parallelization at task level of sequential applications. The tasks
(functions or subroutines) selected by the programmer will be executed in the SPE processors. Furthermore, the runtime detects when tasks are data independent between them and is able to schedule
the simultaneous execution of several of them on different SPEs. Since, the SPE cannot access the
main memory, the data required for the computation in the SPE is transferred by DMA. All the above
mentioned actions (data dependency analysis, scheduling and data transfer) are performed transparently to the programmer. However, to take benefit of this automation, the computations to be executed
in the Cell BE should be of certain granularity (about 50µs). A limitation on the tasks is that they can
only access their parameters and local variables. In case global variables are accessed the compilation
will fail.
3.1
C Programming
The C version of CellSs borrows the syntax from OpenMP in the way that the code is annotated using
special preprocessor directives. Therefore, the same general syntax rules apply; that is, directives are
one line long but they can span into multiple lines by escaping the line ending.
3.1.1
Task selection
In CellSs, it is a responsibility of the application programmer to select tasks of certain granularity.
For example, blocking is a technique that can be applied to increase the granularity of the tasks in
applications that operate on matrices. Below there is a sample code for a block matrix multiplication:
void block_addmultiply (double C[BS][BS], double A[BS][BS],
double B[BS][BS])
{
int i, j, k;
for (i=0; i < BS; i++)
for (j=0; j < BS; j++)
for (k=0; k < BS; k++)
C[i][j] += A[i][k] * B[k][j];
}
3.1.2
Specifying a task
A task is conceived in the form of a procedure, i.e. a function without return value. Then, a procedure
is “converted” into a CellSs task by providing a simple annotation before its declaration or definition:
#pragma css task [input(<input parameters>)]optional \
[inout(<inout parameters>)]optional \
Cell Superscalar User’s Manual
5
[output(<output parameters>)]optional \
[highpriority]optional
<function declaration or definition>
Where each clause serves the following purposes:
• input clause lists parameters whose input value will be read.
• inout clause lists parameters that will be read and written by the task.
• output clause lists parameters that will be written to.
• highpriority clause specifies that the task will be scheduled for execution earlier than tasks
without this clause.
The parameters listed in the input, inout and output clauses are separated by commas. Only the
parameter name and dimension(s) need to be specified, not the type. Although the dimensions must
be omitted if present in the parameter declaration.
Examples
In this example, the “factorial” task has a single input parameter “n” and a single output parameter
“result”.
#pragma css task input(n) output(result)
void factorial (unsigned int n, unsigned int *result)
{
*result = 1;
for (; n > 1; n--)
*result = *result * n;
}
The next example, has two input vectors “left”, of size “leftSize”, and “right”, of size “rightSize”; and
a single output “result” of size “leftSize+rightSize”.
#pragma css task input(leftSize, rightSize) \
input(left[leftSize], right[rightSize]) \
output(result[leftSize+rightSize])
void merge (float *left, unsigned int leftSize,
float *right, unsigned int rightSize, float *result)
{
...
}
6
Barcelona Supercomputing Center
The next example shows another feature. In this case, with the keyword highpriority the user
is giving hints to the scheduler: the jacobi tasks will be, when data dependencies allow it, executed
before the ones that are not marked as high-priority.
#pragma css task input(lefthalo[32], tophalo[32], righthalo[32], \
bottomhalo[32]) inout(A[32][32]) highpriority
void jacobi (float *lefthalo, float *tophalo,
float *righthalo, float *bottomhalo, float *A)
{
...
}
3.1.3
Scheduling a task
Once all the tasks have been specified, the next step is to use them. The way to do it is as simple as
it gets: just call the annotated function normally. However, there still exists a small requirement, in
order to have the tasks scheduled by the CellSs runtime, the annotated functions must be invoked in a
block surrounded by these two directives:
#pragma css start
#pragma css finish
These two directives can only be used once in a program, i.e. it is not possible to annotate a start
directive after3 a finish directive has been annotated. They are also mandatory and must enclose
all annotated function invocations.
Beware of the fact that the compiler will not detect these issues; the runtime will complain in some
cases but may also incur into unexpected behaviour. Section 3.1.4 provides some simple examples.
3.1.4
Waiting on data
When code outside the tasks needs to handle data manipulated also by code inside the tasks, the
automatic dependency tracking performed by the runtime is not enough to ensure correct read and
write order. To solve this, CellSs offers some synchronization directives.
As in OpenMP, there is a barrier directive:
#pragma css barrier
This forces the main thread to wait for the completion of all generated tasks so far. However, this kind
of synchronization is too coarse grained and in many cases can be counter productive. To achieve a
finer grained control over data readiness, the wait on directive is also available:
3
After in the code execution path.
Cell Superscalar User’s Manual
7
#pragma css wait on(<list of variables>)
In this case, the main thread waits (or starts running tasks) until all the values of the listed variables
have been committed. Like in other clauses4 , multiple variable names are separated by commas.
The data unit to be waited on should be consistent with the data unit of the task. For example, if the
task is operating on the full range of an array, we cannot wait on a single element arr[i] but on its
base address arr.
Examples
The next example shows how a wait on directive can be used:
#pragma css task inout(data[size]) input(size)
void bubblesort (float *data, unsigned int size)
{
...
}
void main ()
{
...
#pragma css start
bubblesort(data, size);
#pragma css wait on(data)
for (unsigned int i = 0; i < size; i++)
printf("%f ", data[i]);
#pragma css finish
}
In this particular case, a barrier could have served for the same purpose since there is just one output
variable.
3.1.5
Mixed SPU and PPU code
The CellSs programming model allows by design to mix SPU and PPU code in the same source file.
Tasks are always compiled for the SPU architecture. Functions reachable by tasks are also on the SPU
side. All other functions are on the PPU side by default, including those reachable by them. These
heuristics can lead to some functions appearing on both sides.
4
Clauses that belong to the task directive.
8
Barcelona Supercomputing Center
However, the architecture selection heuristics are only applied individually to each source file. This
could lead to some functions being compiled for the wrong architecture and producing linking errors.
For instance, a function in one source file could be called by a task in another source file. Unless that
function is also called by a task from the same source file, it will not be compiled for the SPU side.
To solve this problem, the architecture selection heuristics can be skipped by specifying explicitly the
target architecture. Function definitions and declarations can have their target architecture(s) specified
by using the following construct:
#pragma css target [spu]optional [ppu]optional
<function declaration or definition>
Functions can have either target or both simultaneously.
3.2
Fortran Programming
As in C, the Fortran version of CellSs also is based on the syntax of OpenMP for Fortran-95. This
version of CellSs only supports free form code and needs some Fortran-95 standard features.
3.2.1
Task selection
In CellSs it is responsibility of the application programmer to select tasks of a certain granularity. For
example, blocking is a technique that can be applied to increase such granularity in applications that
operate on matrices. Below there is a sample code for a block matrix multiplication:
subroutine block_addmultiply(C, A, B, BS)
implicit none
integer, intent(in) :: BS
real, intent(in) :: A(BS,BS), B(BS,BS)
real, intent(inout) :: C(BS,BS)
integer :: i, j, k
do i=1, BS
do j=1, BS
do k=1, BS
C(i,j) = C(i,j) + A(i,k)*B(k,j)
enddo
enddo
enddo
end subroutine
Cell Superscalar User’s Manual
3.2.2
9
Specifying a task
A task is conceived in the form of a subroutine. The main difference with C CellSs annotations it that
in Fortran, the language provides the means to specify the direction of the arguments in a procedure.
Moreover, while arrays in C can be passed as pointers, Fortran does not encourage that practice. In
this sense, annotations in Fortran are simpler than in C. The annotations have the form of a Fortran-95
comment followed by a “$” and the framework sentinel keyword (CSS in this case). This is very
similar to the syntax OpenMP uses in Fortran-95.
In Fortran, each subprogram calling tasks must know the interface for those tasks. For this purpose,
the programmer must specify the task interface in the caller subprograms and also write some CellSs
annotations to let the compiler know that there is a task. The following requirements must be satisfied
by all Fotran tasks in CellSs:
• The task interface must specify the parameter directions of all parameters. That is, by using
INTENT (<direction>); where <direction> is one of: IN, INOUT or OUT.
• Provide an explicit shape for all array parameters in the task (caller subprogram).
• Provide a !$CSS TASK annotation for the caller subprogram with the task interface.
• Provide a !$CSS TASK annotation for the task subroutine.
The following example shows how a subprogram calling a CellSs task looks in Fortran. Note that it
is not necessary to specify the parameter directions in the task subroutine, they are only necessary in
the interface.
subroutine example()
...
interface
!$CSS TASK
subroutine block_add_multiply(C, A, B, BS)
! This is the task interface. Specifies the size and
! direction of the parameters.
implicit none
integer, intent(in) :: BS
real, intent(in) :: A(BS,BS), B(BS,BS)
real, intent(inout) :: C(BS,BS)
end subroutine
end interface
...
!$CSS START
...
call block_add_multiply(C, A, B, BLOCK_SIZE)
...
!$CSS FINISH
...
10
Barcelona Supercomputing Center
end subroutine
!$CSS TASK
subroutine block_add_multiply(C, A, B, BS)
! Here goes the body of the task (the block multiply_add
! in this case)
...
end subroutine
It is also necessary, as the example shows, to call the tasks between START and FINISH annotation
directives. These are executable statements that must be after the declarations, in the executable part
of the subprogram. START and FINISH statements must only be executed once in the application.
Example
The following example shows part of a CellSs application using another feature. The HIGHPRIORITY
clause is used to indicate that one task is high priority and must be executed before non-high priority
tasks as soon as its data dependencies allow.
interface
!$CSS TASK HIGHPRIORITY
subroutine jacobi(lefthalo, tophalo, righthalo, bottomhalo, A)
real, intent(in), dimension(32) :: lefthalo, tophalo, &
righthalo, bottomhalo
real, intent(inout) :: A(32,32)
end subroutine
end interface
3.2.3
Waiting on data
CellSs provides two different ways of waiting on data. These features are useful when the user wants
to read the results of some tasks in the main thread. By default tasks will write their results back to
main memory after its execution. Nevertheless, the main thread does not know when these results are
ready. With BARRIER and WAIT ON, the main program stops its execution until the data is available.
BARRIER is the most conservative option. When the main thread reaches a barrier, waits until all
tasks have finished and have written their results back to main memory. The syntax is simple:
...
do i=1, N
call task_a(C(i),B)
enddo
...
!$CSS BARRIER
Cell Superscalar User’s Manual
11
print *, C(1)
...
The other way is to specify exactly which variables we want the program to wait to be available before
the execution goes on.
!$CSS WAIT ON(<list of variables>)
Where the list of variables is a comma separated list of variable names whose values must be correct
before continuing the execution.
Example
!$CSS TASK
subroutine bubblesort (data, size)
integer, intent(in) :: size
real, intent(inout) :: data(size)
...
end subroutine
program main
...
interface
!$CSS TASK
subroutine bubblesort (data, size)
integer, intent(in) :: size
real, intent(inout) :: data(size)
end subroutine
end interface
...
call bubblesort(data, size);
!$CSS WAIT ON(data)
do i=1, size
print *, data(i)
enddo
end
3.2.4
Fortran compiler restrictions
This is the first release of CellSs with a Fortran compiler and it has some limitations. Some will disappear in the future. They consist of compiler specific and non-standard features. Also deprecated forms
in the Fortran-95 standard are not supported and are not planned to be included in future releases.
12
Barcelona Supercomputing Center
• Case sensitiveness: The CellSs Fortran compiler is case insensitive. However, task names must
be written in lowercase.
• It is not allowed to mix generic interfaces with tasks.
• Internal subprograms cannot be tasks.
• Use of modules within tasks has not been tested in this version.
• Optional and named parameters are not allowed in tasks.
• Some non-standard common extensions like Value parameter passing are not supported or have
not been tested yet. In further releases of CellSs we expect to support a subset of the most
common extensions.
• Only explicit shape arrays and scalars are supported as task parameters.
• The MULTOP parameter is not supported.
• Tasks cannot have an ENTRY statement.
• Array subscripts cannot be used as task parameters.
• PARAMETER arrays cannot be used as task parameters.
4
Compiling
The CellSs compiler infastructure is composed of a C99 source-to-source compiler, a Fortran-95
source-to-source compiler and a common driver.
The driver is called cellss-cc and depending on each source filename suffix invokes transparently
the C compiler or the Fortran-95 compiler. C files must have the “.c” suffix. Fortran files can have
either the “.f”, “.for”, “.f77”, “.f90”, or “.f95” suffix.
The cellss-cc driver behaves similarly to a native compiler. It can compile individual files one at
a time, several ones, link several objects into an executable or perform all operations in a single step.
The compilation process consists in processing the CellSs pragmas, transforming the code according
to those, compiling both for the PPU and SPU with the corresponding compilers (ppu-c99 and
spu-c99 for C programs and, in the case of Fortran programs, with ppuxlf95 and spuxlf), and
packing both objects and additional information required for linking into a single object..
The linking process consists in unpacking the object files, generating additional code required for the
SPU part, compiling it, linking all SPU objects together, embedding the SPU executable into a PPU
object (ppu32-embedspu), generating additional code required for the PPU part , compiling it, and
finally linking all PPU objects together with the CellSs runtime to generate the final executable.
13
Cell Superscalar User’s Manual
4.1
Usage
The cellss-cc compiler has been designed to mimic the options and behaviour of common C compilers. However, it uses two other compilers internally that may require different sets of compilation
options.
To cope with this distinction, there are general options and target specific options. While the general
options are applied to PPU code and SPU code, the target specific options allow to specify options to
pass to the PPU compiler and the SPU compiler independently.
The list of supported options is the following:
> cellss-cc -help
Usage: cellss-cc <options and sources>
Options:
-D<macro>
-D<macro>=<value>
-g
-h|--help
-I<directory>
-k|--keep
-l<library>
-L<directory>
-O<level>
-o <filename>
-c
-t|--tracing
-v|--verbose
Defines ’macro’ with value ’1’ in the
preprocessor.
Defines ’macro’ with value ’value’ in the
preprocessor.
Enables debugging.
Shows usage help.
Adds ’directory’ the list of preprocessor
search paths.
Keeps intermediate source and object files.
Links with the specified library.
Adds ’directory’ the list of library search
paths.
Enables optimization level ’level’.
Sets the name of the output file.
Specifies that the code must only be compiled
(and not linked).
Enables run time tracing.
Enables verbose operation.
PPU specific options:
-WPPUp,<options>
Passes the comma separated
the PPU C preprocessor.
-WPPUc,<options>
Passes the comma separated
the PPU C compiler.
-WPPUf,<options>
Passes the comma separated
the PPU Fortran compiler.
-WPPUl,<options>
Passes the comma separated
the PPU linker.
SPU specific options:
list of options to
list of options to
list of options to
list of options to
14
Barcelona Supercomputing Center
-WSPUp,<options>
-WSPUc,<options>
-WSPUf,<options>
-WSPUl,<options>
4.2
Passes the comma separated
the SPU C preprocessor.
Passes the comma separated
the SPU C compiler.
Passes the comma separated
the SPU Fortran compiler.
Passes the comma separated
the SPU linker.
list of options to
list of options to
list of options to
list of options to
Examples
Contrary to previous versions of the compiler, now it is possible to generate binaries from multiple
source files like any regular C or Fortran95 compiler. Therefore, it is possible to compile multiple
source files directly into a single binary:
> cellss-cc -O3 *.c -o my_binary
Although handy, you may also use the traditional compilation methodology:
>
>
>
>
cellss-cc
cellss-cc
cellss-cc
cellss-cc
-O3
-O3
-O3
-O3
-c code1.c
-c code2.c
-c code3.f90
code1.o code2.o code3.o -o my_binary
This capability allows to easily adapting makefiles by just changing the C compiler, the Fortran compiler and the linker to point to cellss-cc. For instance:
CC = cellss-cc
LD = cellss-cc
CFLAGS = -O2 -g
SOURCES = code1.c code2.c code3.c
BINARY = my_binary
$(BINARY): $(SOURCES)
Combining the -c and -o options makes possible to generate objects with arbitrary filenames. However, changing the suffix to other than .o is not recommended since, in some cases, the compiler
driver relies on them to work properly.
As already mentioned, the same binary serves as a Fortran95 compiler:
> cellss-cc -O3 matmul.f90 -o matmul
Cell Superscalar User’s Manual
15
If there are no compilation errors, the executable file “matmul” (optimized) is created and can be
called from the command line (“> ./matmul ...”).
In some cases, it is desirable to use specific optimization options not included in the -O, -O1, -O2,
or -O3 set. This is possible by using the -WSPUc and/or -WPPUc flags (depending on the kind of
target that requires the optimization):
> cellss-cc -O2 -WSPUc,-funroll-loops,-ftree-vectorize \
-WSPUc,-ftree-vectorizer-verbose=3 matmul.c -o matmul
In the previous example, the native options are passed directly to the native compiler (for example
spu-c99), to perform automatic vectorization of the code to be run in the SPUs.
Option -k, or --keep, will not delete the intermediate files (files generated by the preprocessor,
object files, ...).
> cellss-cc -k cholesky.c -o cholesky
Finally, option -t enables executable instrumentation to generate a runtime trace to be analyzed later
with the appropriate tool:
> cellss-cc -O2 -t matmul.c -o matmul
When executing “matmul”, a trace file of the execution of the application will be generated. See
section 8.1 for further information on trace analysis.
5
Setting the environment and executing
Depending on the path chosen for installation (see section 2), the LD LIBRARY PATH environment
variable may need to be set appropriately or the application will not be able to run.
If CellSs was configured with --prefix=/foo/bar/CellSS, then LD LIBRARY PATH should
contain the path /foo/bar/CellSS/lib. If the framework is installed in a system location such as /usr,
setting the loader path is not necessary.
5.1
Setting the number of SPUs and executing
Before executing a CellSs application, the number of SPU processors to be used in the execution have
to be defined. The default value is 8, but it can be set to a different number with the CSS NUM SPUS
environment variable, for example:
> export CSS_NUM_SPUS=6
16
Barcelona Supercomputing Center
CellSs applications are started from the command line in the same way as any other application. For
example, for the compilation examples of section 4.2, the applications can be started as follow:
> ./matmul <pars>
> ./cholesky <pars>
6
Programming examples
This section presents a programming example for the block matrix multiplication. The code is not
complete, but you can find the complete and working code under share/docs/cellss/examples/ in the
installation directory. More examples are also provided in this directory.
6.1
Matrix mutlitply
This example presents a CellSs code for a block matrix multiply. The block contains BS × BS floats.
#pragma css task input(A, B) inout(C)
static void block_addmultiply (float C[BS][BS], float A[BS][BS],
float B[BS][BS])
{
int i, j, k;
for (i = 0; i < BS;
for (j = 0; j <
for (k = 0;
C[i][j]
i++)
BS; j++)
k < BS; k++)
+= A[i][k] * B[k][j];
}
int main(int argc, char **argv)
{
int i, j, k;
initialize(argc, argv, A, B, C);
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
for (k = 0; k < N; k++)
block_addmultiply(C[i][j], A[i][k], B[k][j]);
...
}
The main code will run in the Cell PPE while the block_addmultiply calls will be executed in
the SPE processors. It is important to note that the sequential code (including the annotations) can be
compiled and run in a sequential processor. This is very useful for debugging the algorithms.
Cell Superscalar User’s Manual
17
However, the code is not vectorized, and if a compiler that does not vectorize the code is used, it is not
going to be very efficient. The programmer can pass to the corresponding compiler the compilation
flags that automatically vectorize the SPU (see section 4.2). Another option will be to manually
provide a vectorized code as the one that follows:
#define BS 64
#define BSIZE_V BS/4
#pragma css task input(A, B) inout(C)
void block_addmultiply (float C[BS][BS], float A[BS][BS],
float B[BS][BS])
{
vector float *Bv = (vector float*) B;
vector float *Cv = (vector float*) C;
vector float elem;
int i, j, k;
for (i = 0; i < BS; i++)
for (j = 0; j < BS; j++) {
elem = spu_splats (A[i * BS + j]);
for (k = 0; k < BSIZE_V; k++)
Cv[i*BSIZE_V+k] = spu_madd(elem, Bv[j * BSIZE_V + k],
Cv[i * BSIZE_V + k]);
}
}
This code can even be improved by unrolling the inner loop. Even more, the code can be improved if
the data is prefetched in advance, as the next version of the sample code does:
#define BS 64
#define BSIZE_V BS/4
#pragma css task input(A, B) inout(C)
void matmul (float A[BSIZE][BSIZE], float B[BSIZE][BSIZE],
float C[BSIZE][BSIZE])
{
vector float *Bv = (vector float *) B;
vector float *Cv = (vector float *) C;
vector float elem;
int i, j;
int i_size;
int j_size;
vector float tempB0, tempB1, tempB2, tempB3;
i_size = 0;
for (i = 0; i < BSIZE; i++) {
j_size = 0;
18
Barcelona Supercomputing Center
for (j = 0; j < BSIZE; j++) {
elem = spu_splats(A[i][j]);
tempB0 = Bv[j_size+0];
tempB1 = Bv[j_size+1];
tempB2 = Bv[j_size+2];
Cv[i_size+0] = spu_madd(elem, tempB0, Cv[i_size+0]);
tempB3 = Bv[j_size+3];
Cv[i_size+1] = spu_madd(elem, tempB1, Cv[i_size+1]);
tempB0 = Bv[j_size+4];
Cv[i_size+2] = spu_madd(elem, tempB2, Cv[i_size+2]);
tempB1 = Bv[j_size+5];
Cv[i_size+3] = spu_madd(elem, tempB3, Cv[i_size+3]);
tempB2 = Bv[j_size+6];
Cv[i_size+4] = spu_madd(elem, tempB0, Cv[i_size+4]);
tempB3 = Bv[j_size+7];
Cv[i_size+5] = spu_madd(elem, tempB1, Cv[i_size+5]);
tempB0 = Bv[j_size+8];
Cv[i_size+6] = spu_madd(elem, tempB2, Cv[i_size+6]);
tempB1 = Bv[j_size+9]
Cv[i_size+7] = spu_madd(elem, tempB3, Cv[i_size+7]);
tempB2 = Bv[j_size+10];
Cv[i_size+8] = spu_madd(elem, tempB0, Cv[i_size+8]);
tempB3 = Bv[j_size+11];
Cv[i_size+9] = spu_madd(elem, tempB1, Cv[i_size+9]);
tempB0 = Bv[j_size+12];
Cv[i_size+10] = spu_madd(elem, tempB2, Cv[i_size+10]);
tempB1 = Bv[j_size+13];
Cv[i_size+11] = spu_madd(elem, tempB3, Cv[i_size+11]);
tempB2 = Bv[j_size+14];
Cv[i_size+12] = spu_madd(elem, tempB0, Cv[i_size+12]);
tempB3 = Bv[j_size+15];
Cv[i_size+13] = spu_madd(elem, tempB1, Cv[i_size+13]);
Cv[i_size+14] = spu_madd(elem, tempB2, Cv[i_size+14]);
Cv[i_size+15] = spu_madd(elem, tempB3, Cv[i_size+15]);
j_size += BSIZE_V;
}
i_size += BSIZE_V;
}
}
7
CellSs internals
When compiling a CellSs application with cellss-cc, the resulting object files are linked with
the CellSs runtime library. Then, when the application is started, the CellSs runtime is automatically
invoked. The CellSs runtime is decoupled in two parts: one runs in the PPU and the other in each of
19
Cell Superscalar User’s Manual
PPU
Main thread
Helper thread
CellSs PPU lib
SPU0
Data dependence
Data renaming
Scheduling
User main
program
Work assignment
Renaming table
Task control
buffer
DMA in
Task execution
DMA out
Synchronization
SPU2
Original task
code
Synchronization
User
data
SPU1
CellSs SPU lib
Finalization signal
Tasks
...
Stage in/out data
Memory
Figure 1: CellSs runtime behavior
the SPUs. In the PPU, we will differentiate between the master thread and the helper thread.
The most important change in the original user code is that the CellSs compiler replaces calls to tasks
with calls to the css_addTask function. At runtime, these calls will be responsible for the intended
behavior of the application in the Cell BE processor. At each call to css_addTask, the master
thread will do the following actions:
• Add node that represents the called task in a task graph.
• Analyze data dependencies of the new task with other previously called tasks.
• Parameter renaming: similarly to register renaming, a technique from the superscalar processor
area, we do renaming of the output parameters. For every function call that has a parameter
that will be written, instead of writing to the original parameter location, a new memory location will be used, that is, a new instance of that parameter will be created and it will replace
the original one, becoming a renaming of the original parameter location. This allows to execute that function call independently from any previous function call that would write or read
that parameter. This technique allows to effectively remove some data dependencies by using
additional storage, and thus improving the chances to extract more parallelism.
The helper thread is the one that decides when a task should be executed and also monitors the execution of the tasks in the SPUs.
Given a task graph, the helper thread schedules tasks for execution in the SPUs. This scheduling
follows some guidelines:
• A task can be scheduled if its predecessor tasks in the graph have finished their execution.
20
Barcelona Supercomputing Center
• To reduce the overhead of the DMA, groups of tasks are submitted to the same SPU.
• Data locality is exploited by keeping task outputs in the SPU local memory and scheduling
tasks that reuse this data to the same SPU.
The helper thread synchronizes and communicates with the SPUs using a specific area of the PPU
main memory for each SPU. The helper thread indicates the length of the group of tasks to be executed
and information related to the input and output data of the tasks.
The SPUs execute a loop waiting for tasks to be executed. Whenever a group of tasks is submitted for
execution, the SPU starts the DMA of the input data, processes the tasks and writes back the results
to the PPU memory. The SPU synchronizes with the PPU to indicate end of the group of tasks using
a specific area of the PPU main memory.
8
8.1
Advanced features
Using paraver
To understand the behavior and performance of the applications, the user can generate Paraver [3]
tracefiles of their CellSs applications.
If the -t/-tracing flag is enabled at compilation time, the application will generate a Paraver
tracefile of the execution. The default name for the tracefile is gss-trace-id.prv. The name can be
changed by setting the environment variable CSS TRACE FILENAME. For example, if it is set as
follows:
> export CSS_TRACE_FILENAME=tracefile
After the execution, the files: tracefile-0001.row, tracefile-0001.prv and tracefile-0001.pcf are generated. All these files are required by the Paraver tool.
The traces generated by CellSs can be visualized and analyzed with Paraver. Paraver [3] is distributed
independently of CellSs.
Several configuration files to visualise and analyse CellSs tracefiles are provided in the CellSs distribution in the directory <install dir>/share/cellss/paraver cfgs/. The following table summarizes
what is shown by each configuration file.
Configuration file
2dh inbw.cfg
2dh inbytes.cfg
Feature shown
Histogram of the bandwidth achieved by individual DMA IN
transfers.
Zero on the left, 10GB/s on the right.
Darker colour means more times a transfer at such bandwidth
occurred.
Histogram of bytes read by the stage in DMA transfers.
21
Cell Superscalar User’s Manual
Configuration file
2dh outbw.cfg
2dh outbytes.cfg
3dh duration phase.cfg
3dh duration tasks.cfg
DMA bw.cfg
DMA bytes.cfg
execution phases.cfg
flushing.cfg
general.cfg
stage in out phase.cfg
task.cfg
task distance histogram.cfg
task number.cfg
Task profile.cfg
Feature shown
Histogram of the bandwidth achieved by individual DMA
OUT transfers.
Zero on the left, 10GB/s on the right.
Darker colour means more times a transfer at such bandwidth
occurred.
Histogram of bytes writen by the stage out DMA transfers.
Histogram of duration for each of the runtime phases.
Histogram of duration of SPU tasks. One plane per task
(Fixed Value Selector).
Left column: 0 microseconds. Right column: 300 us.
Darker colour means higher number of instances of that duration.
DMA (in + out) bandwidth per SPU.
Bytes being DMAed (in + out) by each SPU.
Profile of percentage of time spent by each thread (main,
helper and SPUs) at each of the major phases in the runt time
library (i.e. generating tasks, scheduling, DMA, task execution, . . . ).
Intervals (dark blue) where each SPU is flushing its local
trace buffer to main memory.
For the main and helper threads the flushing is actually to
disk. Overhead in this case is thus significant as this stalls
the respective engine (task generation or submission).
Mix of timelines.
Identification of DMA in (grey) and out phases (green).
Outlined function being executed by each SPU.
Histogram of task distance between dependent tasks.
Number (in order of task generation) of task being executed
by each SPU.
Ligth green for the initial tasks in program order, blue for the
last tasks in program order.
Intermixed green an blue indicate out of order execution.
Time (microseconds) each SPU spent executing the different
tasks.
Change statistic to:
• #burst: number of tasks of each type by SPU.
• Average burst time: Average duration of each task
type.
task repetitions.cfg
Total DMA bw.cfg
Shows which SPU executed each task and the number of
times that the task was executed.
Total DMA (in+out) bandwidth to Memory.
22
8.2
Barcelona Supercomputing Center
Configuration file
With the objective of tuning the behaviour of the CellSs runtime, a configuration file where some
variables are set is introduced. However, we do not recommend to play with them unless the user
considers that it is required to improve the performance of her/his applications. The current set of
variables is the following (values between parenthesis denote the default value):
• scheduler.min tasks (16): defines minimum number of ready tasks before they are scheduled
(no more tasks are scheduled while this number is not reached).
• scheduler.initial tasks (128): defines the number of ready for execution tasks that are generated
at the beginning of the execution of an application before starting their execution in the SPEs.
• scheduler.max strand size (8): defines the maximum number of tasks that are simultaneously
scheduled to an SPE.
• task graph.task count high mark (1000): defines the maximum number of non-executed tasks
that the graph will hold.
• task graph.task count low mark (900): whevever the task graph reaches the number of tasks
defined in the previous variable, the task graph generation is suspended until the number of
non-executed tasks goes below this amount.
• renaming.memory high mark (∞): defines the maximum amount of memory used for renaming
in bytes.
• renaming.memory low mark (1): whenever the renaming memory usage reaches the size specified in the previous variable, the task graph generation is suspended until the renaming memory
usage goes below the number of bytes specified in this variable.
This variables are set in a plain text file, with the following syntax:
scheduler.min_tasks
scheduler.initial_tasks
scheduler.max_strand_size
task_graph.task_count_high_mark
task_graph.task_count_low_mark
renaming.memory_high_mark
renaming.memory_low_mark
=
=
=
=
=
=
=
32
128
8
2000
1500
134217728
104857600
The file where the variables are set is indicated by setting the CSS CONFIG FILE environment variable. For example, if the file “file.cfg” contains the above variable settings, the following command
can be used:
> export CSS_CONFIG_FILE=file.cfg
Some examples of configuration files for the execution of CellSs applications can be found at location
<install dir>/share/docs/cellss/examples/.
Cell Superscalar User’s Manual
9
23
CellSs SPU memory functionality
9.1
Dynamic memory allocation
Local Storage (LS) space in each SPU is limited, so CellSs tries to control as much of it as possible.
In particular, the layout of the libraries does not permit the use of the heap5 . CellSs has its own
rudimentary memory allocator, where heap space and code managing can be subsumed; as a result,
more LS space becomes available. In return, all task code must use the dynamic memory allocation
interface offered by CellSs.
This interface differs only syntactically from the familiar libc counterpart. If CellSs has been installed in prefix, then this header file can be found in prefix/CellSS-release/worker/. Allocated
space must be freed by the end of the task. Failure to do so will cause it to be lost for the remainder
of the execution. Memory obtained through this interface will always be 16-byte aligned. Large local
variables should be allocated using this interface, instead of being pushed on the stack because, by
default, CellSs reserves 4 kilobytes of stack space.
#include "css_malloc_red.h"
void *css_malloc (unsigned int size);
void css_free
(void *chunk);
9.2
DMA accesses
Although CellSs handles all data transfers for the parameters in the tasks interface, in some cases the
programmer may want to be able to do explicit data transfers from main memory. From a CellSs
task, the user can access main memory via the following set of DMA routines. All accesses are
asynchronous, and the locations in main memory should be 16-byte aligned. For transfers of 1, 2, 4, 8
bytes, or multiples of 16 bytes up to 16 kilobytes (or 16384 bytes), the interface offers the following
functions:
#include "css_dma_red.h"
void css_get_a (void *ls, uint32_t ea, unsigned int dma_size,
tagid_t tag);
void css_put_a (void *ls, uint32_t ea, unsigned int dma_size,
tagid_t tag);
Where each argument stands for:
• ls is a pointer to a 16-byte aligned user-allocated buffer in LS.
5
Which might involve serious problems if system libraries or user libraries include calls to malloc or free.
24
Barcelona Supercomputing Center
• ea is the pointer to main memory where the buffer resides that holds the object to be transfered
to ls, or where the object ls points to will be transfered.
• dma_size is the size of the object in bytes.
• tag is the identifier of the DMA transfer.
A DMA transfer or a group of DMA transfers are identified by a tag. Any number of transfers can be
grouped together by using the same tag. After starting an asynchronous transfer, its completion can
be assured via the DMA tag. A tag corresponds to a number between 0 and 31, and the user is free to
choose from this range. However, in order to avoid grouping together unrelated DMA transfers, the
user should request a DMA tag:
tagid_t css_tag (void);
Range
0–7
8 – 15
Use
short-circuit + dummy
stage in
16 – 23
stage out + alternating
24 – 31
reserved
Table 2: Use of DMA tags in the worker library.
The worker library itself divides the group of available tags according to the scheme in table 2. These
functions start the asynchronous DMA transfers. To check for their completion, the user performs a
call to:
void css_sync (tagid_t tag)
DMA transfers that do not comply to the criteria outlined above, should use the following interface
instead:
void css_get (void *ls, unsigned int address, unsigned int size,
tagid_t tag);
void css_put (void *ls, unsigned int address, unsigned int size,
tagid_t tag);
9.3 Strided Memory Access
CellSs offers an interface to scatter/gather memory access patterns for one, two and three dimensional
arrays. The only requirement is that all parts are aligned on a 16-byte boundary. Each of the functions
25
Cell Superscalar User’s Manual
chunk
stride
start
Figure 2: One dimensional memory access pattern.
in this interface returns and accepts a parameter called c_list. If a preceding call to a function
from this interface accessed main memory according to a certain pattern, this pattern can be reused by
passing it the c_list created by that call.
CellSs implements its scatter/gather functionality via DMA lists. The reuse of c_list objects enables the worker library to in turn reuse those DMA lists, instead of recreating them from scratch.
esize is the size of a single argument from the matrix in bytes. size is the total number of objects
you want to scatter/gather. ls is a pointer to a 16-byte aligned buffer in LS that has been previously
allocated by the user. start is a pointer to main memory, indicating the address of the very first element of the matrix that will be collected (gather), or the address of the location where the first element
will be put (scatter). ls is a pointer to LS, which is the beginning of the buffer that will contain the
objects to be gathered, or the beginning of the buffer that contains the objects to be scattered. start
and ls must be 16-byte aligned.
start
local_y
local_x
global_x
Figure 3: Two dimensional memory access pattern.
For the one dimension case, the situation is depicted in figure 2, and the interface is:
#include "css_stride_red.h"
26
dmal_h_t *css_gather_1d
(void *ls, unsigned
int stride, size_t
dmal_h_t *c_list);
dmal_h_t *css_scather_1d (void *ls, unsigned
int stride, size_t
dmal_h_t *c_list);
Barcelona Supercomputing Center
int start, int chunk,
size, size_t e_size,
int start, int chunk,
size, size_t e_size,
For the two dimensions case, the situation is depicted in figure 3, and the interface is:
#include "css_stride_red.h"
dmal_h_t *css_gather_2d
(void *ls, unsigned int start, int
int local_y, int global_x, size_t
dmal_h_t *c_list);
dmal_h_t *css_scather_2d (void *ls, unsigned int start, int
int local_y, int global_x, size_t
dmal_h_t *c_list);
local_x,
e_size,
local_x,
e_size,
The three dimensions case is an extension of the two dimensions case, adding an extra dimension:
#include "css_stride_red.h"
dmal_h_t *css_gather_3d
(void *ls, unsigned int start,
int local_y, int local_z, int
int global_z, size_t e_size,
dmal_h_t *c_list);
dmal_h_t *css_scather_3d (void *ls, unsigned int start,
int local_y, int local_z, int
int global_z, size_t e_size,
dmal_h_t *c_list);
int local_x,
global_x,
int local_x,
global_x,
The above DMA operations are asynchronous. After invoking these functions and starting the DMAs,
the user should wait for their completion before accessing the data they transfer. Each transfer has
associated with it a DMA tag, that can be retrieved through the tag field of the dmal_h_t object
returned by the initial invocation. For example, the following code extract illustrates how to use
css_gather_1d in a CellSs task:
#pragma css task input(A[16*16],A_p)
void matmul (float *A, unsigned int A_p)
{
#ifdef SPU_CODE
dmal_h_t *entry = css_gather_1d(A, A_p, 4, 16, 128, sizeof(float), NULL);
short tag = entry->tag;
Cell Superscalar User’s Manual
27
css_sync(tag);
#endif
}
Remark that the declaration of A as a task argument ensures that there will be a 16 × 16 byte buffer
available, and that the actual direction of A in main memory gets passed via A_p. An alternative is to
use css_malloc and css_free to manage buffers inside the task.
References
[1] Pieter Bellens, Josep M. Pérez, Rosa M. Badia, and Jesús Labarta. CellSs: A programming model
for the Cell BE architecture. In Proceedings of the ACM/IEEE SC 2006 Conference, November
2006.
[2] Barcelona Supercomputing Center. Cell Superscalar website. http://www.bsc.es/cellsuperscalar.
[3] CEPBA/UPC. Paraver website. http://www.bsc.es/paraver.
[4] Josep M. Pérez, Pieter Bellens, Rosa M. Badia, and Jesús Labarta. CellSs: Programming the
Cell/B.E. made easier. IBM Journal of R&D, 51(5), August 2007.
SMP Superscalar (SMPSs) User’s Manual
Version 2.0
Barcelona Supercomputing Center
May 2008
i
SMP Superscalar User’s Manual
Contents
1
Introduction
1
2
Installation
2.1 Compilation requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 User environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
2
3
3
Programming with SMPSs
3.1 C Programming . . . . . . . . . . .
3.1.1 Task selection . . . . . . . .
3.1.2 Specifying a task . . . . . .
3.1.3 Scheduling a task . . . . . .
3.1.4 Waiting on data . . . . . . .
3.2 Fortran Programming . . . . . . . .
3.2.1 Task selection . . . . . . . .
3.2.2 Specifying a task . . . . . .
3.2.3 Waiting on data . . . . . . .
3.2.4 Fortran compiler restrictions
.
.
.
.
.
.
.
.
.
.
3
3
3
4
5
6
7
7
7
9
10
4
Compiling
4.1 Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
11
12
5
Setting the environment and executing
5.1 Setting the number of CPUs and executing . . . . . . . . . . . . . . . . . . . . . . .
14
14
6
Programming examples
6.1 Matrix mutlitply . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
14
7
SMPSs internals
15
8
Advanced features
8.1 Using paraver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Configuration file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
17
18
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
References
19
List of Figures
1
SMPSs runtime behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
ii
Barcelona Supercomputing Center
SMP Superscalar User’s Manual
1
1
Introduction
This document is the user manual of the SMP Superscalar (SMPSs) framework, which is based on
a source-to-source compiler and a runtime library. The programming model allows programmers to
write sequential applications and the framework is able to exploit the existing concurrency and to use
the different cores of a multi-core or SMP by means of an automatic parallelization at execution time.
The requirements we place on the programmer are that the application is composed of coarse grain
functions (for example, by applying blocking) and that these functions do not have collateral effects
(only local variables and parameters are accessed). These functions are identified by annotations
(somehow similar to the OpenMP ones), and the runtime will try to parallelize the execution of the
annotated functions (also called tasks).
The source-to-source compiler separates the annotated functions from the main code and the library
calls the annotated code. However, an annotation before a function does not indicate that this is a
parallel region (as it does in OpenMP). To be able to exploit the parallelism, the SMPSs runtime
builds a data dependency graph where each node represents an instance of an annotated function and
edges between nodes denote data dependencies. From this graph, the runtime is able to schedule
for execution independent nodes to different cores at the same time. Techniques imported from
the computer architecture area like the data dependency analysis, data renaming and data locality
exploitation are applied to increase the performance of the application.
While OpenMP explicitly specifies what is parallel and what is not, with SMPSs what is specified
are functions whose invocations could be run in parallel, depending on the data dependencies. The
runtime will find the data dependencies and will determine, based on them, which functions can be run
in parallel with others and which not. Therefore, SMPSs provides programmers with a more flexible
programming model with an adaptive parallelism level depending on the application input data and
the number of available cores.
2
Installation
SMP Superscalar is distributed in source code form and must be compiled and installed before using it. The runtime library source code is distributed under the LGPL license and the rest of the
code is distributed under the GPL license. It can be downloaded from the SMPSs web page at
http://www.bsc.es/smpsuperscalar.
2.1
Compilation requirements
The SMPSs compilation process requires the following system components:
• GCC 4.1 or later
• Gfortran (install it with GCC)
• GNU make
2
Barcelona Supercomputing Center
• PAPI 3.6 only if harware counter tracing is desired
Additionally, if you change the source code you may require:
• automake
• autoconf ≥ 2.60
• libtool
• rofi-bison1
• GNU flex
2.2
Compilation
To compile and install SMPSs please follow the following steps:
1. Decompress the source tarball.
tar -xvzf SMPSS-2.0.tar.gz
2. Enter into the source directory.
cd SMPSS-2.0
3. Run the configure script, specifying the installation directory as the prefix argument. The configure script also accepts the following optional parameters:
• --enable-papi Specifies that tracing should include hardware counters. This option
requires a recent installation of PAPI.
• --with-papi=prefix Specifies the PAPI installation path.
More information can be obtained by running ./configure --help.
./configure --prefix=/opt/SMPSS
There are also some environment variables that affect the configuration behaviour.
•
•
•
•
•
•
C compiler may be specified with the CC variable.
C compiler flags may be given with the CFLAGS variable.
C++ compiler may be specified with the CXX variable.
C++ compiler flags may be given with the CXXFLAGS variable.
Fortran compiler may be specified with the FC variable.
Fortran compiler flags may be given with FCFLAGS.
4. Run make.
make
5. Run make install.
make install
1
Available at http://www.bsc.es/plantillaH.php?cat id=351
SMP Superscalar User’s Manual
2.3
3
User environment
If SMPSs has not been installed into a system directory, then the user must set the following environment variables:
1. The PATH environment variable must contain the bin subdirectory of the installation.
export PATH=$PATH:/opt/SMPSS/bin
2. The LD LIBRARY PATH environment variable must contain the lib subdirectory from the installation.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/SMPSS/lib
3
Programming with SMPSs
SMPSs applications are based on the parallelization at task level of sequential applications. The tasks
(functions or subroutines) selected by the programmer will be executed in the different cores. Furthermore, the runtime detects when tasks are data independent between them and is able to schedule the
simultaneous execution of several of them on different cores. All the above mentioned actions (data
dependency analysis, scheduling and data transfer) are performed transparently to the programmer.
However, to take benefit of this automation, the computations to be executed in the cores should be of
certain granularity (about 50µs). A limitation on the tasks is that they can only access their parameters
and local variables. In case global variables are accessed the compilation will fail.
3.1
C Programming
The C version of SMPSs borrows the syntax from OpenMP in the way that the code is annotated using
special preprocessor directives. Therefore, the same general syntax rules apply; that is, directives are
one line long but they can span into multiple lines by escaping the line ending.
3.1.1
Task selection
In SMPSs, it is a responsibility of the application programmer to select tasks of certain granularity.
For example, blocking is a technique that can be applied to increase the granularity of the tasks in
applications that operate on matrices. Below there is a sample code for a block matrix multiplication:
void block_addmultiply (double C[BS][BS], double A[BS][BS],
double B[BS][BS])
{
int i, j, k;
for (i=0; i < BS; i++)
for (j=0; j < BS; j++)
4
Barcelona Supercomputing Center
for (k=0; k < BS; k++)
C[i][j] += A[i][k] * B[k][j];
}
3.1.2
Specifying a task
A task is conceived in the form of a procedure, i.e. a function without return value. Then, a procedure
is “converted” into a SMPSs task by providing a simple annotation before its declaration or definition:
#pragma css task [input(<input parameters>)]optional \
[inout(<inout parameters>)]optional \
[output(<output parameters>)]optional \
[highpriority]optional
<function declaration or definition>
Where each clause serves the following purposes:
• input clause lists parameters whose input value will be read.
• inout clause lists parameters that will be read and written by the task.
• output clause lists parameters that will be written to.
• highpriority clause specifies that the task will be scheduled for execution earlier than tasks
without this clause.
The parameters listed in the input, inout and output clauses are separated by commas. Only the
parameter name and dimension(s) need to be specified, not the type. Although the dimensions must
be omitted if present in the parameter declaration.
Examples
In this example, the “factorial” task has a single input parameter “n” and a single output parameter
“result”.
#pragma css task input(n) output(result)
void factorial (unsigned int n, unsigned int *result)
{
*result = 1;
for (; n > 1; n--)
*result = *result * n;
}
SMP Superscalar User’s Manual
5
The next example, has two input vectors “left”, of size “leftSize”, and “right”, of size “rightSize”; and
a single output “result” of size “leftSize+rightSize”.
#pragma css task input(leftSize, rightSize) \
input(left[leftSize], right[rightSize]) \
output(result[leftSize+rightSize])
void merge (float *left, unsigned int leftSize,
float *right, unsigned int rightSize, float *result)
{
...
}
The next example shows another feature. In this case, with the keyword highpriority the user
is giving hints to the scheduler: the jacobi tasks will be, when data dependencies allow it, executed
before the ones that are not marked as high-priority.
#pragma css task input(lefthalo[32], tophalo[32], righthalo[32], \
bottomhalo[32]) inout(A[32][32]) highpriority
void jacobi (float *lefthalo, float *tophalo,
float *righthalo, float *bottomhalo, float *A)
{
...
}
3.1.3
Scheduling a task
Once all the tasks have been specified, the next step is to use them. The way to do it is as simple as
it gets: just call the annotated function normally. However, there still exists a small requirement, in
order to have the tasks scheduled by the SMPSs runtime, the annotated functions must be invoked in
a block surrounded by these two directives:
#pragma css start
#pragma css finish
These two directives can only be used once in a program, i.e. it is not possible to annotate a start
directive after2 a finish directive has been annotated. They are also mandatory and must enclose
all annotated function invocations.
Beware of the fact that the compiler will not detect these issues; the runtime will complain in some
cases but may also incur into unexpected behaviour. Section 3.1.4 provides some simple examples.
2
After in the code execution path.
6
Barcelona Supercomputing Center
3.1.4
Waiting on data
When code outside the tasks needs to handle data manipulated also by code inside the tasks, the
automatic dependency tracking performed by the runtime is not enough to ensure correct read and
write order. To solve this, SMPSs offers some synchronization directives.
As in OpenMP, there is a barrier directive:
#pragma css barrier
This forces the main thread to wait for the completion of all generated tasks so far. However, this kind
of synchronization is too coarse grained and in many cases can be counter productive. To achieve a
finer grained control over data readiness, the wait on directive is also available:
#pragma css wait on(<list of variables>)
In this case, the main thread waits (or starts running tasks) until all the values of the listed variables
have been committed. Like in other clauses3 , multiple variable names are separated by commas.
The data unit to be waited on should be consistent with the data unit of the task. For example, if the
task is operating on the full range of an array, we cannot wait on a single element arr[i] but on its
base address arr.
Examples
The next example shows how a wait on directive can be used:
#pragma css task inout(data[size]) input(size)
void bubblesort (float *data, unsigned int size)
{
...
}
void main ()
{
...
#pragma css start
bubblesort(data, size);
#pragma css wait on(data)
for (unsigned int i = 0; i < size; i++)
3
Clauses that belong to the task directive.
SMP Superscalar User’s Manual
7
printf("%f ", data[i]);
#pragma css finish
}
In this particular case, a barrier could have served for the same purpose since there is just one output
variable.
3.2
Fortran Programming
As in C, the Fortran version of SMPSs also is based on the syntax of OpenMP for Fortran-95. This
version of SMPSs only supports free form code and needs some Fortran-95 standard features.
3.2.1
Task selection
In SMPSs it is responsibility of the application programmer to select tasks of a certain granularity.
For example, blocking is a technique that can be applied to increase such granularity in applications
that operate on matrices. Below there is a sample code for a block matrix multiplication:
subroutine block_addmultiply(C, A, B, BS)
implicit none
integer, intent(in) :: BS
real, intent(in) :: A(BS,BS), B(BS,BS)
real, intent(inout) :: C(BS,BS)
integer :: i, j, k
do i=1, BS
do j=1, BS
do k=1, BS
C(i,j) = C(i,j) + A(i,k)*B(k,j)
enddo
enddo
enddo
end subroutine
3.2.2
Specifying a task
A task is conceived in the form of a subroutine. The main difference with C SMPSs annotations it that
in Fortran, the language provides the means to specify the direction of the arguments in a procedure.
Moreover, while arrays in C can be passed as pointers, Fortran does not encourage that practice. In
this sense, annotations in Fortran are simpler than in C. The annotations have the form of a Fortran-95
comment followed by a “$” and the framework sentinel keyword (CSS in this case). This is very
similar to the syntax OpenMP uses in Fortran-95.
8
Barcelona Supercomputing Center
In Fortran, each subprogram calling tasks must know the interface for those tasks. For this purpose,
the programmer must specify the task interface in the caller subprograms and also write some SMPSs
annotations to let the compiler know that there is a task. The following requirements must be satisfied
by all Fotran tasks in SMPSs:
• The task interface must specify the parameter directions of all parameters. That is, by using
INTENT (<direction>); where <direction> is one of: IN, INOUT or OUT.
• Provide an explicit shape for all array parameters in the task (caller subprogram).
• Provide a !$CSS TASK annotation for the caller subprogram with the task interface.
• Provide a !$CSS TASK annotation for the task subroutine.
The following example shows how a subprogram calling a SMPSs task looks in Fortran. Note that it
is not necessary to specify the parameter directions in the task subroutine, they are only necessary in
the interface.
subroutine example()
...
interface
!$CSS TASK
subroutine block_add_multiply(C, A, B, BS)
! This is the task interface. Specifies the size and
! direction of the parameters.
implicit none
integer, intent(in) :: BS
real, intent(in) :: A(BS,BS), B(BS,BS)
real, intent(inout) :: C(BS,BS)
end subroutine
end interface
...
!$CSS START
...
call block_add_multiply(C, A, B, BLOCK_SIZE)
...
!$CSS FINISH
...
end subroutine
!$CSS TASK
subroutine block_add_multiply(C, A, B, BS)
! Here goes the body of the task (the block multiply_add
! in this case)
...
end subroutine
SMP Superscalar User’s Manual
9
It is also necessary, as the example shows, to call the tasks between START and FINISH annotation
directives. These are executable statements that must be after the declarations, in the executable part
of the subprogram. START and FINISH statements must only be executed once in the application.
Example
The following example shows part of a SMPSs application using another feature. The HIGHPRIORITY
clause is used to indicate that one task is high priority and must be executed before non-high priority
tasks as soon as its data dependencies allow.
interface
!$CSS TASK HIGHPRIORITY
subroutine jacobi(lefthalo, tophalo, righthalo, bottomhalo, A)
real, intent(in), dimension(32) :: lefthalo, tophalo, &
righthalo, bottomhalo
real, intent(inout) :: A(32,32)
end subroutine
end interface
3.2.3
Waiting on data
SMPSs provides two different ways of waiting on data. These features are useful when the user
wants to read the results of some tasks in the main thread. Since the main thread does not have the
control over task execution, it does not know when a task has finished executing. With BARRIER and
WAIT ON, the main program stops its execution until the data is available.
BARRIER is the most conservative option. When the main thread reaches a barrier, waits until all
tasks have finished . The syntax is simple:
...
do i=1, N
call task_a(C(i),B)
enddo
...
!$CSS BARRIER
print *, C(1)
...
The other way is to specify exactly which variables we want the program to wait to be available before
the execution goes on.
!$CSS WAIT ON(<list of variables>)
Where the list of variables is a comma separated list of variable names whose values must be correct
before continuing the execution.
10
Barcelona Supercomputing Center
Example
!$CSS TASK
subroutine bubblesort (data, size)
integer, intent(in) :: size
real, intent(inout) :: data(size)
...
end subroutine
program main
...
interface
!$CSS TASK
subroutine bubblesort (data, size)
integer, intent(in) :: size
real, intent(inout) :: data(size)
end subroutine
end interface
...
call bubblesort(data, size);
!$CSS WAIT ON(data)
do i=1, size
print *, data(i)
enddo
end
3.2.4
Fortran compiler restrictions
This is the first release of SMPSs with a Fortran compiler and it has some limitations. Some will
disappear in the future. They consist of compiler specific and non-standard features. Also deprecated forms in the Fortran-95 standard are not supported and are not planned to be included in future
releases.
• Case sensitiveness: The SMPSs Fortran compiler is case insensitive. However, task names must
be written in lowercase.
• It is not allowed to mix generic interfaces with tasks.
• Internal subprograms cannot be tasks.
• Use of modules within tasks has not been tested in this version.
• Optional and named parameters are not allowed in tasks.
11
SMP Superscalar User’s Manual
• Some non-standard common extensions like Value parameter passing are not supported or have
not been tested yet. In further releases of SMPSs we expect to support a subset of the most
common extensions.
• Only explicit shape arrays and scalars are supported as task parameters.
• The MULTOP parameter is not supported.
• Tasks cannot have an ENTRY statement.
• Array subscripts cannot be used as task parameters.
• PARAMETER arrays cannot be used as task parameters.
4
Compiling
The SMPSs compiler infastructure is composed of a C99 source-to-source compiler, a Fortran-95
source-to-source compiler and a common driver.
The driver is called smpss-cc and depending on each source filename suffix invokes transparently
the C compiler or the Fortran-95 compiler. C files must have the “.c” suffix. Fortran files can have
either the “.f”, “.for”, “.f77”, “.f90”, or “.f95” suffix.
The smpss-cc driver behaves similarly to a native compiler. It can compile individual files one at a
time, several ones, link several objects into an executable or perform all operations in a single step.
The compilation process consists in processing the SMPSs pragmas, transforming the code according
to those, compiling for the native architeture with the corresponding compiler and packing the object
with additional information required for linking.
The linking process consists in unpacking the object files, generating additional code required to join
all object files into a single executable, compiling it, and finally linking all objects together with the
SMPSs runtime to generate the final executable.
4.1
Usage
The smpss-cc compiler has been designed to mimic the options and behaviour of common C compilers. We also provide a means to pass non-standard options to the platform compiler and linker.
The list of supported options is the following:
> smpss-cc -help
Usage: cellss-cc <options and sources>
Options:
-D<macro>
Defines ’macro’ with value ’1’ in the
preprocessor.
12
Barcelona Supercomputing Center
-D<macro>=<value>
-g
-h|--help
-I<directory>
-k|--keep
-l<library>
-L<directory>
-O<level>
-o <filename>
-c
-t|--tracing
-v|--verbose
Defines ’macro’ with value ’value’ in the
preprocessor.
Enables debugging.
Shows usage help.
Adds ’directory’ the list of preprocessor
search paths.
Keeps intermediate source and object files.
Links with the specified library.
Adds ’directory’ the list of library search
paths.
Enables optimization level ’level’.
Sets the name of the output file.
Specifies that the code must only be compiled
(and not linked).
Enables run time tracing.
Enables verbose operation.
SMP specific options:
-Wp,<options>
Passes the comma separated list
the C preprocessor.
-Wc,<options>
Passes the comma separated list
the native C compiler.
-Wf,<options>
Passes the comma separated list
the native Fortran compiler.
-Wl,<options>
Passes the comma separated list
the linker.
4.2
of options to
of options to
of options to
of options to
Examples
Contrary to previous versions of the compiler, now it is possible to generate binaries from multiple
source files like any regular C or Fortran95 compiler. Therefore, it is possible to compile multiple
source files directly into a single binary:
> smpss-cc -O3 *.c -o my_binary
Although handy, you may also use the traditional compilation methodology:
>
>
>
>
smpss-cc
smpss-cc
smpss-cc
smpss-cc
-O3
-O3
-O3
-O3
-c code1.c
-c code2.c
-c code3.f90
code1.o code2.o code3.o -o my_binary
This capability allows to easily adapting makefiles by just changing the C compiler, the Fortran compiler and the linker to point to smpss-cc. For instance:
SMP Superscalar User’s Manual
13
CC = smpss-cc
LD = smpss-cc
CFLAGS = -O2 -g
SOURCES = code1.c code2.c code3.c
BINARY = my_binary
$(BINARY): $(SOURCES)
Combining the -c and -o options makes possible to generate objects with arbitrary filenames. However, changing the suffix to other than .o is not recommended since, in some cases, the compiler
driver relies on them to work properly.
As already mentioned, the same binary serves as a Fortran95 compiler:
> smpss-cc -O3 matmul.f90 -o matmul
If there are no compilation errors, the executable file “matmul” (optimized) is created and can be
called from the command line (“> ./matmul ...”).
In some cases, it is desirable to use specific optimization options not included in the -O, -O1, -O2,
or -O3 set. This is possible by using the -Wc flag:
> smpss-cc -O2 -Wc,-funroll-loops,-ftree-vectorize \
-Wc,-ftree-vectorizer-verbose=3 matmul-c -o matmul
In the previous example, the native options are passed directly to the native compiler (for example
c99), to perform automatic vectorization of the code. Note: at the time this manual was written,
vectorization seemed not to work properly on gcc with -O3.
Option -k, or --keep, will not delete the intermediate files (files generated by the preprocessor,
object files, ...).
> smpss-cc -k cholesky.c -o cholesky
Finally, option -t enables executable instrumentation to generate a runtime trace to be analyzed later
with the appropriate tool:
> smpss-cc -O2 -t matmul.c -o matmul
When executing “matmul”, a trace file of the execution of the application will be generated. See
section 8.1 for further information on trace analysis.
14
5
Barcelona Supercomputing Center
Setting the environment and executing
Depending on the path chosen for installation (see section 2), the LD LIBRARY PATH environment
variable may need to be set appropriately or the application will not be able to run.
If SMPSs was configured with --prefix=/foo/bar/SMPSS, then LD LIBRARY PATH should
contain the path /foo/bar/SMPSS/lib. If the framework is installed in a system location such as /usr,
setting the loader path is not necessary.
5.1
Setting the number of CPUs and executing
Before executing a SMPSs application, the number of processors to be used in the execution have to
be defined. The default value is 2, but it can be set to a different number with the CSS NUM CPUS
environment variable, for example:
> export CSS_NUM_CPUS=6
SMPSs applications are started from the command line in the same way as any other application. For
example, for the compilation examples of section 4.2, the applications can be started as follow:
> ./matmul <pars>
> ./cholesky <pars>
6
Programming examples
This section presents a programming example for the block matrix multiplication. The code is not
complete, but you can find the complete and working code under share/docs/cellss/examples/ in the
installation directory. More examples are also provided in this directory.
6.1
Matrix mutlitply
This example presents a SMPSs code for a block matrix multiply. The block contains BS ×BS floats.
#pragma css task input(A, B) inout(C)
static void block_addmultiply (float C[BS][BS], float A[BS][BS],
float B[BS][BS])
{
int i, j, k;
for (i = 0; i < BS; i++)
for (j = 0; j < BS; j++)
for (k = 0; k < BS; k++)
15
SMP Superscalar User’s Manual
C[i][j] += A[i][k] * B[k][j];
}
int main(int argc, char **argv)
{
int i, j, k;
initialize(argc, argv, A, B, C);
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
for (k = 0; k < N; k++)
block_addmultiply(C[i][j], A[i][k], B[k][j]);
...
}
The main code will run in the main thread while the block_addmultiply calls will be executed
in all the threads. It is important to note that the sequential code (including the annotations) can be
compiled with the native compiler, obtaining a sequential binary. This is very useful for debugging
the algorithms.
7
SMPSs internals
CPU0
CPU1
Main thread
User main
program
Original
task code
Worker thread 1
SMPSs runtime library
Data dependence
Data renaming
Global
Ready task queues
Renaming table
...
CPU2
Worker thread 2
SMPSs runtime library
Scheduling
Task execution
Original
task code
Thread 0
Ready task queue
SMPSs ru
Scheduling
Task execution
Original
task code
Thread 1
Ready task queue
Thread 2
Ready task queue
High pri
Work stealing
Work stealing
Low pri
Memory
Figure 1: SMPSs runtime behavior
When compiling a SMPSs application with smpss-cc, the resulting object files are linked with the
SMPSs runtime library. Then, when the application is started, the SMPSs runtime is automatically
invoked. The SMPSs runtime is decoupled in two parts: one runs the main user code and the other
runs the tasks.
16
Barcelona Supercomputing Center
The most important change in the original user code is that the SMPSs compiler replaces calls to tasks
with calls to the css_addTask function. At runtime, these calls will be responsible for the intended
behavior of the application. At each call to css_addTask, the main thread will do the following
actions:
• Add node that represents the called task in a task graph.
• Analyze data dependencies of the new task with other previously called tasks.
• Parameter renaming: similarly to register renaming, a technique from the superscalar processor
area, we do renaming of the output parameters. For every function call that has a parameter
that will be written, instead of writing to the original parameter location, a new memory location will be used, that is, a new instance of that parameter will be created and it will replace
the original one, becoming a renaming of the original parameter location. This allows to execute that function call independently from any previous function call that would write or read
that parameter. This technique allows to effectively remove some data dependencies by using
additional storage, and thus improving the chances to extract more parallelism.
Every thread has its own ready task queue, including the main thread. There is also a global queue
with priority. Whenever a task that has no predecessors is added to the graph, it is also added to the
global ready task queue.
The worker threads consume ready tasks from the queues in the following order of preference:
1. High priority tasks from the global queue.
2. Tasks from its their own queue in LIFO order.
3. Tasks from any other thread queue in FIFO order.
Whenever a thread finishes executing a task, it checks what tasks have become ready and adds them
to its own queue. This allows the thread to continue exploring the same area of the task graph unless
there is a high priority task or that area has become empty.
In order to preserve temporal locality, threads consume tasks of their own queue in LIFO order, which
allows them to reuse output parameters to a certain degree. The task stealing policy tries to minimise
adverse effects on the cache by stealing in FIFO order, that is, it tries to steal the coldest tasks of the
stolen thread.
The main thread purpose is to populate the graph in order to feed tasks to the worker threads. Nevertheless, it may stop generating new tasks for several conditions: too many tasks in the graph, a wait
on, a barrier or and end of program. In those situations it follows the same role as the worker threads
by consuming the tasks until the blocking condition is no longer valid.
17
SMP Superscalar User’s Manual
8
8.1
Advanced features
Using paraver
To understand the behavior and performance of the applications, the user can generate Paraver [2]
tracefiles of their SMPSs applications.
If the -t/-tracing flag is enabled at compilation time, the application will generate a Paraver
tracefile of the execution. The default name for the tracefile is gss-trace-id.prv. The name can be
changed by setting the environment variable CSS TRACE FILENAME. For example, if it is set as
follows:
> export CSS_TRACE_FILENAME=tracefile
After the execution, the files: tracefile-0001.row, tracefile-0001.prv and tracefile-0001.pcf are generated. All these files are required by the Paraver tool.
The traces generated by SMPSs can be visualized and analyzed with Paraver. Paraver [2] is distributed
independently of SMPSs.
Several configuration files to visualise and analyse SMPSs tracefiles are provided in the SMPSs distribution in the directory <install dir>/share/cellss/paraver cfgs/. The following table summarizes
what is shown by each configuration file.
Configuration file
3dh duration phase.cfg
3dh duration tasks.cfg
execution phases.cfg
flushing.cfg
general.cfg
task.cfg
task distance histogram.cfg
task number.cfg
Feature shown
Histogram of duration for each of the runtime phases.
Histogram of duration of tasks. One plane per task (Fixed
Value Selector).
Left column: 0 microseconds. Right column: 300 us.
Darker colour means higher number of instances of that duration.
Profile of percentage of time spent by each thread (main and
workers) at each of the major phases in the runt time library
(i.e. generating tasks, scheduling, task execution, . . . ).
Intervals (dark blue) where each thread is flushing its local
trace buffer to disk.
Mix of timelines.
Outlined function being executed by each thread.
Histogram of task distance between dependent tasks.
Number (in order of task generation) of task being executed
by each thread.
Ligth green for the initial tasks in program order, blue for the
last tasks in program order.
Intermixed green an blue indicate out of order execution.
18
Barcelona Supercomputing Center
Configuration file
Task profile.cfg
Feature shown
Time (microseconds) each thread spent executing the different tasks.
Change statistic to:
• #burst: number of tasks of each type by thread.
• Average burst time: Average duration of each task
type.
task repetitions.cfg
8.2
Shows which thread executed each task and the number of
times that the task was executed.
Configuration file
With the objective of tuning the behaviour of the SMPSs runtime, a configuration file where some
variables are set is introduced. However, we do not recommend to play with them unless the user
considers that it is required to improve the performance of her/his applications. The current set of
variables is the following (values between parenthesis denote the default value):
• task graph.task count high mark (1000): defines the maximum number of non-executed tasks
that the graph will hold.
• task graph.task count low mark (900): whevever the task graph reaches the number of tasks
defined in the previous variable, the task graph generation is suspended until the number of
non-executed tasks goes below this amount.
• renaming.memory high mark (∞): defines the maximum amount of memory used for renaming
in bytes.
• renaming.memory low mark (1): whenever the renaming memory usage reaches the size specified in the previous variable, the task graph generation is suspended until the renaming memory
usage goes below the number of bytes specified in this variable.
• tracing.papi.hardware events: Specifies a series of hardware counters that will be measured
when tracing is enabled and the runtime has been compiled with PAPI support. The list of available hardware counters can be obtained with the PAPI command “papi avail -a” and “papi native avail
-a”. The elements in the list can be separated by comas or spaces. Note that not all counter combinations are valid.
This variables are set in a plain text file, with the following syntax:
task_graph.task_count_high_mark
task_graph.task_count_low_mark
renaming.memory_high_mark
renaming.memory_low_mark
tracing.papi.hardware_events
=
=
=
=
=
2000
1500
134217728
104857600
PAPI_TOT_CYC,PAPI_TOT_INS
SMP Superscalar User’s Manual
19
The file where the variables are set is indicated by setting the CSS CONFIG FILE environment variable. For example, if the file “file.cfg” contains the above variable settings, the following command
can be used:
> export CSS_CONFIG_FILE=file.cfg
Some examples of configuration files for the execution of SMPSs applications can be found at location
<install dir>/share/docs/cellss/examples/.
References
[1] Barcelona Supercomputing Center. SMP Superscalar website. http://www.bsc.es/smpsuperscalar.
[2] CEPBA/UPC. Paraver website. http://www.bsc.es/paraver.
[3] Josep M. Pérez, Rosa M. Badia, and Jesús Labarta. A flexible and portable programming model
for SMP and multi-cores. Technical report, Barcelona Supercomputing Center – Centro Nacional
de Supercomputación, June 2007.
Descargar