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.