Universidad de Las Palmas de Gran Canaria Estudio de utilización efectiva de procesadores vectoriales Proyecto de Fin de Carrera Ingenierı́a en Informática Laura Autón Garcı́a Tutores: Francisca Quintana Domı́nguez Roger Espasa Sans Las Palmas de Gran Canaria, 9 de julio de 2014 Agradecimientos Quiero agradecer a Francisca Quintana y a Roger Espasa, mis tutores de proyecto, el haberme brindado la oportunidad de adentrarme en una experiencia que bien podrı́a ser el sueño de cualquier futuro ingeniero informático cuando avista cada vez más cerca la meta de su esfuerzo. Este viaje no solo ha dado como resultado el presente trabajo, sino también la satisfacción profesional de haber trabajado en Intel, empresa puntera en el ámbito de la computación, y personal de haber trabajado con extraordinarios ingenieros a la vez que fantásticas personas durante todo el proceso. Entre ellos, quiero agradecer especialmente a Manel Fernández por la enorme paciencia y dedicación con las que consiguió guiarme cuando me desviaba del camino, y a Jesús Sánchez porque su buen humor y positivismo amenizaba todas las tormentas de ideas, por muy oscuras que pudieran divisarse a lo lejos. Del mismo modo, quiero agradecer muy especialmente a Susana y Delfı́n, por haber sido mi familia durante mi estancia en Canarias. A mis padres, Marı́a y Cándido por haber sabido apoyarme desde la distancia con sus palabras al otro lado del teléfono. Y a Raúl, mi gran compañero en este viaje, porque ha sido la única persona de este mundo que realmente ha conocido mis más profundas inquietudes, y que ha sabido iluminarme el camino y cederme las mangas sobre las que derramar mis lágrimas. i Índice de figuras 2.1. SISD . . . . . . . . . . . . 2.2. SIMD . . . . . . . . . . . 2.3. MISD . . . . . . . . . . . 2.4. MIMD . . . . . . . . . . . R Xeon PhiTM . . . 2.5. Intel 2.6. Esquema general . . . . . 2.7. Microarquitectura . . . . 2.8. Vector Processing Unit . . 2.9. Interconexion . . . . . . . 2.10. Directorio de etiquetas . . 2.11. Controladores de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 5 9 9 10 10 11 11 12 4.1. Arquitectura software de Pin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 5.1. 5.2. 5.3. 5.4. 5.5. 5.6. Diagrama de funcionamiento CMP$im . . . . Simulación en modo buffer . . . . . . . . . . Simulación en modo instrucción a instrucción Ejemplo de bloque básico . . . . . . . . . . . Proceso de descubrimiento de bloques . . . . Punteros a objetos cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 32 33 34 34 36 6.1. Índice de vectorización de las aplicaciones de Polyhedron . 6.2. Razones para no vectorizar bucles en Polyhedron . . . . . 6.3. Índice de vectorización de las aplicaciones de Mantevo 1.0 6.4. Razones para no vectorizar bucles en Mantevo 1.0 . . . . 6.5. Índice de vectorización de las aplicaciones de Sequoia . . . 6.6. Razones para no vectorizar bucles en Sequoia . . . . . . . 6.7. Índice de vectorización de las aplicaciones de NPB . . . . 6.8. Razones para no vectorizar bucles en NPB . . . . . . . . . 6.9. Índice de vectorización de las aplicaciones de SPEC fp . . 6.10. Razones para no vectorizar bucles en SPEC fp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 41 42 43 44 44 45 46 47 48 7.1. 7.2. 7.3. 7.4. 7.5. 7.6. 7.7. Pipeline dentro de CMP$im . . . . . . . . . . . . . Pipeline del bloque de s171 . . . . . . . . . . . . . Localización del simulador de pipeline en CMP$im Idea para la implementación de KNC . . . . . . . . Instrucción que toca dos lı́neas . . . . . . . . . . . Bloques con ningún y un corte . . . . . . . . . . . Bloques con 2 cortes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 52 56 56 58 63 64 8.1. 8.2. 8.3. 8.4. 8.5. Versión vectorizada vs no vectorizada de Polyhedron Ciclos desglosados de las aplicaciones de Polyhedron Versión vectorizada vs no vectorizada de Mantevo . Ciclos desglosados de las aplicaciones de Mantevo . . Versión vectorizada vs no vectorizada de Sequoia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 67 69 69 70 iii . . . . . . . . . . . . . . . . . ÍNDICE DE FIGURAS iv 8.6. Ciclos desglosados de las aplicaciones de Sequoia . . . . 8.7. Versión vectorizada vs no vectorizada de NPB . . . . . . 8.8. Ciclos desglosados de las aplicaciones de NPB . . . . . . 8.9. Versión vectorizada vs no vectorizada de SPEC fp . . . 8.10. Ciclos desglosados de las aplicaciones de SPEC fp 2006 . 8.11. Comparación entre las versiones :nodes y do de gas dyn 8.12. Resultado de doblar la UL2 de 1024Kb a 2048Kb . . . . 8.13. Mejora de SPEC fp/433.milc al doblar la L2 . . . . . . . 8.14. Consecuencia posible por aumento de aciertos en L2 . . 8.15. Resultado de doblar las lı́neas de DTLB2 de 256 a 512 . 8.16. Mejora de IS de NPB al doblar la TLB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 . 71 . 72 . 73 . 73 . 84 . 97 . 98 . 99 . 100 . 100 Índice de tablas R ICC . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1. Knobs soportados por Intel 28 6.1. 6.2. 6.3. 6.4. 6.5. . . . . . 40 42 43 45 47 7.1. Latencias de memoria y de instrucción (load-op) . . . . . . . . . . . . . . . . . . . 51 8.1. Bloques 1 y 2 de la lista de bloques básicos más ejecutados en Fatigue, Polyhedron 8.2. Bloque 3 de la lista de bloques básicos más ejecutados en Fatigue, Polyhedron . . . 8.3. Bloques 1, 2, 4 y 5 de la lista de bloques básicos más ejecutados en Induct, Polyhedron 8.4. Bloques 1 y 2 más ejecutados de Aermod, Polyhedron . . . . . . . . . . . . . . . . 8.5. Bloques 3, 5 y 10 más ejecutados de Aermod, Polyhedron . . . . . . . . . . . . . . 8.6. Bloques 7 y 9 más ejecutados de Aermod, Polyhedron . . . . . . . . . . . . . . . . 8.7. Bloque 8 más ejecutado de Aermod, Polyhedron . . . . . . . . . . . . . . . . . . . 8.8. Desglose de instrucciones de las versiones escalar y vectorial de Gas dyn, Polyhedron 8.9. Bloques 1 y 3 más ejecutados de Gas dyn, Polyhedron . . . . . . . . . . . . . . . . 8.10. Bloques 1, 2, 3 y 4 más ejecutados de SPhotmk, Sequoia . . . . . . . . . . . . . . . 8.11. Bloque 1 de los más ejecutados de BT, NPB . . . . . . . . . . . . . . . . . . . . . . 8.12. Bloques 1 y 2 de los más ejecutados de LU, NPB . . . . . . . . . . . . . . . . . . . 8.13. Bloques 1 y 2 de los más ejecutados de Povray, SPEC FP . . . . . . . . . . . . . . 8.14. Aplicaciones con una mejora inferior al 1 % . . . . . . . . . . . . . . . . . . . . . . 75 76 77 79 80 80 82 82 83 88 90 91 94 97 Desglose Desglose Desglose Desglose Desglose de de de de de instrucciones instrucciones instrucciones instrucciones instrucciones de de de de de las las las las las aplicaciones aplicaciones aplicaciones aplicaciones aplicaciones de de de de de Polyhedron Mantevo . . Sequoia . . NPB . . . . SPEC FP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . R ICC Specific Pragmas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 A.1. Intel R ICC Supported Pragmas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 B.1. Intel R Fotran Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 C.1. Intel D.1. Mensajes del compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 v Índice general Agradecimientos I Lista de figuras VIII Lista de tablas VIII 1. Introducción 1.1. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Estado del arte 2.1. Taxonomı́a de Flynn . . . . . . . . . . . . . . . 2.2. Vectorización . . . . . . . . . . . . . . . . . . . 2.2.1. SIMD . . . . . . . . . . . . . . . . . . . R Xeon PhiTM Coprocessor . . . . . . . . 2.3. Intel 2.3.1. Microarquitectura . . . . . . . . . . . . R Advanced Vector Extensions . . . . . . 2.4. Intel R Advanced Vector Extensions 1 . 2.4.1. Intel R Advanced Vector Extensions 2 . 2.4.2. Intel R Advanced Vector Extensions 512 2.4.3. Intel 1 2 . . . . . . . . . 3 4 5 5 8 9 13 13 13 13 3. Metodologı́a 3.1. Plan de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 4. Herramientas 4.1. Pin . . . . . . . . . . . . . . . . . . . . . 4.1.1. Pintools . . . . . . . . . . . . . . 4.1.2. Arquitectura software . . . . . . 4.2. CMP$im . . . . . . . . . . . . . . . . . . 4.3. Benchmarks . . . . . . . . . . . . . . . . 4.3.1. Polyhedron Fortran Benchmarks 4.3.2. Mantevo 1.0 . . . . . . . . . . . . 4.3.3. ASC Sequoia Benchmark Codes . 4.3.4. NAS Parallel Benchmarks . . . . 4.3.5. SPEC CPU 2006 . . . . . . . . . 4.4. Compiladores . . . . . . . . . . . . . . . 4.4.1. ICC . . . . . . . . . . . . . . . . 4.4.2. IFORT . . . . . . . . . . . . . . 4.5. Pragmas . . . . . . . . . . . . . . . . . . 4.6. Herramientas internas . . . . . . . . . . . . . . . . . . . . . . . . . 19 19 19 21 22 23 24 25 25 25 26 27 27 28 29 29 5. Arquitectura del Simulador 5.1. Flujo de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2. Estructuras y clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3. Parámetros de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 33 37 vii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ÍNDICE GENERAL viii 6. Caracterización de benchmarks 6.1. Polyhedron . . . . . . . . . . . 6.2. Mantevo 1.0 . . . . . . . . . . . 6.3. Sequoia . . . . . . . . . . . . . 6.4. NPB . . . . . . . . . . . . . . . 6.5. SPEC FP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 41 42 43 46 7. Adaptación del Simulador 7.1. Pipeline . . . . . . . . . . . . . . . . . . . . . . 7.2. Detección de instrucciones y registros . . . . . 7.2.1. Instrucciones . . . . . . . . . . . . . . . 7.2.2. Registros . . . . . . . . . . . . . . . . . 7.2.3. Latencias . . . . . . . . . . . . . . . . . 7.3. Nuevas estructuras y clases . . . . . . . . . . . 7.4. Estadı́sticas . . . . . . . . . . . . . . . . . . . . 7.5. Invocación activando la funcionalidad vectorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 49 53 53 54 54 55 62 64 8. Estudio experimental 8.1. Resultados . . . . . . . . . . 8.1.1. Polyhedron . . . . . 8.1.2. Mantevo . . . . . . . 8.1.3. Sequoia . . . . . . . 8.1.4. NPB . . . . . . . . . 8.1.5. SPEC fp . . . . . . . 8.2. Diagnóstico Software . . . . 8.2.1. Polyhedron . . . . . 8.2.2. Mantevo . . . . . . . 8.2.3. Sequoia . . . . . . . 8.2.4. NPB . . . . . . . . . 8.2.5. SPEC fp . . . . . . . 8.3. Diagnóstico Hardware . . . 8.3.1. Incremento de UL2 . 8.3.2. Incremento de TLB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 65 65 68 70 71 72 74 75 84 86 89 92 96 97 99 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9. Conclusiones 101 9.1. Trabajo Futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 R ICC Specific Pragmas A. Intel 105 R ICC Supported Pragmas B. Intel 109 R Fortran Directives C. Intel 113 D. Mensajes del compilador 117 Capı́tulo 1 Introducción Echando una profunda mirada al pasado para recorrer toda la historia de la informática, desde donde venimos, en qué punto nos encontramos y a dónde vamos, nos damos cuenta del gran esfuerzo que ha hecho y sigue haciendo el ser humano para no solo automatizar tareas, sino también para que éstas se hagan lo más rápido posible. Y es que, ya en su momento, para realizar cálculos balı́sticos de gran utilidad en posibles contiendas, se incrementaba el número de personas que realizaban una tarea. Al segmentar el trabajo se conseguı́a realizar la tarea en menos tiempo de lo que lo conseguirı́a una sola. A estas personas se las denominaba antiguamente computers[Bar08], nombre que más tarde se adoptó para las máquinas que sustituyeron su trabajo. Desde entonces, la capacidad de cómputo de las máquinas ha ido evolucionado enormemente gracias a, por ejemplo, la cantidad de transistores que fuimos capaces de insertar dentro de una onza de silicio y que bien supo pronosticar Gordon Moore, cuya afirmación se bautizo como Ley de Moore. Cuando las limitaciones fı́sicas se convirtieron en un problema, empezamos a introducir más núcleos en un mismo procesador: primero dos, luego cuatro... Está claro que, sean los motivos que sean los que impulsen al ser humano a seguir escudriñando mejoras en cualquier tipo de artefacto, mecanismo o sistema que tenga entre manos, y sobre el que haya trabajado desde tiempos inmemoriales, el mundo, inexorablemente, se sigue moviendo. Y es en ese mundo en constante cambio y movimiento, donde acaban por surgir ideas como aquella sobre la que se ha construido el trabajo que se presenta: la vectorización. Hoy en dı́a, una importante muestra de procesadores disponibles en el mercado disponen de unidades de cómputo, denominadas vectoriales, que permiten la explotación de este concepto. Y es que la vectorización explota un caso particular de paralelismo cuyo objetivo consiste en realizar la misma operación, en vez de sobre un único dato como venı́a siendo hasta ahora, sobre la mayor cantidad de datos contenidos en un vector que le sea posible. Por ello, se denomina DLP (Data Level Parallelism) o Paralelismo de datos. Algunos de estos procesadores, por ejemplo, son los de R basados en la arquitectura Sandy Bridge que, con el objetivo de permitir la explotación del Intel paralelismo de datos, incluyen extensiones AVX (Advanced Vector Extensions) sobre el repertorio de instrucciones x86. Sin embargo, esta obra de ingenierı́a no es suficiente por sı́ sola. Es necesario un engranaje más, y que no es otro que un compilador especialmente construido para máquinas como estas, que sea capaz de extraer el mayor paralelismo de datos posible de una aplicación. Pese a que todos los elementos mencionados conforman la receta perfecta para sacar el mayor rendimiento posible a aplicaciones que requieren de una importante capacidad de cómputo, no siempre se obtienen los resultados esperados. Las razones pueden residir tanto en el software como en el hardware. Puede que la aplicación no experimente las mejoras esperadas después de ser vectorizada. Es posible que el compilador no sea capaz por sı́ mismo de encontrar potenciales secciones de código vectorizables debido a ambigüedades en el acceso a los datos. O bien, podrı́a 1 2 CAPÍTULO 1. INTRODUCCIÓN ser que la memoria esté suponiendo un cuello de botella a la hora de recuperar los datos sobre los que operar. Basándonos pues en la realidad descrita, se propuso la realización del trabajo que se detalla en este documento, con el objetivo fundamental de determinar el grado de utilización efectiva de la unidad vectorial de un procesador. Se realizarı́a entonces, para aquellos casos donde el uso fuera menor del esperado, un diagnóstico del problema que permitiera lograr una mejora en el rendimiento de la aplicación. 1.1. Objetivos El objetivo principal de este Proyecto Final de Carrera, consiste en determinar el grado de utilización efectiva de la unidad vectorial de un procesador. Para lograr la consecución del mismo, se proponen los siguientes objetivos parciales: Analizar y clasificar un conjunto de aplicaciones numéricas en función del grado de vectorización sobre un compilador determinado. Determinar las causas del bajo grado de vectorización, a partir de la simulación de las aplicaciones según el funcionamiento de un producto existente que hace uso de la unidad vectorial. Las posibles causas serán las siguientes: • Problemática en el algoritmo base de la aplicación debido a dependencias en el código. • Problemática en los criterios seguidos a la hora de escribir el código fuente. • Incapacidad del compilador de detectar que el código es vectorizable. • Problemas en la microarquitectura. Proponer cambios hardware/software que faciliten el uso efectivo de la unidad vectorial. Capı́tulo 2 Estado del arte La computación paralela es una forma de cómputo consistente en paralelizar la mayor cantidad de tareas posible con el objetivo de reducir el coste de cómputo de un programa. Tradicionalmente se utilizaba otro paradigma: la computación serie. Con ella las instrucciones se ejecutaban una tras otra en la Unidad Central de Procesamiento (CPU). La utilización de este paradigma produce que, a medida que se incrementa la frecuencia de funcionamiento de la máquina, se disminiuya el tiempo que tardan en ejecutarse los programas[HP02]. El aumento de la frecuencia, que tuvo su apogeo durante las dos últimas décadas del siglo XX y principios del XXI, no podı́a ser infinito, ya que es directamente proporcional al aumento de la energı́a consumida por el procesador y, por ende, a la generación de calor. Por este motivo, pese a que la computación paralela se empezó a usar principalmente en el área de la computación de altas prestaciones, este lı́mite en el aumento de la frecuencia propició que desde la ultima década, el paradigma principal en arquitectura de computadores sea la computación paralela.[Bar07] Existen diferentes fuentes de paralelismo disponibles para sacar partido a la computación paralela. Estas son: Paralelismo de Instrucciones (ILP), Paralelismo de Datos (DLP) y Paralelismo de Tareas (TLP)[Dı́06]: ILP: consiste en ejecutar el mayor número de instrucciones posibles en paralelo sin que ello afecte al correcto flujo del programa. Como ejemplos tenemos las arquitecturas superescalares y VLIW, del inglés Very Long Instruction Word: • Superescalares: capaces de introducir en el pipeline de ejecución una o más instrucciones por ciclo, de manera que se pueden estar ejecutando paralelamente varias en un mismo ciclo. • VLIW: la arquitectura permite empaquetar varias instrucciones independientes que se ejecutarán simultáneamente. Con el objetivo de explotar al máximo esta fuente de paralelismo, existen diferentes técnicas que se podrı́an clasificar en técnicas de planificación estática y dinámica[Dı́06]. • Técnicas de planificación estática: trabajan sobre el código de la aplicación para conseguir eliminar todos los obstáculos que impiden que las instrucciones se ejecuten lo antes posible: desenrollamiento de bucles, reordenamiento de instrucciones... • Técnicas de planificación dinámica: se aplican sobre el diseño del hardware para que tengan lugar en tiempo de ejecución: Ejecución Fuera de Orden (OOO). DLP: consiste en la realización de la misma operación simultáneamente sobre un conjunto de datos. Para explotar esta técnica de paralelismo, es necesario que el programa tenga 3 4 CAPÍTULO 2. ESTADO DEL ARTE secciones de código que se puedan adaptar a la aplicación de este concepto. Además, la arquitectura tiene que proveer de instrucciones especiales, denominadas SIMD, del ingles Simple Instruction Multiple Data, y de recursos suficientes, como por ejemplo los registros vectoriales, los cuales puedan contener más de un dato. La estructura de ejecución por antonomasia sobre la que se pone en práctica este mecanismo, es el bucle. Las estructuras de datos análogas son los vectores y matrices. TLP: el concepto radica en la descomposición de la ejecución del programa en diferentes trazas con instrucciones independientes para ejecutarlas de forma concurrente. Un ejemplo es la Tecnologı́a Multihilos, SMT, del inglés Simultaneous MultiThreading. Consiste en ejecutar instrucciones de diferentes hilos independientes en el mismo ciclo de reloj. En el mercado existen una gran variedad de arquitecturas que implementan recursos para explotar cualquiera de las fuentes de paralelismo arriba mencionadas. Por ejemplo, la arquitectura ARM con su extensión SIMD Avanzada, también conocida como NEON o MPE, del inglés Media Processing Engine, para explotar el paralelismo de instrucciones; Nvidia y su plataforma CUDA, del inglés Computed Unified Device Architecture, junto con el set de instrucciones PTX, del inglés Parallel Thread Execution[nvi], que define una máquina virtual y un ISA con el objetivo de R explotar la GPU como máquina de ejecución de hilos paralelos de propósito general; Intely su R MIC, del inglés Many Integrated Core, sobre la que han desarrollado varios arquitectura Intel R Xeon PhiTM Coprocessor, lanzado al mercado en productos, siendo el último de ellos el Intel Noviembre de 2012 y descrito en la Sección 2.3 (pág. 8), que explota tanto el paralelismo de instrucciones como de tareas. 2.1. Taxonomı́a de Flynn La taxonomı́a de Flynn consiste en una clasificación de arquitecturas paralelas desarrollada por Michael J. Flynn en 1966 y expandida en 1972[Fly72]. Desde el punto de vista del programador en lenguaje ensamblador, las arquitecturas paralelas estarı́an clasificadas según la concurrencia del procesamiento de secuencias, datos e instrucciones. Esto da como resultado una metodologı́a de clasificación de las distintas operaciones paralelas disponibles en el procesador. Propuesta como una aproximación que clarificara los tipos de paralelismo soportados tanto a nivel hardware como software, en ella se definen las siguientes cuatro arquitecturas[fly]: Single Instruction Single Data (SISD): arquitectura secuencial que no explota el paralelismo ni a nivel de instrucciones ni a nivel de datos. Las máquinas tradicionales de un único procesador secuencial o antiguos mainframes entrarı́an en esta categorización. Ver Figura 2.1 (pág. 5). Single Instruction Multiple Data (SIMD): arquitectura que explota el paralelismo durante la ejecución de una única instrucción para realizar operaciones de naturaleza paralela. Claros ejemplos son los procesadores vectoriales o las GPU. Ver Figura 2.2 (pág. 5). Multiple instruction Single Data (MISD): múltiples instrucciones operan sobre un único stream de datos. Es una arquitectura poco común generalmente usada para tolerancia de fallos, esto es, varios sistemas operan en el mismo stream de datos y obtienen un resultado que debe ser concorde para todos ellos. Ver Figura 2.3 (pág. 5). Multiple instruction Multiple Data (MIMD): múltiples procesadores executan simultáneamente diferentes instrucciones sobre diferentes datos. Las arquitecturas VLIW son un claro ejemplo aparte de sistemas distribuidos o procesadores multicore. Ver Figura 2.4 (pág. 5). 2.2. VECTORIZACIÓN Figura 2.1: SISD 2.2. 5 Figura 2.2: SIMD Figura 2.3: MISD Figura 2.4: MIMD Vectorización La vectorización es un proceso de explotación de paralelismo de datos consistente en convertir un algoritmo de implementación escalar a vectorial. La implementación escalar es aquella en la que se realiza una única operación simultánea sobre un par de operandos que contienen un único dato cada uno. La implementación vectorial realizarı́a la misma operación, pero el par de operandos pasan de contener un único dato a contener una serie de valores. Literalmente, las escalares operan sobre un escalar y las vectoriales sobre un vector. El bucle siguiente es un claro ejemplo de candidato a ser vectorizado. 1 2 for ( i =0; i < n ; i ++) c [ i ] = a [ i ] + b [ i ]; Listado 2.1: Bucle vectorizable Con el objetivo de poner en práctica esta técnica, es necesario que la arquitectura sobre la que se va a ejecutar el programa disponga de un repertorio de instrucciones especı́fico, de una unidad vectorial y de registros vectoriales que puedan contener una serie de datos[LPG13] [SMP11] [Pip12] [SLA05]. Este repertorio de instrucciones suele ser una extensión sobre las que ya conformen la ISA con la que se trabaje, denominadas instrucciones SIMD. SIMD también sirve para indicar el tipo de la máquina. La unidad vectorial contendrá las unidades funcionales necesarias para operar sobre los registros vectoriales. Finalmente, para generar el código vectorizado, hace falta utilizar un compilador que sea capaz de reconocer secciones de código potencialmente vectoriales para generar las instrucciones SIMD que correspondan. 2.2.1. SIMD SIMD, como se describió previamente, es un término definido dentro de la taxonomı́a de Flynn para caracterizar aquellas arquitecturas que explotan el Paralelismo de Datos (DLP). Son arquitecturas que contienen múltiples elementos de procesamiento que permiten realizar la misma operación sobre varios datos simultáneamente. Esta forma de paralelismo se diferencia de la concurrencia en que es en el mismo momento, y no en momentos diferentes, cuando se realiza de forma simultánea la misma operación sobre el conjunto de datos que corresponda. Estas instrucciones se denominan comúnmente como instrucciones SIMD. Un procesador vectorial, por tanto, es un procesador que puede operar en todos los elementos de un vector dado con una única instrucción. Esto supone una reducción importante en el número de instrucciones leı́das, decodificadas y ejecutadas para un mismo programa vectorizado, frente a otro que no haya sido compilado o escrito usando este recurso. El vector sobre el que se opere, puede a su vez ser leı́do usando diferentes técnicas que diferencian a unos procesadores vectoriales de otros. En el caso de ser una arquitectura vectorial memoria-memoria, los operandos son inyectados a la 6 CAPÍTULO 2. ESTADO DEL ARTE unidad vectorial directamente de memoria, siendo el resultado devuelto a la misma a posteriori. En el caso de las arquitecturas vectoriales vector-registro, los operandos son depositados en registros vectoriales que alimentaran a la unidad vectorial, siendo asimismo el resultado depositado en otro registro vectorial[MS03]. Independientemente de sendos modus operandi descritos, la mejora en la reducción del número de instrucciones leı́das y decodificadas resulta no ser tal si al final la instrucción va a tener que esperar a que se lea el vector o porción del vector de memoria. Por esto, existen diversos esquemas de optimización del rendimiento centrados en reducir el tiempo de acceso a la memoria: Hardware y software prefetching: prefetching en términos generales significa traer datos o instrucciones de memoria antes de que se necesiten. Cuando la aplicación necesita datos que se han traı́do con el prefetching, puede tomarlos directamente en vez de tener que esperar por la memoria. Esta técnica puede ser iniciada tanto desde el hardware como desde el software. Gather-scatter: estas instrucciones permiten un tipo de direccionamiento de memoria propio del tratamiento de vectores. El gather se encargarı́a de indexar la lectura del vector mientras que el scatter se encargarı́a de la escritura. En su funcionamiento intervienen máscaras que indicarı́an los elementos del vector sobre los que se realizarı́a la operación. Estas podrı́an ser útiles en caso de haber condiciones en el interior del bucle para acceder a datos de forma dispersa. Stripmining: técnica que afronta un problema en la vectorización consistente en que los registros vectoriales no tienen por qué ser capaces de contener un vector completo definido en la aplicación. Consistirı́a en romper el bucle de la aplicación que opera sobre el vector en diferentes bucles: prólogo, principal y epı́logo según conviniera, para tratar un número de datos <= M V L, donde MVL es la Longitud Máxima de Vector del procesador. Bancos de memoria: permiten realizar varios load y stores simultáneamente. Los datos tratados cada vez no tendrı́an por qué ser necesariamente secuenciales. Repertorio de instrucciones SIMD El primer uso de instrucciones SIMD fue en los supercomputadores vectoriales a principios de los 70. Sin embargo, el modo de funcionamiento de entonces era ligeramente distinto al concepto actual. Anteriormente la instrucción que operaba sobre el vector lo hacı́a sobre un pipeline de procesadores, cada uno de los cuales operaba únicamente sobre una palabra del vector. En el concepto moderno, es un procesador el que realiza la operación simultáneamente sobre el vector completo o una porción del mismo. A lo largo del tiempo, esta tecnologı́a salto a las máquinas sobremesa, mercado en el que tuvo gran repercusión y posterior demanda debido a que podı́a soportar aplicaciones tales como procesamiento de vı́deo y juegos en tiempo real. Es por ello que a lo largo de la historia ha habido gran variedad de repertorios SIMD fruto de la competencia entre diferentes compañı́as del momento. Hoy en dı́a no hay computadora de uso general que no haga uso de una arquitectura que explote el paralelismo de datos a través de la vectorización. A continuación se nombran algunos de los diferentes repertorios de instrucciones SIMD de la historia: VIS: Visual Instruction Set. Repertorio desarrollado y lanzado al mercado en 1994 por Sun Microsystems, adquirida por Oracle Corporation en 2010. La primera version fue implementada en el UltraSPARC en 1995 y por Fujitsu en el SPARC64 GP. Registros de 8, 16 y 32 bits. Hubieron varias versiones posteriores esta: VIS2, VIS2+ y VIS3. R y lanzado en 1997 en su gama Pentium MMX. MMX: repertorio desarrollado por Intel Registros de 64 bits. 2.2. VECTORIZACIÓN 7 3DNow!: extensión al repertorio de la arquitectura x86 desarrollado por AMD. Registros 32 bits para operaciones de punto flotante de simple precisión. El objetivo era mejorar el ya existente repertorio MMX de Intel de cara a elevar el rendimiento de las aplicaciones gráficas. En 2010 AMD anunció el fin del mantenimiento y soporte del mismo. SSE: Streaming SIMD Extensions. Extensión de la arquitectura x86 diseñado por Intel y lanzado al mercado en 1999 en su serie Pentium III, como respuesta al lanzamiento por parte de AMD de su extensión 3DNow. Registros de 128 bits. AltiVec: repertorio diseñado por y propiedad de la siguientes empreas: Apple, aquı́ recibe el nombre de Velocity Engine; IBM, donde se denomina VMX; Freescale Semiconductor, propietaria de la marca registrada AltiVec. Registros de 128 bits. AVX: Advanced Vector Extensions. Extensión al repertorio de la arquitectura x86 destinada a procesadores Intel y AMD. Registros de 256 bits. Su desarrollo fue propuesto por Intel en 2008 pero no fue hasta tres años después cuando salió al mercado como caracterı́stica de la generación de procesadores Sandy Bridge de Intel y Bulldozer de AMD. Posterior a ella tenemos otra extension denominada AVX2 soportada por Haswell, Broadwell, Skylake y Cannonlake de Intel y por Excavator de AMD. En Julio de 2013 Intel anunció AVX-512, última extensión con registros de 512 bits. Ventajas Los procesadores vectoriales y por extensión el uso de la vectorización, proporcionan las siguientes ventajas: Los programas son de menor tamaño al reducir el número de instrucciones que pudiera requerir un bucle. Ademas, el número de instrucciones ejecutadas también se ve reducido al poder concentrar un bucle en una única instrucción. El rendimiento de la aplicación mejora. Se tienen N operaciones independientes que utilizan la misma unidad funcional, explotando al máximo la localidad espacial de la memoria cache. Las arquitecturas son escalables: se obtiene un mayor rendimiento cuantos más recursos hardware haya disponibles. El consumo de energı́a se reduce. Mientras la instrucción vectorial se está ejecutando, no es necesario alimentar otras unidades tales como el ROB o el decodificador de instrucciones. Tomando como ejemplo una aplicación multimedia que cambia el brillo a una imagen, la vectorización proporciona dos mejoras claras: en primer lugar el conjunto de datos se entiende como un vector y no como valores individuales. Esto permitirá cargar en un registro todos aquellos datos que éste pueda albergar, en vez de convertir el programa en una retahı́la carga-opera-guarda sobre una gran cantidad de pı́xeles individuales. En segundo lugar, la paralelización del trabajo en una única instrucción es evidente. Cuanto más datos pueda albergar, mayor será el rendimiento. Inconvenientes Los procesadores vectoriales comparados con multiprocesadores o procesadores superescalares podrı́an resultar menos interesantes si lo miramos desde el punto de vista del coste: Necesitan memoria on-chip rápida y por consiguiente cara. 8 CAPÍTULO 2. ESTADO DEL ARTE Hay que diseñarlos de forma especı́fica. La unidad vectorial de los procesadores vectoriales no se compone de elementos prefabricados más allá de las unidades básicas, por tanto pocas ventas del producto final se traducirı́a en pérdidas debido a su coste de diseño y validación. Mientras que se conseguı́a una ventaja en el consumo de energı́a al reducir la alimentación de otras unidades, el consumo por parte de los registros vectoriales podrı́a no mejorar el balance final de consumo. Los compiladores, al igual que pueden facilitar la tarea, pueden dificultarla en caso de que la vectorización del código no se adapte a los requerimientos esperados por el compilador. Existe entonces una posibilidad real de que haya una importante implicación del ingeniero tanto a nivel alto como bajo para conseguir vectorizar una aplicación. No hay un estándar establecido en el proceso. La utilización de un repertorio de instrucciones SIMD u otro puede dificultar la tarea en caso de compilar la misma aplicación sobre distintas arquitecturas. Aparte, es posible que el ingeniero tenga que proveer de una implementación no vectorial de la misma. No todas las aplicaciones se pueden vectorizar. Por ejemplo, las aplicaciones de análisis de código, caracterizadas por su fuerte control del flujo de ejecución, son claras candidatas para no beneficiarse de las ventajas de la vectorización. 2.3. R Xeon PhiTM Coprocessor Intel R Xeon PhiTM es el primer producto basado en la arquitectura Intel R El coprocesador Intel R MIC que se ha comercializado. Se lanzó al mercado en Noviembre de 2012. La arquitectura Intel R dentro de un único chip. Esta desMIC se basa en la combinación de muchos núcleos de Intel tinada a su uso en la Computación de Alto Rendimiento o HPC, del inglés High Performance Computing, para la ejecución de programas paralelos que se venı́an ejecutando en grandes clústeres1 . Pese a que su objetivo no es sustituir los sistemas ya existentes, es una interesante alternativa para conseguir buenos resultados de rendimiento de throughput 2 , en entornos donde no haya demasiado espacio para la instalación de múltiples clústeres y donde se impongan limitaciones de consumo de energı́a. Ademas, un punto clave de la microarquitectura es que esta construida especialmente para proporcionar un entorno de programación similar al entorno de programación del R XeonTM [intb]. procesador Intel R Xeon PhiTM puede pasar por un sistema en sı́ mismo, puesto que corre El coprocesador Intel una distribución completa del sistema operativo Linux, soporta el modelo x86 de ordenamiento de memoria y el estándar IEEE 754 de aritmética en punto flotante. Ademas es capaz de ejecutar aplicaciones escritas en lenguajes de programación propios de la industria del HPC como es el caso de Fortran, C y C++. Esto permite proporcionar con el producto un rico entorno de desarrollo que incluye compiladores, numerosas librerı́as de apoyo (siendo de especial importancia aquellas con soporte multi-thread y operaciones matemáticas para HPC) y herramientas de caracterización y depurado. R Xeon, denominado ”host”, a través de un bus PIC Está conectado a un procesador Intel Express. Véase Figura 2.6 (pág. 9). Dado que el coprocesador ejecuta de forma autónoma el sistema operativo Linux, es posible virtualizar una comunicación tcp/ip entre éste y el procesador, permitiendo al usuario acceder como si fuera un nodo más en la red. Por tanto, cualquier usuario puede conectarse al mismo a través de una sesión ssh (secure shell) y ejecutar sus aplicaciones. Además, soporta aplicaciones heterogéneas en las que una parte de la misma se ejecutarı́a en el host y otra en la propia tarjeta. Ni qué decir tiene que se pueden conectar más de un Xeon 1 Se aplica a los conjuntos de computadoras construidos mediante la utilización de elementos hardware comunes y que se comportan como si fuesen una única computadora 2 Cantidad de trabajo que un ordenador puede hacer en un periodo de tiempo determinado R XEON PHITM COPROCESSOR 2.3. INTEL 9 R Xeon PhiTM Figura 2.5: Intel Phi en un mismo sistema, pudiéndose establecer entre ellos la comunicación ya sea a través de la interconexión p2p (peer to peer) o a través de la tarjeta de red del sistema, sin intervención en ambos casos del host. Figura 2.6: Esquema general 2.3.1. Microarquitectura R Xeon Phi esta formado por más de 50 núcleos de procesamiento, El coprocesador Intel memorias cache, controladores de memoria, lógica de cliente PCIe y un anillo de interconexión bidireccional que proporciona un elevado ancho de banda al sistema. Véase Figura 2.7 (pág. 10). La ejecución es en orden mientras que la terminación es en desorden. Cada core consta de una L2 privada que mantiene completamente la coherencia con el resto gracias a un directorio de etiquetas distribuido denominado TD, del inglés Tag Directory. Los controladores de memoria y la lógica de cliente PCIe proporcionan una interfaz directa con la memoria GDDR5 del coprocesador y el bus PCIe respectivamente. Ademas, cada core fue diseñado para minimizar el uso de energı́a a la vez que maximiza el throughput en programas altamente paralelos. Usan un pipeline en orden y soportan hasta 4 hilos hardware. 10 CAPÍTULO 2. ESTADO DEL ARTE Figura 2.7: Microarquitectura VPU R Xeon PhiTM es la VPU. Un importante componente de cada núcleo del coprocesador Intel Véase Figura 2.8 (pág. 10). La VPU cuenta con un repertorio de instrucciones SIMD de 512 bits, R Initial Many Core Instructions (Intel R IMCI). Por ello puede oficialmente conocido como Intel ejecutar 16 operaciones de simple precisión (SP) u 8 de doble precisión (DP) por ciclo. También soporta instrucciones Fused Multiply-Add (FMA), que ordenan multiplicar y sumar en la misma instrucción, y gracias a las cuales se pueden ejecutar 32 instrucciones de simple precisión o 16 de punto flotante por ciclo. Ni qué decir tiene que proporciona soporte para operaciones con enteros. Figura 2.8: Vector Processing Unit Las unidades vectoriales proporcionan una evidente mejora energética en la ejecución de aplicaciones HPC, ya que una única operación codifica una gran cantidad de trabajo, a la vez que no incurre en el coste adicional de energı́a que supondrı́an las etapas de fetch, decode y retire para la ejecución de múltiples instrucciones. Sin embargo, hicieron falta varias mejoras para lograr soportar instrucciones SIMD de estas caracterı́sticas. Por ejemplo, se añadió el uso de máscaras a la VPU para permitir predecir sobre qué datos operar dentro de un registro vectorial. Esto ayudó en la vectorización de bucles con flujos de ejecución condicionales, mejorando ası́ la eficiencia software del pipeline. La VPU tambien soporta instrucciones de tipo gather y scatter directamente a través del hardware. De este modo, para aquellos códigos con patrones de acceso a memoria esporádicos e irregulares, el uso de este tipo de instrucciones ayuda a mantener el código vectorizado. Finalmente, la VPU también cuenta con una EMU, del inglés Extended Math Unit, que puede ejecutar instrucciones trascendentes como son las recı́procas, raı́ces cuadradas y logarı́tmicas. La EMU funciona calculando aproximaciones polinómicas de estas funciones. R XEON PHITM COPROCESSOR 2.3. INTEL 11 Interconexión La interconexión se implementa como un anillo bidireccional. Véase Figura 2.9 (pág. 11). Cada dirección está compuesta de tres anillos independientes. El primero, que se corresponde con el más ancho y caro de los tres, es el anillo de datos. Este es de 64 bytes para soportar el requisito de gran ancho de banda debido a la gran cantidad de cores presentes. El anillo de direcciones es más estrecho y se utiliza para enviar comandos de lectura/escritura y direcciones de memoria. Por último, el anillo más estrecho y barato es el anillo de reconocimiento, que envı́a mensajes de control de flujo y coherencia. Figura 2.9: Interconexion Figura 2.10: Directorio de etiquetas Cuando un core accede a su cache L2 y falla, una solicitud de dirección se envı́a sobre el anillo de direcciones a los directorios de etiquetas. Véase Figura 2.10 (pág. 11). Las direcciones de memoria se distribuyen de manera uniforme entre los distintos directorios que hay en el anillo para añadir la fluidez de tráfico como una caracterı́stica más del mismo. Si el bloque de datos solicitado se encuentra en la cache L2 de otro core, se dirige una petición a la L2 de ese core sobre el anillo de direcciones. Finalmente, el bloque de solicitud es posteriormente reenviado sobre el anillo de datos. Si los datos solicitados no se encuentran en ninguna de las caches, se envı́a la dirección de memoria desde el directorio de etiqueta hasta el controlador de memoria. La Figura 2.11 (pág. 12) muestra la distribución de los controladores de memoria en el anillo. Como se aprecia, se intercalan de forma simétrica alrededor del él. La asignación de los directorios de etiquetas a los controladores de memoria se realiza de forma todos-a-todos. Las direcciones se distribuyen uniformemente a través de todos los controladores, eliminando de este modo los hotspots y proporcionando un patrón de acceso uniforme esencial para un uso efectivo del ancho de banda. Volviendo al modo de funcionamiento, durante un acceso de memoria, cada vez que se produce un error en el nivel L2 de cache en un core, éste genera una petición de dirección en el anillo 12 CAPÍTULO 2. ESTADO DEL ARTE Figura 2.11: Controladores de memoria de direcciones y consulta a los directorios de etiquetas. Si los datos no se encuentran en estos directorios, el core genera otra solicitud de dirección y solicita los datos a la memoria. Una vez que el controlador recibe el bloque de datos desde la memoria, se entrega al core a través del anillo de datos. En todo el proceso los elementos trasmitidos a los anillos son: un bloque de datos, dos solicitudes de dirección junto con dos mensajes de confirmación. Debido a que los anillos de datos son los más caros y están diseñados para soportar el ancho de banda requerido, es necesario incrementar el número de anillos de dirección y reconocimiento, más baratos en comparación, en un factor de dos para soportar las necesidades de ancho de banda causadas por el elevado número de peticiones sobre los anillos. Caches R MIC invierte en mayor medida tanto en caches L1 como L2 en compaLa arquitectura Intel R Xeon PhiTM implementa un subsistema ración con las arquitecturas GPU. El coprocesador Intel de memoria en el que cada core está equipado con una cache de instrucciones L1 de 32KB, una cache de datos L1 de 32KB y una cache L2 unificada de 512KB. Son totalmente coherentes e implementan el modelo de orden de memoria x86. Las caches L1 y L2 proporcionan un ancho de banda agregado que es entre 15 y 7 veces, respectivamente, más rápido que el ancho de banda de la memoria principal. Por lo tanto, el uso efectivo de esta jerarquı́a es clave para lograr el máximo rendimiento en el coprocesador. Además de mejorar el ancho de banda, son también más eficientes que la memoria principal en cuanto al uso de energı́a para el suministro de datos al core. En la era de la computación exascale 3 , las caches jugarán un papel crucial a la hora de conseguir maximizar el rendimiento bajo estrictas restricciones de potencia. R Imágenes cortesı́a de Intel. 3 La computacion exascale se refiere a los sistemas de computacion capaces de alcanzar un exaFLOPS. R ADVANCED VECTOR EXTENSIONS 2.4. INTEL 2.4. 13 R Advanced Vector Extensions Intel AVX, del inglés Advanced Vector Extensions engloba, como veı́amos en la Sección 2.2.1 (pág. 6), el conjunto de extensiones sobre la arquitectura del repertorio de instrucciones x86, propuestas por primera vez por Intel en Marzo de 2008 tanto para procesadores de Intel como de AMD. El primer producto en soportarlo fue el procesador Sandy Bridge de Intel en el primer cuarto de 2011, seguido por el procesador Bulldozer de AMD en el tercer cuarto del mismo año. 2.4.1. R Advanced Vector Extensions 1 Intel R Advanced Vector Extensions 1 (AVX) mejoraban las extensiones SSE Las extensiones Intel mediante el incremento del ancho del banco de registros SIMD de 128 bits a 256 bits. El nombre de los registros, XMM0-XMM7, se cambió en consecuencia de YMM0-YMM7 (en el caso de x8664, YMM0-YMM15). Sin embargo, en los procesadores con soporte AVX, las instrucciones de la extensión SSE podı́an ser usadas para operar en los 128 bits menos significativos de los registros YMM. Entonces podı́a seguir usándose la nomenclatura XMM0-XMM7. AVX introdujo además un formato de instrucción SIMD de tres operandos donde el registro de destino podı́a ser distinto a los dos registros fuente. Por ejemplo, una instrucción SSE usando la forma convencional a = a + b, podı́a ahora utilizar el método de tres operandos c = a + b, impidiendo que se destruyera la información almacenada en alguno de ellos como ocurrı́a hasta el momento. Este formato estaba limitado a las instrucciones que utilizan los registros YMM, no incluyendo por tanto instrucciones con registros de propósito general (por ejemplo EAX). 2.4.2. R Advanced Vector Extensions 2 Intel R Advanced Vector Extensions 2 (AVX2) mejoraban el set de extensiones Las extensiones Intel R Haswell. La compañı́a AVX, y fueron introducidas por primera vez en la microarquitectura Intel amplió por tanto el juego AVX con nuevas instrucciones que funcionaban también sobre números naturales, ampliando casi la totalidad del conjunto SSE de 128 bits a 256 bits. El formato no destructivo de tres operandos estuvo ahora también disponible para instrucciones a nivel de bits y multiplicación de propósito general y para instrucciones FMA (Fused Multiply-Accumulate). Finalmente, esta nueva ampliación permitió realizar instrucciones gather, lo que significarı́a la posibilidad de acceder a la vez a varias posiciones no contiguas en memoria, aumentando considerablemente las capacidades de procesado vectorial de la arquitectura x86-64. 2.4.3. R Advanced Vector Extensions 512 Intel R Advanced Vector Extensions 512, AVX-512, son las extensiones a 512 bits de las insIntel trucciones SIMD recogidas en las Advanced Vector Extensions de 256 bits. Fueron propuestas R Xeon PhiTM denominado por Intel en Julio de 2013 para ser incluidas en el coprocesador Intel Knights Landing que se espera lanzar al mercado en el año 2015[inta]. No todas las extensiones están destinadas a ser soportadas por todos los procesadores que las implementen. Sólo la extensión del núcleo AVX-512F (AVX-512 Foundation) se requiere para todas las implementaciones. Atendiendo al repertorio de instrucciones y a las principales caracterı́sticas de AVX-512, las extensiones se clasifican del siguiente modo: AVX-512 Foundation: expande la mayorı́a de instrucciones AVX de 32 y 64 bits con el esquema de codificación EVEX para soportar los registros de 512 bits, las operaciones con 14 CAPÍTULO 2. ESTADO DEL ARTE máscaras, la difusión de parámetros y las excepciones de control y redondeo empotradas. AVX-512 Conflict Detection Instructions (CDI): añade detección de conflictos eficiente para permitir que más bucles puedan ser vectorizados. AVX-512 Exponential and Reciprocal Instructions (ERI): operaciones exponenciales y recı́procas diseñadas para ayudar en la implementación de operaciones trascendentes, como por ejemplo la función de logaritmo. AVX-512 Prefetch Instructions (PFI): soporte para prefetches. En cuanto a las caracterı́sticas técnicas, se resumen en los siguientes puntos: 32 registros vectoriales de 512 bits de ancho bajo la nomenclatura ZMM0-ZMM31. 8 registros dedicados a las máscaras, lo cual es de especial trascendencia para las instrucciones gather y scatter. Operaciones de 512 bits sobre datos empaquetados enteros y de punto flotante. Los programas podrán entonces empaquetar en los nuevos registros de 512 bits cualquiera de las siguientes combinaciones de datos: 8 datos en punto flotante de precisión doble, o 16 datos en punto flotante de precisión simple, u 8 enteros de 64 bits o 16 enteros de 32 bits. Esto permitirá el procesamiento del doble de elementos que el AVX/AVX2 con una sola instrucción y cuatro veces el de SSE. R AVX-512 ofrece un nivel de compatibilidad con AVX Es interesante resaltar que Intel muchı́simo mayor que las transiciones anteriores sobre el ancho de las operaciones. A diferencia de lo que ocurre con SSE y AVX, que no se pueden mezclar sin penalizaciones en el rendimiento, la mezcla de instrucciones AVX y AVX-512 es posible sin penalización alguna. Los registros YMM0YMM15 de AVX se mapean en los registros ZMM0–ZMM15 de AVX-512 del mismo modo que se mapeaban los registros SSE sobre AVX. Por lo tanto, en procesadores que soporten AVX-512, las instrucciones AVX y AVX2 operarán en los 128 o 256 bits inferiores de los primeros 16 registros ZMM. Capı́tulo 3 Metodologı́a Las ideas que surgieron a la hora de definir el anteproyecto del presente Proyecto Final de Carrera lo describı́an claramente como un trabajo con una fuerte carga de análisis. Comprendı́a desde el análisis de todas y cada una de las herramientas a utilizar, hasta el análisis de cada resultado, cada gráfica elaborada, cada bloque básico implicado y cada lı́nea de código que supusiera un objeto de interés sobre el que adentrarse. Por este motivo no se aplicó una estrategia especı́ficamente etiquetada que seria propia de un trabajo vinculado a la rama de ingenierı́a del software. Ciertamente, parte de este trabajo consistı́a en desarrollar un simulador sobre el que ejecutar las aplicaciones, con el objetivo de obtener más estadı́sticas aparte de aquellas conseguidas gracias a R Sin embargo, incluso el tomar la las herramientas ya disponibles para los ingenieros de Intel. decisión de incorporar el desarrollo de este simulador como una extensión de otro ya existente, supuso un fuerte trabajo de análisis para tratar de reciclar la mayor cantidad de información ya disponible. Esta información se encontraba almacenada, en su mayor parte, sobre una gran variedad de estructuras y clases que evitaron, ya no solo no reinventar la rueda, sino también impedir sobrecargar al simulador con operaciones y tareas que eran necesarias y que por supuesto ya se estaban realizando. En este capı́tulo, se describirá la forma de trabajo, es decir aquellas actividades que supusieron una importante parte para la consecución del trabajo, cómo se distribuyeron todas las tareas a realizar y el tipo de metodologı́a usada cuando se procedió al desarrollo de la nueva extensión del simulador. 3.1. Plan de trabajo Al plan de trabajo diseñado inicialmente y presentado en el anteproyecto se le aplicaron modificaciones sin que ello repercutiese en el computo total de horas. A continuación se presenta la planificación final y las justificaciones sobre los cambios en el caso de haberlos. Fase 1: Selección y caracterización de benchmarks 1. Selección del conjunto de benchmarks sobre los que realizar el estudio a partir de los disponibles, como NPB, Polyhedron, PARSEC, etc. 2. Compilar las aplicaciones de los benchmarks para la arquitectura x86 con la extensión AVX512 para la obtención de una caracterización inicial. Utilizando los compiladores disponibles, 15 16 CAPÍTULO 3. METODOLOGÍA como ICC e IFORT, se realiza la compilación del conjunto de aplicaciones seleccionadas utilizando aquellas opciones que permitan realizar optimizaciones y vectorización del código. Implica un pequeño análisis mediante el parsing del informe generado, para tener una aproximación inicial al comportamiento de cada aplicación. Esta fase se redujo a la selección y posterior caracterización de las aplicaciones. El motivo por el que no se realizó una criba inicial radica en que al principio, sin más datos que los disponibles estáticamente con el informe del compilador, la mera descripción de la aplicación no parecı́a suficiente para descartar unas u otras. Era más interesante quedarnos con todas las disponibles y, a partir de diferentes estadı́sticas, tomar decisiones sobre cuáles analizar según las soluciones software o hardware a aplicar. Fase 2: Recopilación y análisis de información sobre la ejecución vectorial de los programas de prueba 1. Determinación del grado de vectorización de los programas: Usando el emulador Pin/SDE, se obtiene el número de instrucciones ejecutado por cada programa y se determina el grado de vectorización de los mismos. 2. Recopilación de información sobre la jerarquı́a de memoria: Usando el emulador CMP$im, se obtiene la tasa de fallos de los diferentes niveles de la jerarquı́a de memoria para cada uno de los programas de prueba. 3. Determinación del grado de utilización de la unidad vectorial: En este apartado se desaR Xeon PhiTM rrollará un núcleo sencillo basado en la arquitectura del coprocesador Intel sobre el simulador CMP$im, que permita obtener datos estadı́sticos para ası́ determinar el grado de utilización de la unidad vectorial. La única modificación planteada en esta fase consistió en realizar el desarrollo del simulador de un core con arquitectura vectorial embebido dentro del simulador de cache CMP$im. El motivo radica en que el simulador de cache proporciona mucha información de interés que puede utilizarse para la simulación de la arquitectura. Además, contiene multitud de estructuras y clases que se pueden utilizar a la vez que se introduce el código sobre el esqueleto correspondiente a la simulación de cache. En primer lugar se llevarı́a a cabo una exhaustiva fase de análisis sobre CMP$im, para conocer en profundidad tanto la configuración de ficheros del simulador, como las estructuras y clases usados, aparte del funcionamiento especı́fico de la simulación de la cache. Los objetivos principales eran reutilizar estructuras, esquemas y clases ya presentes, saber de qué modo introducir el código correspondiente al simulador de la arquitectura para no entorpecer la simulación ya hecha y poder hacer uso de las estadı́sticas recopiladas. Una vez conocidas las caracterı́sticas principales mencionadas, habı́a que proceder a la fase de desarrollo de la extensión correspondiente a la simulación de la arquitectura vectorial. La metodologı́a más apropiada para desarrollarlo serı́a incremental. Era preciso en todo momento que el simulador fuese funcional. Por ello, cada vez que se incorporase una nueva funcionalidad, esta debı́a protegerse con las macros correspondientes. Ademas se tenı́a que comprobar que todo seguı́a funcionando correctamente antes de proceder a la incorporación del siguiente incremento. Cada una de las funcionalidades se iba a discutir y diseñar en reuniones semanales, de manera que al final de cada semana se tendrı́an tanto los progresos obtenidos como las dificultades encontradas. Si todo estaba correcto, se tomaban las decisiones oportunas sobre las siguientes funcionalidades a desarrollar. 3.1. PLAN DE TRABAJO 17 Fase 3: Determinación de cuellos de botella en la ejecución vectorial y propuesta de soluciones 1. Selección de un subconjunto de aplicaciones numéricas de entre todos los benchmarks seleccionados, teniendo como referencia las estadı́sticas recolectadas. Para la selección se tomaron como criterio tanto la relación entre las versiones escalar y vectorial, ası́ como el desglose de ciclos de la aplicación según las dependencias ocasionadas durante la ejecución. 2. Con la información recolectada en los puntos anteriores se determinará cuáles son las regiones del código que tienen un bajo uso de la unidad vectorial. Se estudiará a posteriori cuál es el motivo: si se trata de una falta de vectorización, si se produce por lı́mites en la microarquitecura u otros. 3. Adicionalmente, se propondrán mejoras hardware y/o software encaminadas a aumentar el rendimiento de estas regiones con bajo uso de la unidad vectorial. En esta fase, se incluyó la selección de las aplicaciones, ya que a estas alturas están completamente caracterizadas, de manera que la información disponible permite tomar decisiones basándonos exclusivamente en el comportamiento de las mismas. Capı́tulo 4 Herramientas En este capı́tulo se introducen las caracterı́sticas principales de las herramientas más significativas usadas a lo largo del proyecto. 4.1. Pin R destinada a Pin1 es una herramienta de código abierto desarrollada por la empresa Intel, la instrumentación de aplicaciones [pin]. Se denomina herramienta de Instrumentación Binaria Dinámica porque la instrumentación se realiza en tiempo de ejecución (JIT, Just in Time): No requiere que la aplicación a instrumentar se tenga que recompilar. Permite instrumentar programas que generan código dinámicamente. Se puede adherir a procesos que ya se estuvieran ejecutando. Proporciona una extensa API para escribir aplicaciones tanto en C, C++ como ensamblador, denominadas pintools, que permitirán instrumentar aplicaciones, ya sean estas single-threaded o multi-threaded, compiladas para las arquitecturas IA-32 (x86 32-bit), IA-32E (x86 64-bit) y R Dicha API permite al programador abstraerse de todas las peculiaridades procesadores Itanium. de la arquitectura para la que se haya compilado el binario. Por tanto, podrá utilizar información de contexto, tales como el contenido de los registros o las direcciones de acceso a memoria, pasándola como diferentes parámetros dentro del código que el proceso de instrumentación inserta en el binario. Además, Pin salva y recupera el contenido de los registros que se utilizaran en dicho código, de manera que no influyan en la ejecución normal del programa instrumentado. Fue utilizada a la hora de estudiar y modificar el simulador que se va a utilizar para simular las aplicaciones numéricas. 4.1.1. Pintools Una pintool, sea cual sea su funcionalidad, constará de dos secciones fundamentales en su código: 1 No es un acrónimo. 19 20 CAPÍTULO 4. HERRAMIENTAS Instrumentación: contendrá las instrucciones necesarias que indiquen a Pin qué información se quiere recoger del código que se está ejecutando, como códigos de registro, hilos en ejecución o contador de programa, entre otros. También indica en dónde se quieren insertar las llamadas a las funciones y procedimientos que harán uso de toda la información recogida. Análisis: contendrá la definición de todas las funciones y procedimientos que tratarán la información recogida en la sección de instrumentación. Además de estas dos secciones, también contendrá, como cualquier aplicación de C o C++, la función main y, como novedad, un procedimiento denominado Fini que es invocado por Pin cuando la aplicación termina. El motivo por el que se tiene que desarrollar este último procedimiento, reside en que una vez hemos dado el control a Pin, no retorna a la función main para terminar. Pin proporciona una amplia baterı́a de pintools con diferentes funcionalidades. En el Listado 4.1 (pág. 20) se muestra el código de una pintool denominada inscount0 que cuenta el número de instrucciones ejecutadas de una aplicación. Se encuentra dentro de la extensa baterı́a de pintools de ejemplo que acompañan a la herramienta. Es una muestra perfecta de la estructura más común de una pintool, en donde se aprecian tanto las secciones principales descritas, como la definición de knobs, opciones de la pintool y otras funciones de interés. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # include < iostream > # include < fstream > # include " pin . H " ofstream OutFile ; static UINT64 icount = 0; // Seccion de analisis VOID docount () { icount ++; } // Seccion de in st r um en ta c io n VOID Instruction ( INS ins , VOID * v ) { INS_ InsertCa ll ( ins , IPOINT_BEFORE , ( AFUNPTR ) docount , IARG_END ) ; } KNOB < string > K nobOutp utFile ( KNOB_MODE_WRITEONCE , " pintool " ," o " , " inscount . out " , " specify output file name " ) ; VOID Fini ( INT32 code , VOID * v ) { OutFile . setf ( ios :: showbase ) ; OutFile << " Count " << icount << endl ; OutFile . close () ; } INT32 Usage () { cerr << " This tool counts the number of dynamic instructions executed " << ←endl ; cerr << endl << KNOB_BASE :: S t r i n g K n o b S u m m a r y () << endl ; return -1; } int main ( int argc , char * argv []) { if ( PIN_Init ( argc , argv ) ) return Usage () ; OutFile . open ( Knob OutputFi le . Value () . c_str () ) ; I N S _ A d d I n s t r u m e n t F u n c t i o n ( Instruction , 0) ; P I N _ A d d F i n i F u n c t i o n ( Fini , 0) ; P I N _ S t a r t P r o g r am () ; return 0; } Listado 4.1: Código de ejemplo de la pintool inscount0 4.1. PIN 21 Analizando más en detalle el Listado 4.1 se observan llamadas a funciones que forman parte de la gran API proporcionada por pin: PIN Init: inicializa Pin con los argumentos de entrada. Devuelve false si hay errores. INS AddInstrumentFunction: registra qué función se encargará de la instrumentación a nivel de instrucción. PIN AddFiniFunction: registra qué función se invocará antes de que la aplicación instrumentada termine. PIN StartProgram: arranca la ejecución de la aplicación. No se retorna. INS InsertCall: registra qué función se ha de llamar cuando se encuentren instrucciones candidatas a instrumentar. Entre los parámetros que se observan en Listado 4.2 tenemos: • ins: instrucción a instrumentar. • IPOINT BEFORE: se invocara el análisis antes de que se ejecute. • docount: función de análisis. • IARG END: fin de parámetros, tanto si los hubiera como si no. Estos parámetros son los que pasarı́an a la función docount del ejemplo. Véase Listado 4.3. 1 2 3 4 INS_ InsertCa ll ( ins , IPOINT_BEFORE , ( AFUNPTR ) docount , IARG_END ) ; Listado 4.2: Función de instrumentación 1 2 3 4 5 6 7 8 9 10 11 VOID docount ( UINT32 threadId , ADDRINT pc ) { ... } VOID Instruction ( INS ins , VOID * v ) { INS_ InsertCa ll ( ins , IPOINT_BEFORE , ( AFUNPTR ) docount , IARG_THREAD_ID , IARG_ADDRINT , INS_Address ( ins ) , IARG_END ) ; } Listado 4.3: Argumentos para la función de análisis 4.1.2. Arquitectura software En la Figura 4.1 se observa la arquitectura software de Pin [LCM+ 05]. El elemento principal es la máquina virtual, que contiene un compilador just in time que se encarga de recompilar aquellas porciones de la aplicación que se hayan indicado en la sección de instrumentación, sobre las que se inyectará el código de la sección de análisis que le corresponda. El dispatcher es el encargado de lanzar el código recién compilado que almacena en el área denominada code cache. La emulation unit interpreta aquellas instrucciones que no puedan ser ejecutadas directamente, como es el caso de las llamadas al sistema que tienen un tratamiento particular dentro de la máquina virtual. De entre las entradas que alimentan a Pin, se encuentran lógicamente tanto la pintool como la aplicación. En el Listado 4.4 se observa el esqueleto general de la invocación. 22 CAPÍTULO 4. HERRAMIENTAS Figura 4.1: Arquitectura software de Pin 1 pin [ pin_opts ] -t pintool . so [ pintool_opts ] -- app [ input ] Listado 4.4: Ejemplo de invocación de Pin 4.2. CMP$im CMP$im (Chip Multi-Processor Cache Simulator) un simulador de cache desarrollado por R Intely orientado a chips multiprocesador, cuyo objetivo es analizar el rendimiento de memoria de aplicaciones tanto single-threaded, multi-threaded como multi-program. Fue desarrollada sobre Pin, por lo que fundamentalmente es una pintool, aprovechando el perfil de Pin como herramienta de instrumentación binaria dinámica, que suponı́a una alternativa frente a otros métodos de simulación como trace-driven basado en trazas [JCLJ06][JCLJ08]. Es interesante destacar que CMP$im es una herramienta muy rápida y fácil de utilizar. Además proporciona una gran cantidad opciones, tanto estáticas de forma #define MACRO, como dinámicas (knobs) de forma -knob valor, que la hacen flexible. Por ello permite configurar al detalle el sistema de memorias cache que van a participar en la simulación. Entre estas opciones se destacan las siguientes: Número de niveles de cache. Número de caches por nivel. Número máximo de threads que se pueden lanzar con la aplicación. Polı́ticas de escritura y reemplazo. Caracterı́sticas particulares para cada cache, como tamaño, asociatividad, latencia, tamaño de lı́nea, etc. Latencia de la memoria principal, aunque no se simule. 4.3. BENCHMARKS 23 TLBs. Inclusividad o exclusividad. Una vez compilada la pintool con las macros deseadas, se lanza la simulación del mismo modo especificado en el Listado 4.4 (pág. 22). La salida se compone de un informe con estadı́sticas muy detalladas, que proporcionan tanto información general de la aplicación, como particular desglosada por hilos de ejecución y por nivel de cache. Entre ellos, se muestran datos relativos a: Número total de instrucciones ejecutadas y desglosadas por hilos. Estimación2 del número de ciclos, donde la latencia de cada instrucción es, por defecto, de un ciclo, añadiendo al total la latencia total generada por los accesos a memoria. Número de accesos, aciertos, fallos y tasas de fallo desglosadas por nivel de cache y tipo de acceso (load, store, write back, etc.). R Esta herramienta fue utilizada como base para desarrollar el núcleo del coprocesador IntelXeon R PhiTM . Imágenes cortesı́a de Intel. 4.3. Benchmarks El benchmarking es una técnica consistente en medir el rendimiento de un sistema o un componente del mismo, con el objetivo de realizar una comparativa con otro sistema similar de modo que se tenga una referencia base sobre la que trabajar. De este modo, se podrı́a saber si la máquina obtiene buenos resultados o no, de cara a utilizarlos para el fin que convenga. Aplicado en el campo de la informática consistirı́a en la ejecución de aplicaciones especı́ficamente diseñadas para medir el rendimiento de una máquina o de uno de sus componentes. Por ello, de cara a este trabajo el uso de benchmarks software constituye una piedra angular para poder determinar el grado de utilización efectiva de la unidad vectorial de un procesador. Los benchmarks se pueden clasificar en diferentes categorı́as. Veamos una posible clasificación: Benchmarks basados en el nivel del rendimiento que miden: • Benchmarks de bajo nivel o nivel componente: se encargan de medir directamente un componente especı́fico del sistema como, por ejemplo, la memoria RAM, la tarjeta gráfica o el procesador. • Benchmarks de alto nivel o nivel sistema: evalúan el rendimiento global de una máquina. Este tipo es interesante para comparar sistemas que se basan en arquitecturas distintas. Benchmarks basados en el código que los componen: • Benchmarks sintéticos: creados especı́ficamente combinando diferentes funciones del sistema a probar, en las proporciones que los desarrolladores estiman oportunas. El objetivo es conseguir medir determinados aspectos del sistema. Esta descripción se asocia rápidamente a los benchmarks de tipo bajo nivel ya que, por ejemplo, para medir el rendimiento del funcionamiento del disco, se pueden incluir funcionalidades de lectura, escritura o búsqueda de datos en disco en el benchmark. 2 No implementa ningún mecanismo que haga uso del paralelismo de instrucciones (ILP). 24 CAPÍTULO 4. HERRAMIENTAS • Benchmarks de aplicación: hacen uso de aplicaciones reales. En este caso los desarrolladores del benchmark pueden tener interés en utilizar determinadas aplicaciones que realizan funciones enfocadas a una industria concreta o a un determinado tipo de producto. En este caso, se asocian con los benchmarks de alto nivel ya que este tipo de aplicaciones miden el rendimiento global del sistema, pudiendo analizar cómo contribuye cada componente al dicho rendimiento. Independiente de esta clasificación, se pueden tipificar siguiendo otros patrones más especı́ficos. Por ejemplo, existen benchmarks que sirven para medir el rendimiento de máquinas con múltiples núcleos en su procesador o con múltiples procesadores. Existen otros benchmarks para medir la respuesta de las consultas sobre una base de datos. Existen una gran cantidad de benchmarks disponibles hoy en dı́a, entre los cuales podemos mencionar los siguientes a modo de ejemplo: Whetstone: considerado el padre de los benchmarks sintéticos, fue creado en el Laboratorio nacional de Fı́sica de Inglaterra. Su objetivo inicial era servir como test para el compilador ALGOL 60 aunque hoy en dı́a forma parte de otros benchmarks. 3DMark: benchmark sintético creado por la compañı́a Futuremark Corporation, con el objetivo de medir la capacidad de rendering sobre gráficos 3D que tiene la GPU de una máquina, ası́ como la capacidad de procesamiento de la CPU. Ciusbet: creado por Ciusbet. Es un benchmark que se compone de un gran número de pruebas para probar diferentes componentes de una máquina, como la memoria cache, la CPU, el disco duro, etc. FurmKar: benchmark sintético que mide el rendimiento de una tarjeta gráfica al través de la ejecución de un algoritmo de renderizado de pelaje. Su peculiaridad es que, al tratarse de un algoritmo que somete a la GPU a un nivel de estrés muy fuerte, permite medir muy bien la capacidad de aguante y estabilidad de la tarjeta. El conjunto de benchmarks que se presenta a continuación es una muestra continente de variedad de aplicaciones numéricas usadas comúnmente en el ámbito de la computación de alto rendimiento. Todos los programas se caracterizan por ser o bien Free software, o bien Open source. 4.3.1. Polyhedron Fortran Benchmarks Polyhedron[pol] es un paquete de 17 programas escritos en Fortran 90, diseñados para comparar el rendimiento de los diferentes ejecutables generados por distintos compiladores. Todos los programas se pueden descargar y hacer uso de ellos según convenga. El paquete que actualmente está disponible para descarga, se denomina pb11. Sin embargo, para el presente trabajo hicimos uso de 15 aplicaciones del antiguo repertorio, denominado pb05, debido a que el conjunto de datos de entrada permitı́a una mayor rapidez de cara a la simulación completa de los programas. El nuevo benchmark tiene conjuntos de datos que ralentizaban demasiado su simulación. Las 15 aplicaciones son las siguientes: ac channel induct protein armod doduc linpk test fpu air fatigue mdbx tfft capacita gas dyn nf 4.3. BENCHMARKS 4.3.2. 25 Mantevo 1.0 Mantevo[man] es un benchmark que proporciona una interesante variedad de aplicaciones clasificadas en: Miniapplications: partiendo de la idea de que la medición del rendimiento de las aplicaciones viene determinada por una combinación de diferentes opciones, proporcionan una aproximación excelente para explorarlas. Las opciones mencionadas englobarı́an las siguientes: el compilador usado, el hardware de la máquina a medir, el algoritmo, el entorno de ejecución, el uso de miniaplicaciones, definidas como pequeños proxies autocontenidos para aplicaciones reales, etc. Minidrivers, pequeñas aplicaciones que sirven para simular el funcionamiento de diferentes controladores. Application proxies, aplicaciones parametrizables cuyo objetivo es simular el comportamiento de aplicaciones a gran escala. Todas ellas realizan mayoritariamente operaciones en coma flotante para, por ejemplo, resolver ecuaciones diferenciales en derivadas parciales tanto implı́citas como explı́citas, simular modelos de dinámica molecular que implican operaciones sobre vectores, etc. Las aplicaciones de que se compone son las siguientes: CloverLeaf miniFE CoMD miniMD HPCCG-200 miniXyce miniGhost De los tres conjuntos de ficheros de entrada disponibles para realizar las simulaciones, nos hemos quedado con los mediums, con el objetivo de tener los resultados en tiempos razonables. 4.3.3. ASC Sequoia Benchmark Codes Los investigadores del Laboratorio Nacional Lawrence Livermore (LLNL), con motivo del programa ASC (Advanced Simulation and Computing) de la Administracion Nacional de Seguridad Nacional (NNSA) de Estados Unidos, llevan a cabo multitud de simulaciones sobre el supercomputador de IBM denominado Sequoia. Dentro de los recursos que están disponibles en la plataforma online dedicados a este programa y a los trabajos realizados sobre el supercomputador, se encuentra todo un interesante repertorio de aplicaciones[seq]. De entre todas ellas, y dado que solo se iba R Xeon PhiTM , se seleccionaron solamente las a simular uno de los núcleos del coprocesador Intel aplicaciones correspondientes a la sección Tier 3, que se caracterizan por ser single-threaded : UMTmk 4.3.4. IRSmk SPhotmk Crystalmk NAS Parallel Benchmarks Los NAS Parallel Benchmarks[nas] (NPB) son un conjunto de aplicaciones destinadas a la medición del rendimiento de supercomputadores paralelos. Fueron desarrolladas por la división NAS (NASA Advanced Supercomputing). Inicialmente, en la especificación NPB 1, estaba conformado por 5 kernels y 3 pseudo aplicaciones. Más adelante fue extendida para incluir nuevos benchmarks sobre mallas adaptativas, aplicaciones E/S paralelas y redes computacionales. Se utilizó la versión 3.3.1 que contiene las siguientes aplicaciones: 26 CAPÍTULO 4. HERRAMIENTAS BT IS CG LU DC MG EP SP FT UA Los diferentes inputs se categorizan en las siguientes clases: Class S : para pequeñas pruebas. Class W : destinada a estaciones de trabajo. Classes A, B, C : test de mayor tamaño, cada uno de los cuales es aproximadamente 4x superior al anterior. Classes D, E, F : en este caso son entradas muy grandes, del orden de los 16x de incremento entre un test y el siguiente. Para el presente caso, era suficiente utilizar la clase W, puesto que el número de instrucciones ejecutadas se encontraba en el orden de magnitud de los otros benchmarks seleccionados. 4.3.5. SPEC CPU 2006 La Standard Performance Evaluation Corporation (SPEC), es una organización sin ánimo de lucro cuyo objetivo es producir, establecer, mantener y promocionar un paquete estándar de benchmarks para medir el rendimiento de diferentes máquinas. En este sentido, se dispone del conjunto de benchmarks denominado SPEC CPU2006 diseñado para proporcionar una medida comparativa, con el objetivo de analizar el rendimiento conseguido después de realizar cálculos intensivos sobre una máquina. El conjunto de aplicaciones que lo conforman fueron desarrolladas basadas en aplicaciones de usuario reales. Los resultados que se obtengan serán fuertemente dependientes del procesador, la memoria y el compilador utilizado. Dado que el presente trabajo está centrado en el estudio efectivo de un procesador vectorial, nos centramos fundamentalmente en uno de los dos suites en que se divide: CFP2006, que sirve para medir el rendimiento de las operaciones en punto flotante. El otro suite, CINT2006 está enfocado a operaciones enteras. Las aplicaciones son las siguientes: 410.bwaves 435.gromacs 447.dealII 459.GemsFDTD 482.sphinx3 416.gamess 436.cactusADM 450.soplex 465.tonto 433.milc 437.leslie3d 453.povray 470.lbm 434.zeusmp 444.namdP 454.calculix 481.wrf Como datos de entrada, están disponibles los siguientes: all : común para todos los benchmarks, se usa en caso de ser necesario. ref : es el conjunto de datos real y completo. test: entrada para tests más sencillos. train: tests más grandes. Para nuestras necesidades, basta con usar test. 4.4. COMPILADORES 4.4. 27 Compiladores Los compiladores son una parte fundamental de este trabajo, puesto que son los responsables de generar el código necesario con el que se trabajara después. Si bien todos los elementos expuestos en este capı́tulo son indispensables para conseguir las sinergias que permitirán completar todos los objetivos definidos en la Sección 1.1 (pág. 2), los compiladores se alzan con la responsabilidad suprema. En el caso particular de los optimizadores de que constan, son responsables de generar un código adecuado con el que se pueda partir, trabajar y mejorar en caso necesario. Si no fuera ası́, ningún resultado tendrı́a la fiabilidad suficiente. Por estos motivos trabajamos con los compiladores R C++ Compiler e Intel R Fortran Compiler. Intel Una de las ventajas principales de trabajar con estos compiladores y de realizar este trabajo R es que se disponı́a en todo momento de la última versión de los comen la propia empresa Intel, piladores. Por tanto, este trabajo servı́a también como depurador de los cambios introducidos en cada versión. La versión utilizada en las simulaciones se corresponde con la de Mayo de 2013. Para las simulaciones realizadas durante los meses de Junio y Julio utilizamos la misma para mantener la coherencia en los resultados. Utilizar otro implicarı́a obtener mejoras que no recaerı́an sobre los cambios en configuraciones utilizadas al simular, sino en las propias mejoras del compilador. El código generado al compilar contiene las extensiones AVX-512 descritas en la Sección 2.4 (pág. 13). Asimismo, mientras los compiladores están disponibles en multitud de plataformas, en este trabajo se usó la versión para Linux. Los compiladores fueron usados a la hora de construir la caracterización de todas y cada una de las aplicaciones, y para tener los ejecutables disponibles en el momento de proceder a la simulación con la versión de CMP$immodificada. 4.4.1. ICC R ICC permite generar código sobre arquitecturas IA-32, Intel64 R R El compilador Intel e Intel MIC (Multiple Integrated Core) [intc], disponibles para los sistemas operativos Mac OS X, Linux y MicrosoftTM Windows. Contiene soporte para la vectorización de aplicaciones, pues puede generar instrucciones de los repertorios SSE (Streaming SIMD Extensions), SS2, SSE3, SSSE3 (Suplemental Streaming SIMD Extensions), SSE4, AVX(Advanced Vector Extensions) y AVX2. Las versiones internas del compilador, además, permitı́an generar código AVX-512. El vectorizador automático es un componente importante del compilador, ya que utiliza automáticamente instrucciones SIMD (Simple Instruction Multiple Data) de los repertorios de instrucciones mencionados anteriormente. Se encarga de detectar aquellas operaciones en el programa que se pueden vectorizar para explotar el procesamiento automático de las instrucciones de tipo SIMD. El usuario puede ayudar a este módulo mediante el uso de pragmas. Consúltese el conjunto de pragmas disponibles en la Sección 4.5 (pág. 29) ICC tiene multitud de knobs de compilación[intc]. Para este trabajo, el formato de las mismas se corresponde con aquel para Linux. A continuación se mostrarán las opciones más significativas usadas en este trabajo. La lı́nea de compilación tiene el formato mostrado en el Listado 4.5. La opción -no-vec que se observa en la lı́nea de compilación, se usaba para indicar al compilador que no vectorizase. Como se verá más adelante, la versión no vectorizada de las aplicaciones es de interés porque sirven de referencia para compararlas con la versión vectorizada. 1 2 3 icc -g - debug inline - debug - info -vec - report6 - ansi - alias - O3 -no - prec - div - ipo - static - xKNL [ - no - vec ] -o < ouput > < inputfiles > Listado 4.5: Lı́nea de compilación 28 CAPÍTULO 4. HERRAMIENTAS -g Produce información para depuración simbólica en el fichero objeto. -debug inline-debug-info Genera información de depuración mejorada tanto en el caso de código del que se haga inline como en el rastro generado por sucesivas llamadas a funciones. -vec-report6 Genera un log con toda la información concerniente a los bucles y bloques que han sido vectorizados, ası́ como de las razones porque otros no lo han sido. -ansi-alias Indica al compilador que compile bajo las reglas de aliasability estándares de la ISO de C. -O3 Aparte de las optimizaciones de la opción -O2, incluye prefetching, transformación de bucles y sustitución de escalares. -no-prec-div Permite optimizaciones que, a cambio de divisiones algo menos precisas que las operaciones de división en sı́ mismas, sustituye estas por multiplicaciones. Por ejemplo, en vez de A/B, se calcuları́a A*(1/B) -ipo Interprocedural Optimization. Indica al compilador que haga inline de las llamadas a funciones que se encuentran en otros ficheros. Un ejemplo serı́a en aquellas llamadas dentro de bucles. Por defecto el valor es 0, esto es que se dejara al compilador decidir si crear uno o más ficheros objetivo dependiendo de una estimación del tamaño de la aplicación. -static Impide enlazar con librerı́as compartidas. En su lugar, enlaza todas las librerı́as estáticamente. -xKNL La arquitectura para la que se está generando código es KNL, que incluye AVX-512. -no-vec Impide al compilador hacer uso del módulo de vectorización. El log resultante de aplicar esta opción no contendrı́a más información que la lı́nea de compilación usada y los ficheros compilados. R ICC Tabla 4.1: Knobs soportados por Intel 4.4.2. IFORT R al igual que ICC, permite compilar aplicaciones sobre las El compilador Fortran de Intel R 64 y IntelMIC R arquitecturas IA-32, Intel (Multiple Integrated Core) [intc], disponibles para los sistemas operativos Mac OS X, Linux y MicrosoftTM Windows. Igualmente, tiene caracterı́sticas análogas al compilador ICC, como es el soporte para la vectorización de aplicaciones, y comparte la mayorı́a de knobs disponibles. La lı́nea de compilación solo difiere en la opción -fpp, la cual sirve para indicar al compilador 4.5. PRAGMAS 29 que corra el preprocesador de Fortran sobre los ficheros fuentes antes de realizar la compilación. El resto permanecen invariantes. 1 2 3 ifort - fpp -g - debug inline - debug - info -vec - report6 - ansi - alias - O3 -no - prec - div - ipo - static - xKNL [ - no - vec ] -o < ouput > < inputfiles > Listado 4.6: Lı́nea de compilación 4.5. Pragmas Los pragmas son directivas que sirven para especificar qué tiene que hacer el compilador en determinadas situaciones. Estas instrucciones pueden tener efectos a nivel global o a nivel local. Por ejemplo, para el caso del presente trabajo, puede ser necesario el uso del pragma vector para indicar al compilador que un bucle concreto tiene que ser vectorizado, en cuyo caso es un pragma a nivel local. Los pragmas no forman parte de un lenguaje de programación, ya que no figuran en su gramática, pero algunos lenguajes, como C++ y Fortran, tienen disponible palabras clave para su uso, las cuales son tratadas por el preprocesador. En C++ es #pragma y en Fotran es !DIR$. Además, hay que tener en cuenta que los pragmas son dependientes tanto de la máquina como del sistema operativo, aparte de que cada compilador tiene su propio conjunto. Es también posible que la funcionalidad proporcionada por un pragma se consiga con alguna opción particular de compilación. En caso de coincidir en funcionalidad las opciones del compilador con los pragmas, tienen prioridad los segundos. Los pragmas, que se pueden consultar en el Apéndice A (pág. 105), el Apéndice B (pág. 109) R como de otras y el Apéndice C (pág. 113), están disponibles tanto para procesadores Intel R lleven a cabo optimizaciones adicionales. empresas, pero es posible que en procesadores Intel Hay que tener en cuenta que en el caso de Fortran, al hablar de los pragmas, en realidad se habla de directivas y su listado no coincide más que en algunos casos con los de ICC. En el Listado 4.7 y el Listado 4.8, se visualizan las diferencias de sintaxis. Particularmente, los pragmas de ICC tienen la siguiente clasificación: R ICC Specific Pragmas: desarrollados especı́ficamente para trabajar con el comIntel R C++. pilador Intel R ICC Supported Pragmas: desarrollados por fuentes externas que, por razones Intel de compatibilidad, son soportados por este compilador. 1 # pragma nombre [ parametros ] Listado 4.7: Sintaxis de los pragmas de ICC 1 ( c | C |!|*) ( DEC | DIR ) $ directiva [ parametros ] Listado 4.8: Sintaxis de las directivas de Fortran 4.6. Herramientas internas Toda empresa dispone de un conjunto de herramientas que han sido desarrolladas por los propios empleados. Estas suelen tener fines exclusivamente internos e incluso de uso restringido en 30 CAPÍTULO 4. HERRAMIENTAS la propia organización, al poner su disponibilidad bajo la previa aprobación del jefe del grupo. R es una empresa muy grande, de importante capital y con gran cantidad de recursos dispoIntel nibles para sus empleados, incluyendo las herramientas desarrolladas por los diferentes grupos de trabajo. Por este motivo, previa autorización, tuve acceso a un conjunto de ellas de cara a facilitarme la tarea durante el desarrollo de este trabajo. Estas herramientas facilitaban el análisis de las aplicaciones, al presentar todos los datos procedentes de los informes generados por diferentes simuladores, como es el caso de CMP$im y otras pintools, y del informe del compilador, de un modo más legible y fácil de estudiar, que lo que un fichero de texto con multitud de números y lı́neas permite. La herramienta que se usó principalmente permitı́a, para cada unas de las aplicaciones simuladas, entre otras funcionalidades, las siguientes: Analizar los bloques básicos individualmente. Consultar el código fuente asociado a cada bloque básico. Visualizar el flujo de ejecución del programa. Consultar la distribución de funciones e instrucciones. Sintetizar la información procedente de cada nivel de la cache. Todas ellas se usaron principalmente durante la caracterización de las aplicaciones y a la hora de realizar el diagnóstico software para averiguar las causas del bajo grado de vectorización. Capı́tulo 5 Arquitectura del Simulador En el presente capı́tulo se hará un análisis más en detalle del funcionamiento del simulador CMP$im presentado en la Sección 4.2 (pág. 22). Debido a que es una pintool y que esta ı́ntimamente relacionado con la herramienta Pin, habrá detalles de Pin que será necesario exponer para comprender mejor el esquema general del simulador. 5.1. Flujo de ejecución Las dos secciones principales en las que se organiza una pintool, como se describieron en la Sección 4.1.1 (pág. 19), son las secciones de instrumentación y de análisis. CMP$im no es una excepción. En la Figura 5.1 (pág. 31) se presenta una visión general de cómo funciona. En primer lugar tenemos la función main, donde se recogen las opciones de configuración de la pintool introducidas por parámetro y tanto la aplicación objeto del análisis como sus datos de entrada. Aquı́ también se llevan a cabo las inicializaciones pertinentes según la configuración de cache que se haya elegido. Una vez se lanza la simulación no se retorna. Por ello, la finalización de la aplicación está ligada a una función de terminación que sera invocada por Pin en el momento oportuno. Figura 5.1: Diagrama de funcionamiento CMP$im 31 32 CAPÍTULO 5. ARQUITECTURA DEL SIMULADOR En segundo lugar tenemos el bloque principal compuesto por las fases de instrumentación y análisis. Pese a que la instrumentación es la fase con la que se inicia, ambas se van intercalando a medida que se avanza en la simulación. Durante esta fase se van insertando las llamadas a las funciones según el tipo de estructura con el que se esté tratando. Estas funciones se encargarán de realizar la simulación de la cache. Asimismo, irán recogiendo todas las estadı́sticas que se hubieran programado en ellas, como por ejemplo aquellas mencionadas en la Sección 4.2 (pág. 22). En el caso de la simulación de cache se recogen, por ejemplo, el número de aciertos y de fallos, desglosados por hilo, nivel de cache y PC. En el caso de las funciones asociadas a los bloques, se almacenarı́an el número total de instrucciones ejecutadas por cada bloque desglosadas por cada hilo lanzado por la aplicación. El objetivo es almacenar en las estructuras definidas a tal efecto, toda la información que sea de utilidad para caracterizar la aplicación y estudiar su comportamiento sobre la cache definida. Finalmente, una vez ha terminado la aplicación, se ejecuta la función de finalización. En esta se lleva a cabo todo el volcado de la información almacenada en las diferentes estructuras para su posterior tratamiento. En la Figura 5.2 (pág. 32) se muestra de forma más detallada el flujo de ejecución para simular la cache en un modo denominado modo buffer. Este modo se usa cuando se van almacenando todas las instrucciones de memoria de un bloque antes de simularlas. Las llamadas insertadas en la instrumentación están indicadas con la palabra clave call. La llamada a foo1 se insertarı́a al detectar una instrucción de carga de memoria; foo2 cuando es de almacenamiento; finalmente, foo3 se insertará al detectar un bloque básico. Para el modo buffer, la función foo3 serı́a la encargada de iniciar la simulación de instrucciones previamente instrumentadas. Dado que foo3 se ejecutarı́a antes que foo1 y foo2, la primera vez no habrı́a instrucciones que simular. En las veces sucesivas ya se habrı́an instrumentado instrucciones de memoria. Por ello, se da la circunstancia de que cada bloque básico lanzarı́a la simulación de las instrucciones registradas en el bloque anterior. Este bloque podrı́a ser tanto él mismo, en caso de bucle, como otro. Para evitar problemas con la simulación de las instrucciones del último bloque, la función de finalización es una buen candidata donde comprobar si faltan instrucciones de memoria por simular. Figura 5.2: Simulación en modo buffer En la Figura 5.3 (pág. 33) se muestra el flujo de ejecución en modo instrucción a instrucción. En este otro caso las instrucciones se van simulando a medida que se van encontrando, lo cual lo hace más lento que el caso anterior al tener que proceder a realizar todo el trasiego de paso de parámetros para cada instrucción de memoria que se encuentre en el programa. 5.2. ESTRUCTURAS Y CLASES 33 Figura 5.3: Simulación en modo instrucción a instrucción 5.2. Estructuras y clases Las estructuras de datos y clases más importantes usadas en el simulador, y que podrı́an aprovecharse más adelante, son las siguientes: Bloque Básico Rutina Cache Estadı́sticas BLOQUE BASICO Dentro de cada bloque básico CMP$im almacena los siguientes campos: Direcciones de comienzo y fin: son los PCs de la primera y última instrucciones del bloque. Es muy importante saber dónde empieza un bloque y dónde termina porque, como vimos en el modo buffer descrito en la Sección 5.1 (pág. 31), las instrucciones que se tratan son las del bloque anterior. Si conocemos sus lı́mites, se pueden utilizarlos en adelante a modo de ı́ndice. Contador: número de veces que se ejecuta un bloque. Se utiliza como estadı́stica para poder elaborar una lista de bloques básicos ordenados por frecuencia de ejecución. La estructura está instanciada a modo de lista STL. Véase el Listado 5.1 (pág. 33). Hay que tener en cuenta que la instrumentación, pese a estar programada en modo buffer para el caso de los bloques, dentro de pin realmente se realizaba a nivel de traza. Como una traza puede contener varios bloques básicos, en el momento en que se reconoce uno y se inserta en la llamada a la función de análisis correspondiente, no se tiene en cuenta si está solapado con otros bloques. En la Figura 5.5 (pág. 34) se presenta la problemática. 1 list < const BBInfo * > BBInfoList ; Listado 5.1: Lista STL de Bloques Básicos 34 CAPÍTULO 5. ARQUITECTURA DEL SIMULADOR Figura 5.4: Ejemplo de bloque básico Figura 5.5: Proceso de descubrimiento de bloques Imaginemos que tenemos un caso de bloques básicos como el de la Figura 5.4 (pág. 34). Antes de reconocer los dos bloques por separado, Pin identifica un único bloque de instrucciones hasta el siguiente salto. En la Figura 5.5 (pág. 34) se ha denominado BBL 0. Durante la instrumentación se insertarı́a entonces una entrada en la lista con la información del BBL 0 y, en caso de simulación en modo buffer, su correspondiente call a la función de análisis tal y como se ejemplificaba en la Figura 5.2 (pág. 32). A continuación, encontrarı́a una instrucción de salto a una posición dentro BBL 0. Es entonces cuando indicarı́a que hay dos bloques, BBL 1 y BBL 2, que se insertarı́an en la lista y sobre los cuales se agregarı́an los correspondientes call. Ya sea con el nombre BBL 0 o con el nombre BBL 1, ambos comenzarı́an en la misma instrucción. Además, en el código instrumentado aparecerı́an dos llamadas a la función de análisis que cuenta el número de veces que se ejecuta el bloque. Dentro de los parámetros de las llamadas a la funciones de análisis se indicarı́a el elemento de la lista de bloques sobre el que incrementar el contador. Dado que al final de la ejecución ambas entradas tendrán la misma cantidad en el campo Contador, serı́a posible añadir un tratamiento posterior que mejorara las estadı́sticas finales. Situaciones como esta son inevitables, ya que Pin no puede predecir inicialmente que va a haber un salto a una instrucción intermedia hasta que ésta tenga lugar. Por tanto, almacenar todos los bloques que encuentre inicialmente es necesario para, al final de la simulación, realizar las intersecciones y uniones pertinentes para imprimirlos en el informe final. RUTINA El almacenamiento de todas las rutinas ejecutadas permite averiguar, para un bloque básico, en qué rutina se encuentra. Esta información, en posteriores análisis, participa en la identificación del código fuente correspondiente a un bloque en concreto. Los campos de más relevancia son los siguientes. Nombre: de cara al código fuente, es un buen modo de identificar todo el código ensamblador perteneciente a una rutina. Hay que tener en cuenta que habrá llamadas sobre las que se haya hecho inline. Direcciones de comienzo y fin: son los PCs de las instrucciones inicial y final de la rutina. 5.2. ESTRUCTURAS Y CLASES 35 Ayudarán a indexar los bloques básicos que encierran. La estructura estaba instanciada como un mapa STL indexado por el contador de programa de la primera instrucción de la rutina. Véase el Listado 5.3 (pág. 36). La documentación de Pin no aconseja averiguar la última instrucción de la rutina. En el caso de que hubiera varios puntos de salida (return) no se asegura que la ultima instrucción sea realmente la esperada. 1 map < UINT64 , RTNInfo * > RTNInfoMap ; Listado 5.2: Mapa STL de Rutinas CACHE La clase CACHE contiene toda la descripción de las caracterı́sticas y comportamiento aproximados de lo que serı́a una cache real. En ella se centran las rutinas más importantes que simulan el comportamiento en memoria de una aplicación. Los atributos de que consta son los propios de una memoria cache tales como tamaño, asociatividad, tamaño de la lı́nea, latencia, inclusividad o exclusividad, polı́tica de reemplazo, etc. Hay que destacar que, tal y como estaba organizado el simulador, pese a que es una clase que podrı́a coexistir en sı́ misma, estaba diseñada como clase derivada de una clase padre denominada ESTADISTICAS. La función más importante se denominaba CacheAccess. Los parámetros principales de esta función son: PC: instrucción responsable del acceso. Address: dirección de la lı́nea a la que se quiere acceder. Tipo de acceso: es un enumerado que acepta cualquiera de los siguientes tipos de acceso: • LOAD: acceso de lectura. • STORE: acceso de escritura. • SOFTWARE PREFETCH: software prefetch. • READ FOR WRITE: caso especial en el que se accederı́a al siguiente o siguientes niveles de cache para traer una lı́nea que ocasionó un fallo y sobre la que se quiere escribir. Este tipo de accesos no se verı́an en una configuración de cache con polı́tica de escritura write-through. • WRITE BACK: si la polı́tica de reemplazo ocasiona que se sustituya un bloque donde el dirty-bit se encontraba a uno, se producirá un acceso de este tipo para escribir el bloque antes de que se produzca el reemplazo. • WRITE THROUGH: en una configuración con polı́tica de escritura write-through, los accesos a los niveles posteriores al más próximo a la CPU tendrı́an este etiquetado. • INVAL: acceso que se produce en todas las caches del mismo nivel para invalidar un dato que podrı́a haberse modificado. • BACK INVAL: acceso en casos de caches inclusivas. Cuando se producen desalojos de bloques en niveles lejanos al procesador, hay que regresar a los más cercanos para invalidarlos si fuera necesario, manteniendo la inclusividad. Resultado: parámetro de entrada/salida que recoge si ha habido acierto o fallo. 36 CAPÍTULO 5. ARQUITECTURA DEL SIMULADOR Figura 5.6: Punteros a objetos cache 1 void CacheAccess ( INT64 PC , ADDR address , enum TYPE_ACCESS typeAccess ) ; Listado 5.3: Función de acceso a la cache En la definición de la función, hay llamadas sucesivas a otras funciones miembro privadas que se encargaban de lo siguiente: Examinar el mapa de su cache asociado. Averiguar si existı́a la lı́nea solicitada. Proceder a realizar los reemplazos necesarios en caso de que se tratara de un fallo. Lanzar las órdenes de invalidación sobre el resto de caches. Actualizar las estadı́sticas de hit o miss para el PC responsable de la solicitud. El número de objetos creados de la clase CACHE dependen de la configuración pasada por parámetro a la pintool. En la Figura 5.6 (pág. 36) se presenta el modo de almacenar todos los punteros a los diferentes objetos cache creados a través de una matriz. CMP$im soporta aplicaciones multi-hilo. En la configuración inicial es posible indicar cuántos threads acceden a determinado objeto de cache. Es un requerimiento que el número de hilos esperados por la pintool sea potencia de dos, aunque los creados por la aplicación no lo sean. Cada vez que un hilo quisiera acceder a su objeto de cache, existe una función que marca la correspondencia entre el id del thread y el ı́ndice de la cache. Una vez obtenido el puntero, se invocarı́a la función CacheAccess. ESTADISTICAS La clase que almacena las estadı́sticas es una clase de la que posteriormente hereda la clase CACHE. Se encarga simplemente de proveer, para cada uno de los niveles de cache, de los atributos y funciones miembro necesarios para contener estadı́sticas y volcarlas al final de la ejecución. La mayorı́a de estos atributos son a su vez estructuras. Todos están indexados por thread y PC en este orden. Entre los datos más importantes que se contabilizan figuran los siguientes: accesos, aciertos, fallos, tipos de acceso, fallos forzosos, desalojos, vı́ctimas e invalidaciones. 5.3. PARÁMETROS DE EJECUCIÓN 5.3. 37 Parámetros de ejecución La lı́nea utilizada para simular todas las aplicaciones se presenta en el Listado 5.4 (pág. 37). Los knobs que en figuran en este listado son: -cache: sirve para configurar los distintos niveles de cache. Habrá un knob por cada nivel. 1 - cache < identificador >: < tamano >: < tamano linea >: < asociatividad >: < bancos > En nuestro caso, vamos a configurar 2 niveles, siendo el primero de ellos una cache de datos L1 y el segundo una caché unificada L2. Los identificadores están predefinidos, por tanto serán DL1 y UL2. La DL1 será de 32KB con lı́neas de 64B y asociatividad 8. La UL2 será de 1KB con lı́neas de 64B y asociatividad 16. -tlb: para configurar los distintos niveles de TLB. 1 - tlb < identificador >: < lineas >: < tamano pagina >: < asociatividad > Configuraremos dos niveles de TLB de datos. En este caso, de nuevo los identificadores están predefinidos, por tanto seran DTLB y DTLB2. La DTLB tendrá de 64 lineas y un tamaño de página de 4KB, permitiendo mapear 256KB. La DTLB2 tendrá 256 entradas y páginas de 4KB para mapear 1MB. -dl1lat: la latencia de la DL1 será 4. -ul2lat: la latencia de la UL2 será 16. -dtlblat: la latencia de la DTLB será 0. -dtlb2lat: la latencia de la DTLB2 será 6. -inclusive: se modelarán caches inclusivas, por tanto se activará indicándolo con un ’1’. -threads: no se modelarán aplicaciones multihilo, por tanto se indicará que solo se quiere 1 hilo. La latencia de la memoria principal se ha dejado a su valor por defecto, 350 ciclos. Esto influirá tanto en los fallos de la UL2 como en los de la DTLB2. 1 2 3 4 5 6 7 8 9 10 pin -t cmpsim - cache DL1 :32:64:8:1 - cache UL2 :1024:64:16:1 - tlb DTLB :64:4096:8 - tlb DTLB2 :256:4096:8 - dl1lat 4 - l2lat 16 - dtlblat 0 - dtlb2lat 6 - inclusive 1 - threads 1 -- <app > < input > Listado 5.4: Lı́nea para el lanzamiento de la simulación Capı́tulo 6 Caracterización de benchmarks En este capı́tulo se presenta la caracterización de los benchmarks descritos en la Sección 4.3 (pág. 23). Se fundamenta en el primero de los objetivos de la Sección 1.1 (pág. 2) que hacı́a referencia al análisis y clasificación de un conjunto de aplicaciones numéricas en función del grado de vectorización. La caracterización se ha llevado a cabo de la siguiente manera: una vez seleccionada la muestra de benchmarks, se han compilado y se ha extraı́do el porcentaje de vectorización. Para la generación de este porcentaje se ha hecho uso de una herramienta interna disponible en Intel que instrumentaba de manera muy rápida las instrucciones de una aplicación. Como en este punto era importante cuantificar el numero de instrucciones vectoriales de que se compone, se realizó la siguiente clasificación: instrucciones enteras e instrucciones en punto flotante. Las instrucciones en punto flotante están clasificadas a su vez entre aquellas que operan sobre un único elemento, llamadas escalares, vec 1, y las que lo hacen sobre n elementos, llamadas vectoriales, vec n. Adicionalmente, a partir del informe generado por el compilador, se ha extraı́do un resumen sobre las causas encontradas por éste para no vectorizar. Esta información se presenta en dos gráficas por cada benchmark, una con el porcentaje de vectorización y otra con las causas de la no vectorización. 6.1. Polyhedron La Figura 6.1 muestra el porcentaje de instrucciones escalares y vectoriales de los benchmarks de Polyhedron. Están ordenados de izquierda a derecha desde los de menor ı́ndice de vectorización hasta los de mayor, respectivamente. En la Tabla 6.1 (pág. 40) se adjunta el desglose numérico del que se sostiene la figura mencionada. A la derecha de la gráfica tenemos aplicaciones como channel, linpk y test fpu, cuyos ı́ndices de vectorización son óptimos, sobre todo en el caso de channel. En el extremo opuesto tenemos a tfft con el peor ratio de vectorización. Las aplicaciones protein y ace son ejemplos sobre los que no merece la pena centrarse, ya que mayoritariamente tienen instrucciones enteras, las cuales están fuera de ser objetivo de la vectorización. En su lugar, otras aplicaciones como fatigue, doduc, mdbx, nf o induct, parecen interesantes candidatos a ser analizados. Dada la importancia de comprobar el informe generado por el compilador, se presenta la Figura 6.2. El eje de coordenadas izquierdo representa la distribución en porcentajes de las razones más significativas. En él decidimos incluir la distribución de bucles que sı́ han sido vectorizados. El eje de coordenadas derecho presenta la cantidad real de regiones tratadas por el compilador. Se visualizan con una marca blanca rectangular y sirve de referencia para determinar si la vectoriza39 40 CAPÍTULO 6. CARACTERIZACIÓN DE BENCHMARKS Figura 6.1: Índice de vectorización de las aplicaciones de Polyhedron ción en realidad ha sido buena. En la leyenda, una de las series se denomina Other. En esta serie se han incluido aquellas que tenı́an poco peso. En el caso de Polyhedron, se incluyen las siguientes: insufficient computational work, cannot vectorize empty loop, unsupported reduction, conditional assignment to a scalar y statement cannot be vectorized. Conviene resaltar que esta gráfica no deja de ser una representación estática del log del compilador. Existen bucles que, habiendo sido vectorizados, o bien no se ejecutan debido a los datos de entrada, o bien el número de instrucciones que contiene es pequeño, impidiendo que el ı́ndice de vectorización sea más favorable. Polyhedron channel linpk test fpu induct gas dyn nf capacita air mdbx doduc aermod fatigue ac protein tfft Enteras % Escalares % Vectoriales % 468.500.485 899.584.515 4.029.631.938 988.762.995 1.329.097.003 2.760.204.099 52.878.534.140 9.305.166.389 8.203.617.802 29.803.502.394 41.360.047.064 4.582.406.825 8.552.412.161 125.788.239.418 1.307.018.392 24,62 29,43 50,00 9,05 38,71 29,81 61,32 60,05 35,86 35,56 58,32 29,36 79,56 96,33 29,89 2.930.591 134.296.667 519.826.479 5.319.266.886 764.150.080 4.497.941.028 15.135.853.071 3.080.714.025 11.730.283.833 44.849.452.311 25.593.434.008 10.443.420.655 2.009.202.064 3.640.638.101 3.061.907.878 0,15 4,39 6,45 48,66 22,26 48,57 17,55 19,88 51,27 53,51 36,09 66,92 18,69 2,79 70,03 1.431.883.011 2.022.708.587 3.510.250.489 4.623.569.301 1.340.360.535 2.002.646.277 18.214.751.144 3.110.552.221 2.945.337.303 9.160.986.696 3.967.026.727 580.509.855 188.047.565 1.155.380.485 3.443.341 75,23 66,18 43,55 42,30 39,04 21,63 21,12 20,07 12,87 10,93 5,59 3,72 1,75 0,88 0,08 Tabla 6.1: Desglose de instrucciones de las aplicaciones de Polyhedron Se observa que channel está en cabeza en cuanto al número de bucles vectorizados. Hacen un total de 66, cantidad pequeña comparativamente. Pese a ello, el 82 % de dichos bucles está vectorizado permitiendo que la oportunidad de ejecutar una instrucción vectorial aumente. Esto se traduce en un óptimo ı́ndice de vectorización (recuérdese la Figura 6.1 (pág. 40)). La aplicación tfft es interesante porque mientras que el ı́ndice de vectorización era nulo, los bucles vectorizados 6.2. MANTEVO 1.0 41 Figura 6.2: Razones para no vectorizar bucles en Polyhedron ocupan el 33 % del total. Se tratarı́a por tanto de bucles que no se ejecutan o que lo hacen con muy poca frecuencia. Por otro lado, mientras que aermod destaca por la cantidad de bucles que contiene, 2597, un 72 % no se ha vectorizado, siendo la razón de peso que los bucles no iteran lo suficiente como para que sea eficiente, low trip count. En air, el número de bucles vectorizados compite con los que no lo han sido. El motivo principal es not inner loop. Cada vez que el compilador se dispone a vectorizar un bucle de entre un conjunto de bucles anidados, para todos los bucles externos registra como motivo que no son bucles internos. En air, se entiende entonces que hay muchos bucles anidados. 6.2. Mantevo 1.0 El ı́ndice de vectorización de las aplicaciones de Mantevo visualizadas en la Figura 6.3 (pág. 42) no parecen dar tanto juego como en Polyhedron. La Tabla 6.4 (pág. 43) adjunta el desglose de instrucciones. La aplicación CoMD, que podrı́a ser uno de los candidatos, tiene un 69,35 % de instrucciones enteras. Si bien podrı́a ser estudiado para reducir el número de instrucciones escalares, vec 1, las enteras no parecen dejar mucho margen para conseguir una mejora más significativa. En la Figura 6.4 (pág. 43) con las razones del compilador para no vectorizar, las opciones englobadas en la categorı́a other son: low trip count, loop was transformed to memset or memcpy y conditional assignment to a scalar. La aplicación CloverLeaf, que presentaba el mejor ı́ndice de vectorización con un 73,13 % de instrucciones vec n, aquı́ se encontrarı́a por detrás de miniGhost en cuanto a bucles vectorizados. Entre los motivos para no vectorizar, es mayoritaria la razón not inner loop. En Polyhedron ya vimos que no es una razón de peso. Además, dado que es la aplicación con mayor número de bucles, 1127, en realidad se trata de un caso de éxito. En la misma lı́nea tenemos a miniGhost, con un resultado semejante en cuanto a bucles vectorizados, 42 CAPÍTULO 6. CARACTERIZACIÓN DE BENCHMARKS Figura 6.3: Índice de vectorización de las aplicaciones de Mantevo 1.0 Mantevo CloverLeaf miniGhost HPCCG miniMD miniFE miniXyce CoMD Enteras % Escalares % Vectoriales % 3.312.609.977 5.817.781.939 9.153.318.789 9.076.376.697 30.524.781.122 151.901.922.911 3.899.688.850 17,09 57,48 54,30 50,00 66,90 96,07 69,35 1.896.586.386 290.628.926 1.813.492.799 3.886.133.542 5.714.424.611 4.734.235.225 1.711.104.306 9,78 2,87 10,76 21,41 12,52 2,99 30,43 14.174.588.802 4.013.310.416 5.890.463.437 5.189.843.145 9.384.984.596 1.485.511.169 12.159.534 73,13 39,65 34,94 28,59 20,57 0,94 0,22 Tabla 6.2: Desglose de instrucciones de las aplicaciones de Mantevo pero con 294 bucles. Volviendo a su ı́ndice de vectorización, 39,65 %, no deja de ser bajo debido a las instrucciones enteras. Por otro lado, HPCCG sorprende, ya que es el que tiene menor número de bucles, 78, pero pese a ser pocos, se consigue vectorizar un 46 %, que se traducen en un ı́ndice de vectorización del 34,94 %. Finalmente, miniXyce y CoMD están a la cola como ocurrı́a con sus ı́ndices de vectorización respectivos. Sorprende miniXyce, ya que tiene más bucles que CoMD, pero su ı́ndice de vectorización es insignificante al ser una aplicación mayoritariamente entera. 6.3. Sequoia En la Figura 6.5 (pág. 44), las aplicaciones SPhotmk y Crystalmk, con un 50,5 % y un 46,69 % de instrucciones escalares, respectivamente, parecen claros candidatos a elegir para su análisis. Véase también el desglose de instrucciones en la Tabla 6.3 (pág. 43). En la Figura 6.6 (pág. 44), los motivos incluidos en la categorı́a other son: unsupported loop structure, cannot vectorize empty loop y conditional assignment to a scalar. Las aplicaciones de este benchmark tienen pocos bucles, comparados a grosso modo con los benchmarks vistos anteriormente. Aquel que más tiene, UMTmk, asciende a 105. Sin embargo, 6.4. NPB 43 Figura 6.4: Razones para no vectorizar bucles en Mantevo 1.0 Sequoia IRSmk UMTmk Crystalmk SPhotmk Enteras % Escalares % Vectoriales % 6.418.427.371 85.434.122 5.566.079.613 47.201.295 44,33 66,40 47,53 43,94 60.616.654 12.366.599 5.468.000.498 54.255.433 0,42 9,61 46,69 50,50 8.000.112.715 30.872.142 677.000.196 5.972.314 55,25 23,99 5,78 5,56 Tabla 6.3: Desglose de instrucciones de las aplicaciones de Sequoia dejando de lado este dato, sorprende SPhotmk que, junto con Crystalmk, y pese a tener ambos los peores ı́ndices de vectorización, tienen más bucles que la aplicación con mejor ı́ndice, IRSmk. Esto significa dos cosas: el número de bucles vectorizados serı́a mayor proporcionalmente; al haber más bucles, hay más candidatos a ser mejorados y por tanto son aplicaciones interesantes para abordarlas de cara al análisis. Finalmente, la aplicación IRSmk también sorprende porque, teniendo mejor ı́ndice, en realidad el número de bucles tratados es inferior a 20. Por tanto, los bucles de que consta, por pocos que sean, concentran la mayor parte del cómputo dando lugar a un rendimiento óptimo pese a la limitación impuesta por las instrucciones enteras. 6.4. NPB La Tabla 6.4 (pág. 45) y la Figura 6.7 (pág. 45) muestran que las aplicaciones UA, IS, BT y LU se corresponderı́an a los candidatos de más interés. Hasta ahora, en las gráficas mostradas comprobamos que, pese a que una aplicación tenga un buen ı́ndice de vectorización, es en las instrucciones escalares donde se encuentran las oportunidades para vectorizar. 44 CAPÍTULO 6. CARACTERIZACIÓN DE BENCHMARKS Figura 6.5: Índice de vectorización de las aplicaciones de Sequoia Figura 6.6: Razones para no vectorizar bucles en Sequoia 6.4. NPB 45 Figura 6.7: Índice de vectorización de las aplicaciones de NPB NPB FT MG EP SP CG LU BT IS UA DC Enteras % Escalares % Vectoriales % 141.213.978 95.558.650 537.991.137 5.435.933.967 419.556.698 3.604.965.775 986.948.941 141.146.461 2.089.546.384 135.544.766.627 45,61 32,86 36,23 50,67 62,22 28,18 13,41 56,20 26,02 97,66 7.116.085 45.070.701 451.200.929 1.944.681.817 62.606.098 6.447.471.196 5.286.544.985 88.048.143 5.495.880.739 825.664.200 2,30 15,50 30,38 18,13 9,28 50,40 71,85 35,06 68,43 0,59 161.292.981 150.211.106 495.838.486 3.348.529.940 192.116.340 2.740.743.624 1.083.789.029 21.974.211 445.471.779 2.424.457.394 52,09 51,65 33,39 31,21 28,49 21,42 14,73 8,75 5,55 1,75 Tabla 6.4: Desglose de instrucciones de las aplicaciones de NPB En la Figura 6.8 (pág. 46), los motivos englobados en la categorı́a other son: statement cannot be vectorized, condition may protect exception, operator unsuited for vectorization, unsupported data type y cannot vectorize empty loop. La aplicación FT presenta el mejor ı́ndice de vectorización, 52,09 %, pero el informe generado por el compilador para ella destaca por el bajo número de bucles tratados, 35. Se trata de otro caso como IRSmk de Sequoia. El ı́ndice es bueno porque, al ser pocos bucles, aparentemente se han vectorizado aquellos que se ejecutan más, incluso siendo el porcentaje de bucles vectorizados de solo un 35 %. Por otro lado tenemos UA, con 906 bucles. Si de todos estos bucles un 36 % ha sido vectorizado y un 31 % eran bucles externos, parece que no es suficiente la razón vectorization posible but seems inefficient para justificar el bajo ı́ndice de vectorización obtenido, 5,55 %. Parece un caso contrario al de FT, en el que los bucles vectorizados no tienen un peso suficiente en la ejecución. Finalmente, comentar que el compilador ha tratado únicamente 16 bucles para la aplicación EP y, teniendo en cuenta que tiene un ı́ndice de vectorización del 33,39 %, demuestra que el cómputo general del programa está repartido entre pocos bucles. 46 CAPÍTULO 6. CARACTERIZACIÓN DE BENCHMARKS Figura 6.8: Razones para no vectorizar bucles en NPB 6.5. SPEC FP Pese a que la Figura 6.9 (pág. 47) y la Tabla 6.5 (pág. 47) nos muestran que las aplicaciones cactusADM y zeusmp están por detrás de lbm en el grupo de las aplicaciones con mejor ı́ndice de vectorización, el porcentaje de instrucciones escalares no deja de ser menos significativo que otras aplicaciones como namd. Al igual que namd, povray y milc presentan una buena candidatura por el simple hecho de que su ı́ndice de vectorización es prácticamente inexistente. En la Figura 6.10 (pág. 48) las razones incluidas en la categorı́a other son: statement cannot be vectorized, condition may protect exception, operator unsuited for vectorization, unsupported data type y cannot vectorize empty loop. Destaca gamess por la gran cantidad de bucles tratados por el compilador, 52237. Sin embargo, pese al 47 % de bucles vectorizados, el ı́ndice de vectorización no es más que del 13,08 %, con lo que dichos bucles no tienen gran parte del peso del programa. Por su parte, lbm destaca por tener muy poca cantidad de bucles, 64, pero con más de la mitad de ellos vectorizados ha sido suficiente para obtener un ı́ndice de vectorización del 76,41 %. Lo datos presentados dejan fehaciente que, pese a que el compilador esté especialmente diseñado para explotar la vectorización, no se consiguen resultados favorables para todas las aplicaciones. Esto se traducirı́a en un deficiente uso de la unidad de vectorización. También hay que tener presente el contraste entre el carácter dinámico de la gráfica con el porcentaje de vectorización, frente al carácter estático de aquella con el informe generado por el compilador. Estas razones dan lugar a generar informes más completos del comportamiento de las aplicaciones, para lo cual es necesario completar el simulador CMP$im. 6.5. SPEC FP 47 Figura 6.9: Índice de vectorización de las aplicaciones de SPEC fp SPEC fp 470.lbm 436.cactusADM 434.zeusmp 437.leslie3d 481.wrf 410.bwaves 459.GemsFDTD 416.gamess 465.tonto 435.gromacs 482.sphinx3 450.soplex 454.calculix 433.milc 453.povray 444.namd 447.dealII Enteras % Escalares % Vectoriales % 247.043.444 87.202.676 3.098.091.055 3.466.782.814 1.430.157.094 5.608.197.533 2.579.077.218 814.275.217 1.735.105.509 1.077.444.401 3.949.495.345 37.411.459 101.212.053 2.126.321.655 1.225.769.598 18.465.424.606 34.050.212.478 17,79 17,90 22,22 42,50 60,39 61,63 79,49 64,01 63,42 58,22 84,16 80,35 91,84 13,85 65,92 40,98 74,97 80.478.947 717.855.566 5.964.021.317 1.932.662.908 379.485.584 1.506.737.948 135.223.572 291.456.487 686.200.465 567.698.837 256.885.880 7.610.447 5.847.184 12.860.333.431 597.196.874 26.139.516.627 11.045.728.175 5,80 44,73 42,78 23,69 16,02 16,56 4,17 22,91 25,08 30,68 5,47 16,34 5,31 83,76 32,11 58,00 24,32 1.061.043.618 599.834.350 4.879.315.996 2.757.121.526 558.461.158 1.985.594.787 530.125.307 166.450.557 314.702.162 205.426.782 486.219.594 1.539.792 3.139.907 366.588.682 36.644.135 460.045.395 321.833.495 76,41 37,38 35,00 33,80 23,58 21,82 16,34 13,08 11,50 11,10 10,36 3,31 2,85 2,39 1,97 1,02 0,71 Tabla 6.5: Desglose de instrucciones de las aplicaciones de SPEC FP 48 CAPÍTULO 6. CARACTERIZACIÓN DE BENCHMARKS Figura 6.10: Razones para no vectorizar bucles en SPEC fp Capı́tulo 7 Adaptación del Simulador En este capı́tulo se describen las ampliaciones que se han realizado sobre el simulador CMP$impara incluir capacidades de ejecución vectoriales. A la hora de decidir qué tipo de capacidades vectoriales incluir, se ha optado por realizar un modelado sencillo de la arquitectura del coprocesador R Xeon PhiTM . No se pretendı́a realizar un análisis del rendimiento de este producto, sino Intel tener un modelo que se sabe hace uso de unidades vectoriales con el que generar estadı́sticas sobre esta caracterı́stica de formas rápida y sencilla. Para abordar una descripción detallada del simulador construido, se va a partir de las siguientes tres preguntas: R Xeon ¿Qué caracterı́sticas tiene que tener el pipeline para adaptarse a la arquitectura Intel PhiTM Coprocessor? ¿Hay datos intermedios ya generados por CMP$imque puedan ser reutilizados? ¿Qué estadı́sticas particulares permitirán discernir sobre el uso de la unidad vectorial? 7.1. Pipeline Al instrumentar una aplicación, es sencillo contar el número de instrucciones ejecutadas pues únicamente se trata de incrementar un contador. La contabilización de ciclos también serı́a una tarea sencilla en caso de una máquina multiciclo, en la que hasta que no finaliza una instrucción, no se ejecuta la siguiente. En el caso de una máquina segmentada esta tarea ya no es tan trivial. Hay que tener en cuenta las peculiaridades de la arquitectura para situar a una instrucción en la etapa correcta según avance la ejecución. Rememorando lo comentado en la Sección 5.1 (pág. 31), se presenta la Figura 7.1 (pág. 50) con el boceto sobre dónde se colocarı́a la simulación del pipeline. Sabemos que se alternan las fases de instrumentación y análisis a medida que se avanza en la ejecución. La fase de simulación de cache se hace durante la fase de análisis usando datos que se han registrado en la fase de instrumentación. La simulación del pipeline es análoga y, en este caso ademas, necesita de la simulación de cache para utilizar la información que de esta se genera. Por tanto, necesariamente tiene que ir a continuación como se visualiza en la figura. R Xeon Phi R es una máquina en orden con terminación fuera de orden El coprocesador Intel (véase Sección 2.3 (pág. 8)). Para simplificar el modelo no se han implementado etapas, con lo 49 50 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Figura 7.1: Pipeline dentro de CMP$im que cuando una instrucción entra en nuestro pipeline, significa que se va a ejecutar, ya sea un acceso a memoria o una división vectorial. Se han tomado las siguientes consideraciones a la hora de implementar el pipeline: Cada instrucción entrará en el pipeline un ciclo después de la entrada de la instrucción anterior. No hay lı́mite de recursos. Los registros se escriben en el banco de registros después de la finalización de la instrucción. Si se producen dependencias de instrucciones no harı́a falta esperar a la escritura en el banco. Existen buses por donde recoger esa información antes de llegar al banco. La latencia de una instrucción se caracteriza por los ciclos que necesita para completar una operación sin contar las dependencias. Un fallo en el primer nivel de cache que implique traer una lı́nea de los niveles siguientes, congela automáticamente el pipeline tanto si la instrucción siguiente necesita el dato como si no. Esto se produce con cualquier tipo de acceso a memoria bajo demanda (load, store, software prefetch, etc.) Los ciclos situados entre ciclo entrada ins actual − ciclo entrada ins anterior + 1 se utilizarán exclusivamente con fines estadı́sticos. Para ejemplificarlo se utilizará un pequeño kernel escrito en Fortran y basado en el bucle s171 del conjunto de bucles del benchmark LCD. Véase el código en Listado 7.1 (pág. 51). El bloque con mayor número de instrucciones ejecutadas mostrado en el Listado 7.2 (pág. 51) se corresponde exactamente con el bucle do..continue del kernel. Como es un ejemplo simple para mostrar el funcionamiento del pipeline no se ha compilado para la arquitectura AVX-512. Para el ejemplo no se mostrará más que una iteración. La estructura de bloques disponibles es indispensable para lo que serı́a la creación de un histograma con información estática de las instrucciones. Permitirı́a entonces acceder a los datos que caracterizan a las instrucciones, como por ejemplo la latencia para aquellos casos donde se realice una operación. Aparte, como se mostró en la Figura 7.1 (pág. 50), para construir un pipeline ficticio es indispensable hacer uso de los datos proporcionados por las rutinas de simulación de cache de CMP$im. En la Tabla 7.1 (pág. 51) se muestra la información recopilada de las instrucciones de más interés desenrolladas por el compilador. Esta información recoge datos de latencias de tlb, cache y operación. La instrucciones que realizan las operaciones de suma y multiplicación entran dentro de la clasificación de las FMA. Su latencia, 4, se puede consultar más adelante en el Listado 7.5 7.1. PIPELINE 51 (pág. 55). Véase la Figura 7.2 (pág. 52) con el detalle del pipeline. Téngase en cuenta que la cuadrı́cula es una aproximación para entender el funcionamiento de nuestro kernel basado en el R Xeon Phi. R coprocesador Intel c c c -- Fichero : main . f integer n real a (100000) , b (100000) , c (100000) open ( unit =1 , file = " N " ) read (1 ,*) n close (1) call s171 (a , b , c , n ) end c c c -- Fichero : s171 . f subroutine s171 (a , b , c , n ) integer n real a ( n ) ,b ( n ) ,c ( n ) do 10 i = 1 , n a(i) = a(i) + b(i) * c(i) continue return end 10 Listado 7.1: Kernel s171 basado en el mismo bucle en LCD 1 2 3 4 5 6 7 8 9 10 11 4004 f9 : 4004 fd : 400502: 400506: 40050 b : 40050 f : 400514: 400518: 40051 d : 400521: 400524: movups xmm0 , xmmword ptr [ rsi + rax *4] movups xmm1 , xmmword ptr [ rsi + rax *4+0 x10 ] mulps xmm0 , xmmword ptr [ rdx + rax *4] mulps xmm1 , xmmword ptr [ rdx + rax *4+0 x10 ] addps xmm0 , xmmword ptr [ rdi + rax *4] addps xmm1 , xmmword ptr [ rdi + rax *4+0 x10 ] movups xmmword ptr [ rdi + rax *4] , xmm0 movups xmmword ptr [ rdi + rax *4+0 x10 ] , xmm1 add rax , 0 x8 cmp rax , r9 jb 0 x4004f9 Listado 7.2: Bloque básico de s171 con mayor número de instrucciones ejecutadas Latencias TLB Cache load 356 370 Operación load 4 mul 4 4 mul 4 4 370 4 add 4 4 store 4 store 4 add 356 Tabla 7.1: Latencias de memoria y de instrucción (load-op) A continuación se explica en detalle el pipeline de la Figura 7.2 (pág. 52): 52 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Figura 7.2: Pipeline del bloque de s171 load - movups xmm0, xmmword ptr [rsi+rax*4]: La latencia es alta porque se ha producido fallo tanto en TLB como en L1. Se ha tenido que llegar a la memoria principal. Como se comentó, esta situación provoca la congelación del pipeline. La siguiente instrucción no entrarı́a hasta el ciclo 726. load - movups xmm1, xmmword ptr [rsi+rax*4+0x10]: cache, DL1, por lo que entra en el 726 y termina en el 730. Acierto en el primer nivel de mul - mulps xmm0, xmmword ptr [rdx+rax*4]: Instrucción tipo load-op. Dependencia del registro xmm0 sobre la primera carga sin consecuencias. La instrucción entrarı́a en el ciclo 726+1, a continuación de la anterior porque el registro xmm0 estarı́a disponible. Como es load-op carga primero el dato, acierto en DL1 (4 ciclos) y opera (4 ciclos), haciendo un total de 8 ciclos. Entrarı́a pues en el 727 y saldrı́a en el 735. mul - mulps xmm1, xmmword ptr [rdx+rax*4+0x10]: Instrucción tipo load-op. Dependencia del registro xmm1 en el segundo load. Idealmente entrarı́a en el 728, pero como la carga de la que depende no termina hasta el 730, se retrasa 2 ciclos. De nuevo cargarı́a primero el dato (4 ciclos) y operarı́a sobre él (4 ciclos). Entrarı́a pues en el ciclo 730 y sale en el 738. add - addps xmm0, xmmword ptr [rdi+rax*4]: Instrucción tipo load-op. Dependencia del registro xmm0 sobre la primera multiplicación. Idealmente entrarı́a en el 731, pero se retrasa 4 ciclos. Falla en el TLB y en la cache y tiene que ir hasta memoria, ocasionando que se congele el pipeline. La siguiente instrucción no entrarı́a hasta que pasaran los 726 ciclos de memoria. Finalmente realiza la suma (4 ciclos). Entra entonces en el ciclo 735 y sale en el 1465. add - addps xmm1, xmmword ptr [rdi+rax*4+0x10]: Instrucción tipo load-op. Dependencia del registro xmm1 sobre la segunda multiplicación sin consecuencias. Entra en el ciclo 1461 debido a la congelación del pipeline de la instrucción anterior. Termina en el 1469 por el acceso a memoria (4 ciclos) y la suma (4 ciclos). store - movups xmmword ptr [rdi+rax*4], xmm0: Dependencia del registro xmm0 sobre la primera suma. Idealmente entrarı́a en el ciclo 1462 pero se retrasa 3 ciclos. Entonces entra en el 1465 y termina en el 1469. Acierta en la cache. 7.2. DETECCIÓN DE INSTRUCCIONES Y REGISTROS 53 store - movups xmmword ptr [rdi+rax*4+0x10], xmm1: Dependencia del registro xmm1 sobre la segunda suma. Idealmente entrarı́a en el 1466 pero se retrasa 3 ciclos hasta el 1469. Finaliza en el 1473 pues acierta en el primer nivel de cache. 7.2. Detección de instrucciones y registros La caracterización de las instrucciones es un paso esencial para el funcionamiento de este simulador. Permite conocer al detalle las instrucciones de cara a construir, por ejemplo, una tabla de registros. También permitirı́a saber cuánto tiempo va a requerir para terminar la operación que contenga, independientemente de las dependencias de los operandos. De cara a las instrucciones exclusivamente de memoria o las load-op, que cargan un dato y operan sobre el mismo, no es posible conocer los ciclos totales hasta la simulación sobre la cache. La API de Pin para instrumentar instrucciones pone a disposición del usuario multitud de funciones que ayudan a la caracterización. Sin embargo, mientras que existen funciones que detectan fácilmente si, por ejemplo, la instrucción es un salto o una comparación, el repertorio no es suficientemente granular para caracterizar instrucciones vectoriales. No es capaz de discernir si la instrucción es simplemente vectorial o si los registros son vectoriales. Lo mismo ocurre con la caracterización de las instrucciones tipo load-op. Hay que tener en cuenta que las instrucciones que se iban a instrumentar eran de las extensiones AVX-512. Estas instrucciones, en el momento de construir el simulador, no eran públicas. Por tanto no era posible para Pin proporcionar una API que permitiera reconocer los nuevos registros de 512 bits. Ya fuera entonces por la no publicidad de la arquitectura, o porque simplemente no disponı́a de funciones con la suficiente granularidad, se creó el código necesario para subsanar estas carencias. 7.2.1. Instrucciones INS IsVector: para discernir si una instrucción es vectorial o no, usamos librerı́as de uso interno en donde todas y cada una de las instrucciones de AVX-512 estaban caracterizadas. Mediante estas funciones era posible consultar campos como la categorı́a o extensión de una instrucción. Para nuestros propósitos, bastaba con usar la extensión, cuya clasificación era más genérica, reduciendo por tanto las lı́neas de código. is scalar simd: de nuevo, mediante el uso de la misma librerı́a interna, era posible determinar si una instrucción estaba operando sobre un único o múltiples elementos de un vector. is load op: la heurı́stica consiste en contar el número de operandos de lectura totales de la instrucción, x, y el número de operandos de memoria, y. Si x > y entonces la instrucción es load-op, ya que el operando de más es aquel que se operará junto con el dato cargado de memoria. Véase el detalle en el Listado 7.3 (pág. 53). 1 2 3 4 5 6 7 8 9 10 11 12 13 bool is_load_op ( INS ins ) { if ( I s M e m I n s t r u c t i o n ( ins ) ) { UINT32 operandCount = IN S _ O p e r a n d C o u n t ( ins ) ; UINT32 readOp = 0; UINT32 memReadOp = 0; for ( UINT32 i = 0; i < operandCount ; i ++) if ( IN S _O pe ra n dR ea d ( ins , i ) ) { readOp ++; if ( I N S _ O p e r a n d I s M e m o r y ( ins , i ) ) memReadOp ++; 54 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR 14 15 16 17 18 19 20 } if ( memReadOp && readOp > memReadOp ) return true ; } return false ; } Listado 7.3: Función para detectar instrucciones load-op 7.2.2. Registros La detección de registros pasó a formar parte del algoritmo general de instrumentación de instrucciones. También se utilizaron funciones de librerı́as internas que reconocieran los registros zmm de AVX-512. El modus operandi consistió en analizar individualmente cada operando y clasificarlo del siguiente modo: Operando con registro: es aquel sobre el que se va a operar, ya sea para leer o escribir. Operando de memoria: tuvimos en cuenta que habrı́a registros con los que se opera para obtener una dirección de memoria. La ventaja de acceder a los registros de este modo es que nos permite conocer, a través del operando, si es un registro del que se va a leer o sobre el que se va a escribir. Los operandos de memoria nos permitieron hacer un análisis más granular mediante la clasificación de los registros según el tipo de direccionamiento: base, ı́ndice o segmento. 7.2.3. Latencias En CMP$im, la latencia de las instrucciones se toma por defecto a 1. Para las instrucciones de memoria se agregarı́a la cantidad de ciclos correspondientes a la latencia, ya suponga un acierto en la L1 o se tuviera que traer la lı́nea desde memoria. Para este trabajo tuvimos que modificar el tratamiento de latencias de las instrucciones alejándolo del sistema ideal por defecto de CMP$im. En el Listado 7.4 (pág. 54) se presenta la clasificación que hicimos de las instrucciones. Por defecto, los valores se mantuvieron a 1. 1 2 3 4 5 # define # define # define # define # define NONV PU_LATE NCY MISC_LATENCY D I V _ S Q R T _ L A T EN C Y RCP_RSQRT_LATENCY FMA_LATENCY 1 1 1 1 1 Listado 7.4: Clasificación de las latencias por defecto Veámoslas en detalle: NONVPU : conjunto de instrucciones enteras. DIV/SQRT : tanto la división como la raı́z cuadrada son operaciones muy costosas que tienen semejante número de ciclos. 7.3. NUEVAS ESTRUCTURAS Y CLASES 55 RCP/RSQRT : las instrucciones recı́procas tienen equivalente latencia, incluyendo las raı́ces recı́procas. La categorı́a recı́procas indica que se está calculando el inverso: 1/x en el caso √ de las rcp en general y −1/2 x en el caso de las rsqrt. FMA: operaciones de punto flotante que realizan suma y multiplicación en el mismo paso. MISC : resto de instrucciones que no entran en el perfil de las enteras, pero que sı́ pueden ser vectoriales/escalares. Dado que estas latencias deberı́an ser modificables, se tomó la decisión de incorporar un knob que permitiera introducir esta configuración a través de un fichero. Véase el ejemplo del Listado 7.5 (pág. 55) 1 2 3 4 5 6 7 8 9 10 11 # Example configuration file for instruction latencies . # Non - VPU instructions nonvpu 1 # VPU instructions rcprsqrt 8 divsqrt 30 fma 4 # Default VPU instructions misc 2 Listado 7.5: Fichero de ejemplo con latencias Finalmente, durante la caracterización de instrucciones vista en Sección 7.2.1 (pág. 53), se agregó una nueva función denominada GetLatencyByIclass que, haciendo uso de funciones internas, permiten consultar la clase de la instrucción y asignarle una latencia en función de la misma. 7.3. Nuevas estructuras y clases R Xeon PhiTM que se Las estructuras nuevas para el núcleo basado en el coprocesador Intel presenta en este apartado son el resultado de entenderlo como un módulo añadido a toda la simulación de CMP$im. Se necesita la información recogida por las instrucciones, ya sean de memoria o no, y se tiene que respetar el funcionamiento original de CMP$im para ambos modos buffer e instrucción a instrucción. Decidimos por tanto desarrollarlo en dos nuevos ficheros core.cpp y core.h. La localización de las llamadas al pipeline de este núcleo era un problema que habı́a que abordar desde el punto de vista del análisis más que de instrumentación. Recordamos el análisis realizado sobre el pipeline en la Sección 7.1 (pág. 49). Decidimos situarlas antes de la ejecución de cada bloque. Véase la Figura 7.3 (pág. 56). Este esquema encaja con el modo buffer porque previamente se han simulado las instrucciones del bloque anterior. En el caso del primer bloque no se simulará nada, mientras que en el caso del último bloque será la función de finalización la que lanzarı́a el pipeline. También encaja con el modo instrucción a instrucción porque cuando se ha llegado al bloque siguiente, las instrucciones de memoria del anterior ya se han simulado en la cache, aunque fuera individualmente. La idea consistió en desarrollar una clase nueva denominada CORE. Este concepto de core es ligeramente distinto a lo que venı́amos nombrando como núcleo. La clase CORE representa el esquema de los cores que componen el coprocesador. Si quisiéramos simular la tarjeta completa, se crearı́an 50 objetos. Dado que en este trabajo no se toca el campo multi-thread, con simular un core era suficiente. La idea se asemeja a la clase CACHE explicada en la Sección 5.2 (pág. 33). De 56 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Figura 7.3: Localización del simulador de pipeline en CMP$im hecho, están ı́ntimamente relacionados, ya que cada objeto CORE creado tendrá su objeto CACHE asociado. Cada objeto CORE además tendrı́a su propio pipeline y sus propias estructuras de almacenamiento de estadı́sticas y del estado del core en cada momento. Por otro lado, al tener que almacenar información detallada sobre todas las instrucciones instrumentadas, ya fueran de memoria o no, era necesaria una estructura solo modificable en tiempo de instrumentación con el footprint de la aplicación. Además, también serı́a necesario guardar los registros utilizados en tiempo de análisis para detectar dependencias. Véase la Figura 7.4 (pág. 56) para obtener una imagen visual de estos conceptos. Figura 7.4: Idea para la implementación de KNC Las estructuras y clases principales desarrolladas para ello fueron las siguientes: Footprint Basic Block State Register File State Stats Core 7.3. NUEVAS ESTRUCTURAS Y CLASES 57 FOOTPRINT Durante la fase de instrumentación se analizan todas las instrucciones de la aplicación. Es necesario por tanto almacenar la información estática recopilada de cada una de ellas para su posterior uso durante la fase de análisis. Esta estructura se implementó para tal fin. Los campos de que consta son los siguientes: Tipo: enumerado que recoge la siguiente clasificación posible: Entera 1 2 3 4 5 6 7 8 Escalar Vectorial Memoria typedef enum { INS_TYPE_NONVPU , INS_TYPE_V_VECTOR , INS_TYPE_V_SCALAR , INS_TYPE_MEM , INS_TYPE_NUM } INS_TYPE_t ; Latencia operacional: si una instrucción tiene esperar a que otra termine de realizar una operación para usar su resultado, es necesario conocer cuánto va a tardar la operación en sı́. Esto incluye a las instrucciones denominadas load-op, las cuales a la vez que cargan un dato en la cache, operan sobre él una vez disponible. Sin embargo, este campo no servirá para almacenar la latencia provocada por una instrucción de memoria. Tamaño de la instrucción: para saber exactamente en qué punto termina un bloque básico. Registros fuente y destino: necesarios para realizar el control de dependencias de registro. Desensamblado: con fines de depuración. Rutina: a qué rutina pertenece la instrucción. Esta estructura se declaró sobre un mapa STL, siendo la clave de acceso el PC de la instrucción. Véase el Listado 7.6 (pág. 57). 1 typedef map < UINT64 , INS_FOOT_PRINT_t > FOOT_PRINT ; Listado 7.6: Footprint de las instrucciones de la aplicación REGISTER TABLE La detección de las dependencias entre registros requiere llevar un seguimiento de las escrituras de los mismos. Por ello, se creó una pequeña estructura que almacena la siguiente información. Ciclo: se actualiza cada vez que se escriba el registro. Por tanto, indica el ciclo en el que un registro estará disponible. Dirección: PC de la instrucción que ha sido la última en escribir dicho registro. Sirve para poder responsabilizar a una instrucción de que otra ha tenido que atrasar su ejecución. Desglose de ciclos: para casos en que una instrucción de memoria haya escrito el registro, se almacena el desglose de ciclos generados para conseguir el dato. Esta información es temporal y solo influye en tiempo de simulación, no en los resultados finales. Serán vectores de tamaño fijo dado por las macros KNC TLB LVLS y KNC CACHE LVLS. 58 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Figura 7.5: Instrucción que toca dos lı́neas Esta estructura se declaró en el interior de un mapa STL indexado por el código del registro. Habrá tantas entradas como registros se hayan accedido en la ejecución. Véase el Listado 7.7 (pág. 58). Esta tabla solo se actualiza durante la simulación, puesto que ya tenemos disponibles los registros utilizados por cada instrucción. Estos fueron previamente almacenados durante la instrumentación de las instrucciones. 1 typedef map < UINT32 , REG_FILE_STATE_t > REG_FILE ; Listado 7.7: Mapa STL de la Tabla de Registros BASIC BLOCK STATE Para llevar un seguimiento del comportamiento de las instrucciones de memoria para cada uno de los bloques básicos ejecutados por la aplicación, presentamos la siguiente estructura. Tiene carácter temporal. Los campos definidos son los siguientes: Nivel de hit de TLB: es el último nivel de TLB al que se ha accedido. Este nivel incluirı́a la memoria principal como último nivel en caso de tener que acceder a la tabla de páginas del proceso. Nivel de hit de Cache: análogo al campo anterior, pero para el caso de la jerarquı́a de cache. Desglose de ciclos de la TLB y de la CACHE: aquı́ se van almacenando los ciclos requeridos por una instrucción al acceder a distintos niveles de ambas caches. Serán vectores de tamaño fijo dado por las macros KNC TLB LVLS y KNC CACHE LVLS. Fue declarada como parte de un mapa STL indexado por una clave que tiene dos campos <PC, acceso>. Véase el Listado 7.8 (pág. 59) con la declaración con los tipos de datos requeridos. Al realizar una instrucción un acceso a memoria, existe la posibilidad de que tenga que realizar más de un acceso. Es posible que el dato no esté alineado, teniendo que tocar dos lı́neas de cache. Véase la Figura 7.5 (pág. 58). En estos casos es necesario almacenar los accesos por separado ya que el primero puede ser un fallo y el segundo un acierto. Si este histograma se indexara solo por el PC de la instrucción, destruirı́amos la información del primer acceso al registrar el segundo. Esto también ocurre de forma totalmente distinta, en el caso de instrucciones tipo gather. Esta instrucción tiene la especialidad que no toca dos lı́neas porque el dato no está alineado, el interés radica en que se despliega en todo un conjunto de accesos independientes los cuales a su vez pueden estar desalineados. Al acceder a la estructura con el par <PC, acceso> el problema desaparece. En el mapa de ciclos el primer acceso tendrá el valor <PC,0>, y el resto de accesos, por numerosos que fueran, se indexarı́an incrementando el segundo campo. 7.3. NUEVAS ESTRUCTURAS Y CLASES 1 2 59 typedef std :: pair < UINT64 , UINT32 > BBL_ENTRY_KEY ; typedef map < BBL_ENTRY_KEY , BBL_STATE_t > BBL_STATE ; Listado 7.8: Mapa STL de los accesos a cache de las instrucciones de memoria de un BBL STATE Para hacer una fotografı́a al estado actual de la simulación se creó esta estructura. Engloba campos creados a partir de las estructuras anteriores BBL ST AT E y REG F ILE. Además almacenará los siguientes campos: Ciclo de issue: en el momento de la simulación, es importante conocer el ciclo en el que se inserta una instrucción en el pipeline. De este modo se podrá determinar cuándo empezarı́a la siguiente. Ciclo de write back: en el caso de instrucciones que escriban en un registro, ya sea el resultado de una operación o una carga de memoria, es importante saber en qué ciclo escribe de cara a entregarle ese dato a la tabla de registros. Estado de la memoria: desglose de ciclos de la última instrucción de memoria ejecutada. Se creó para abrir la posibilidad de lanzar una aplicación multi-thread. Última instrucción: información sobre la última instrucción que ocupó el pipeline. STATS La estadı́sticas son una parte fundamental. En esta clase se acumuları́an los datos necesarios para su tratamiento a posteriori. En la Sección 7.4 (pág. 62) se explica al detalle este posttratamiento. Las estadı́sticas se acumulan desde dos perspectivas: a nivel de instrucción y a nivel global. Los campos de que consta las estadı́sticas por instrucción son los siguientes: Bytes cargados: para propósitos de validación. Si sabemos exactamente cuántos elementos va a recorrer un bucle y de qué tipo son, podemos localizar el bloque del bucle en el informe final y validar que sus instrucciones de memoria han cargado el número de bytes esperados. Ciclos totales de parada: número acumulado de ciclos que una instrucción ha tenido que esperar, o ha tenido que hacer esperar, para entrar en el pipeline. Estas dos opciones se corresponden con dos modos: productor y consumidor. Se trata de decidir a qué instrucción culpar del retraso experimentado por una instrucción. Véase Sección 7.4 (pág. 62) para más detalles. Desglose de ciclos de parada (stalls): cada vez que una instrucción tiene que entrar más tarde al pipeline, ocasiona un incremento en el cómputo total de ciclos del programa impidiendo un IPC de 1. De cara a conocer qué recursos son los que están trabajando más para generar el dato motivo de la dependencia, es necesario almacenarlo. 1 2 3 4 5 6 typedef enum { INS_STALL_ISSUE , INS_STALL_NONVPU , INS_STALL_V_SCALAR , INS_STALL_V_VECTOR , 60 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR 7 8 INS_STALL_NUM } INS_STALL_t ; Listado 7.9: Tipos de stalls A los tipos de stalls presentados en el Listado 7.9 (pág. 59) se tendrı́an que añadir aquellos relacionados con los accesos a memoria. Sin embargo las paradas por memoria se almacenarán en vectores individuales que no necesitan el uso de un enumerado. Léase la descripción de todas las opciones a continuación: • issue: ciclos correspondientes a las instrucciones que lograron entrar al pipeline exactamente un ciclo después que la anterior. Es un motivo optimista. • nonvpu: ciclos que las instrucciones tuvieron que esperar para entrar al pipeline a causa de dependencias con instrucciones que no son ni escalares ni vectoriales. • vpu 1 y vpu N : ciclos a esperar a causa de dependencias con instrucciones escalares y vectoriales respectivamente. Desglose de ciclos de parada por memoria: en el caso de que el motivo de la parada fuera la memoria, se almacenará en vectores separados para el TLB y para el resto de la jerarquı́a de cache. Léase descripción detallada: • tlb: ciclos a esperar a causa de la traducción de direcciones. Este campo estarı́a a su vez desglosado según la jerarquı́a establecida para los tlbs. • cache: ciclos que se ha tenido que esperar en los niveles de cache que se hayan configurado, incluyendo la memoria principal, a causa tanto de dependencias de datos de memoria como por congelaciones ocurridas en el pipeline debido a fallos en el primer nivel de cache. Estos se tienen que resolver antes de que las siguientes instrucciones se sigan ejecutando. Las estadı́sticas por instrucción fueron definidas en un mapa cuyo ı́ndice es el PC de la instrucción. 1 typedef map < UINT64 , STATS_INS_s > STATS_INS_t ; Listado 7.10: Mapa STL de las estadı́sticas por instrucción Los campos de que consta las estadı́sticas globales son los siguientes: Sumatorio global de ciclos: número global de ciclos consumidos en el core por la aplicación. Desglose global de ciclos de parada (stalls y memoria): como en el caso del desglose por instrucción, en este caso son los datos globales del core. Al final se creó una estructura sencilla ambas estadı́sticas: 1 2 3 4 5 typedef struct { STATS_INS_t * stats_ins ; STATS_GLB_t * stats_glb ; } STATS ; Listado 7.11: Estadı́sticas por instrucción y globales 7.3. NUEVAS ESTRUCTURAS Y CLASES 61 CORE CORE es una clase que englobará todas las estructuras anteriores excepto el Footprint, puesto que ésta es una estructura global para la aplicación y no especı́fica del core. El resto sı́ forman R parte de la especificación detallada de un CORE del coprocesador IntelXeon PhiTM . La visión simplificada de la misma es la siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class CORE { STATE state ; STATS stats ; UINT32 coreID ; void I n s e r t I n P i p e l i n e ( UINT32 tid , BBL_STATE :: iterator ins ) ; void D i s t r i b u t e C y c l e s ( UINT32 tid , UINT64 storeLIP , COUNTER cycles , BBL_ENTRY_KEY culprit , bool regStall , bool memStall , BBL_ENTRY_KEY currentIP = make_pair (0 ,0) , INT32 regDependency = -1) ; void I n s e r t B r e a k d o w n S t a t s ( UINT32 tid , STATS_INS_t :: iterator , UINT32 cycles , BREAKDOWN_t breakdown , UINT32 index ) ; public : CORE ( UINT32 coreID , UINT32 nThreads ) ; ~ CORE () ; void Pipeline ( UINT32 tid , BBInfo * bbl ) ; string P r i n t Gl o b a l S t a t s ( UINT32 tid ) ; } Listado 7.12: Clase CORE Estas son las funciones destacadas: Pipeline: pública. Se describió a grosso modo en la Sección 7.1 (pág. 49). Una vez terminada la simulación de la cache, es la función encargada de organizar el pipeline para las instrucciones del bloque ya simulado en cuestión. InsertInPipeline: privada. Inserta una única instrucción en el pipeline. Será invocada desde la función Pipeline. DistributeCycles: privada. Una vez insertada una instrucción en el pipeline, se invocará para distribuir los ciclos que ha tardado en entrar al pipeline, ya hubiera habido stalls o no. InsertBreakdownStats: privada. Función genérica para determinar sobre qué vector se va a insertar el desglose de ciclos generado por una instrucción. 62 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Finalmente se creó un array de punteros a objetos de la clase CORE sobre el que se insertarı́an todos aquellos que se establecieran en la configuración. 1 extern CORE * CoreArray [ MA X_ EX P ER IM EN T S ][ M AX _ NU M_ TH R EA DS ]; Listado 7.13: Array de punteros a objetos CORE 7.4. Estadı́sticas Durante el procedimiento de construcción del pipeline, no solo se tendrá información del número total de ciclos, sino que para cada bloque tratado también se dispondrá especı́ficamente las razones por las que las instrucciones tuvieron que entrar más tarde al pipeline. En el primer caso habı́a que tener en cuenta que el último ciclo de la aplicación no tiene por qué ser el ciclo de fin de la última instrucción del programa. El motivo es la terminación en desorden. Respecto a la segunda información, es necesario recogerla para poder saber qué recursos son aquellos que impiden al programa ir más rápido. Una parte de la información sobre las instrucciones se encuentra insertada en el FOOTPRINT y otra en la rama de la estructura STATS dedicada a instrucciones exclusivamente. Ambas han sido ya expuestas en la Sección 7.3 (pág. 55). A la hora de almacenar los ciclos que ocasionan stalls se planteó la posibilidad de decidir sobre qué instrucción almacenarlos. Se trataba de determinar a cual culpabilizar. De cara a visualizar el cómputo total de ciclos esta decisión es indiferente, pero no lo es si quisiéramos consultar información de un bloque concreto. Las dos alternativas sobre las que responsabilizar son: Productor: si una instrucción depende de que otra genere un dato, será la productora sobre la que recaerı́an los ciclos. Consumidor: en este caso, culparı́amos a la instrucción dependiente por tener que necesitar un dato. La opción por defecto fue responsabilizar al consumidor. Finalizada por tanto la ejecución de la aplicación, y la recogida de las estadı́sticas correspondientes, es el turno de la función que realmente pone fin a toda la simulación. Si se recuerda la Sección 5.1 (pág. 31), en caso de que la ejecución fuera en modo buffer, al no haber más bloques que se encarguen de lanzar la última simulación de cache, es necesario que esta función se haga cargo de subsanarlo. Posteriormente, tanto para modo buffer como instrucción a instrucción, se lanzarı́a la construcción del pipeline y se recogerı́an las estadı́sticas. Finalmente, llegarı́a el momento de invocar a las funciones que se encargan de escribir toda la información recopilada en un fichero para su posterior tratamiento. Ni qué decir tiene que CMP$im ya almacenaba información acerca de los accesos y fallos de las instrucciones desglosadas por nivel de cache, entre otros. Esta información será aprovechada para completar nuestras estadı́sticas. El objetivo reside en crear una tabla en el informe de salida con toda la información desglosada por cada una de las regiones tratadas: funciones, bloques e instrucciones; y separadas por comas, ’,’. La cabecera contendrı́a los siguientes campos: Región: cada una de las entradas estarı́a clasificada con las palabras clave FUN, BEG, INS y GLB, significando función, bloque, instrucciones y global, respectivamente. La global será una región mostrada al final de la tabla con el acumulado de todos los datos. 7.4. ESTADÍSTICAS 63 Direcciones de comienzo y fin: en el caso de funciones y bloques, serı́an las direcciones de comienzo y fin. La dirección de comienzo coincide con el PC de la primera instrucción, y la dirección de fin coincide con la primera instrucción del bloque siguiente. El objetivo es que esta información incluya el tamaño de estas regiones sin tener que agregar más campos. En el caso de las instrucciones, aparecerı́a únicamente su PC, quedando el campo de dirección de fin vacı́o. Ejecuciones del bloque: solo se mostrará si la región es el bloque, quedando vacı́o para las regiones restantes. Indica el número de veces que se ha ejecutado el bloque. Número de instrucciones: para funciones y bloques, contendrá el número total de instrucciones ejecutadas. Número total de bytes: se corresponde con el total de bytes que han sido solicitados por las instrucciones de carga, load. Su finalidad es, entre otras, de validación, para comprobar que efectivamente el número de bytes cargados por un bloque particular es el esperado. Se mostrará para todas las regiones. Número total de ciclos: es un modo de tener una idea del número total de ciclos requeridos por todas las instrucciones sin necesidad de realizar cálculos adicionales. Accesos desglosados: esta información se encontraba ya disponible para cada instrucción, desglosada por nivel de cache. Se presentará pues para cada instrucción tal y como está ya, y se realizará el acumulado para mostrarlo por bloque y función. Fallos desglosados: es análogo a los accesos, pero mostrando los fallos. Ciclos desglosados: se corresponden con toda la retahı́la de razones explicadas al inicio de esta sección. En este punto, lo tenemos todo para visualizar la información. Sin embargo, si recordamos el punto sobre los bloques básicos de la Sección 5.2 (pág. 33), originalmente se almacenaban en una lista para no ralentizar la simulación. Es ahora cuando se tiene que trabajar sobre la lista para convertirla en un mapa con bloques únicos. Inicialmente, se lleva a cabo generando una worklist indexada por una clave de dos campos, <PC,PC>, correspondientes a la dirección de la primera instrucción y de la última. Véase la declaración en el Listado 7.14 (pág. 63). A continuación, se analizan los bloques de dos en dos, como se muestra en la Figura 7.6 (pág. 63) y la Figura 7.7 (pág. 64), para cortarlos y fusionarlos siempre que sea necesario. Una vez hecho esto, el proceso consiste en un bucle que se recorre mientras haya entradas (bloques) en la worklist. Para evitar problemas al tratar el último bloque, se introduce al final de la worklist uno ficticio con unas claves ficticias. A medida que se recorre la worklist, en caso de no haber ningún corte se elimina el primer bloque para introducirlo en el mapa de bloques únicos. Si se produce un corte, se modifican los bloques consecuentemente y se vuelve a iterar. Cuando la lista solo contiene el bloque ficticio se finaliza el proceso. 1 map < pair < UINT64 , UINT64 > , COUNTER > BBInfoMap ; Listado 7.14: Mapa STL de Bloques Básicos Figura 7.6: Bloques con ningún y un corte Finalmente, se escribirı́a toda la información en el informe final y se finalizarı́a el proceso. 64 CAPÍTULO 7. ADAPTACIÓN DEL SIMULADOR Figura 7.7: Bloques con 2 cortes 7.5. Invocación activando la funcionalidad vectorial La lı́nea de invocación que veı́amos en la Sección 5.3 (pág. 37) se incrementa en un knob con el que se activa la funcionalidad vectorial. La entrada de datos del knob nuevo es el fichero de configuración de las latencias de instrucciones que se presentaron en la Sección 7.2.3 (pág. 54). 1 2 3 4 5 6 7 8 9 10 11 pin -t cmpsim - cache DL1 :32:64:8:1 - cache UL2 :1024:64:16:1 - tlb DTLB :64:4096:8 - tlb DTLB2 :256:4096:8 - dl1lat 4 - l2lat 16 - dtlblat 0 - dtlb2lat 6 - inclusive 1 - threads 1 - dependencies inslat . conf -- <app > < input > Listado 7.15: Lı́nea para el lanzamiento de la simulación Capı́tulo 8 Estudio experimental En el Capı́tulo 6 (pág. 39) presentamos la caracterización inicial de los benchmarks seleccionados, desde los puntos de vista del indice de vectorización y el desglose de motivos determinados por el compilador para no vectorizar. Teniendo esto en cuenta este capı́tulo fue dividido en dos partes: resultados y diagnóstico. En la primera presentaremos gráficamente los resultados obteniR dos a partir de la simulación en el CMP$im modificado con las capacidades vectoriales del Intel Xeon PhiTM descrito en el Capı́tulo 7 (pág. 49). Estos resultados incluyen las ejecuciones escalares y vectoriales de todas las aplicaciones. Querı́amos conocer, tanto a nivel de instrucciones como de ciclos, cuán diferente es la versión vectorizada frente a aquella en la que no se activó la vectorización. El motivo reside en que esta comparativa proporciona un punto de vista adicional al ya visto con los ı́ndices de vectorización. Con esta información decidiremos entonces acerca de qué aplicaciones analizar en profundidad para detectar los focos donde se pueda explotar la vectorización en mayor medida. En la segunda parte del capı́tulo, el diagnóstico, dividiremos el análisis en dos partes: software y hardware. En el diagnóstico software se analizan los bloques básicos más ejecutados de la aplicación, el código fuente y el compilado. En el diagnóstico hardware se presentarán los resultados obtenidos al modificar la cache, de cara a comprobar qué efectos tendrı́a en aquellas aplicaciones que, pese a tener un buen ı́ndice de vectorización, se encuentran limitadas en su ejecución por los accesos a memoria. 8.1. Resultados 8.1.1. Polyhedron La Figura 8.1 (pág. 66) muestra la relación entre el número de instrucciones y de ciclos de las versiones vectorizada y no vectorizada de los benchmarks de Polyhedron. El eje de abscisas se corresponde con la relación de instrucciones ejecutadas de la versión vectorizada frente a la no vectorizada. El eje de ordenadas se corresponde con la relación de ciclos, obtenidos a través de la versión modificada de CMP$im, también entre las versiones vectorizada y no vectorizada. Además, se han dibujado segmentos de los siguientes tres colores: verde: marca los puntos de la gráfica en los que la relación de instrucciones para ambas versiones es igual a 1. rojo: análogamente al segmento verde, marca los puntos de la gráfica en los que la relación de ciclos para ambas versiones es igual a 1. 65 66 CAPÍTULO 8. ESTUDIO EXPERIMENTAL morado: indica los puntos donde las relaciones de instrucciones y ciclos sen han visto afectadas de forma equitativa. A continuación se explican en detalle las diferentes situaciones e implicaciones de cada punto en esta gráfica desde los puntos de vista de las instrucciones y los ciclos: Instrucciones == 1: linea verde. Se pueden dar dos circunstancias: o bien no se ha vectorizado, o bien sı́ se ha hecho pero la inclusión de instrucciones adicionales relacionadas con la vectorización ha hecho que el número total de instrucciones no disminuya. Es común ver a las aplicaciones que cumplen esto arremolinadas en el punto de intersección (1, 1). < 1: se ha conseguido vectorizar. Se encontrarı́an dentro del área cuadrada encerrada por los segmentos verde y rojo. > 1: se ha vectorizado pero, debido a comprobaciones o instrucciones relacionadas con la vectorización, el número de instrucciones ejecutadas ha resultado ser mayor. En la gráfica, estas aplicaciones estarı́an fuera del área cuadrada formada por los segmentos verde y rojo. Figura 8.1: Versión vectorizada vs no vectorizada de Polyhedron Ciclos == 1: linea roja. Hay dos circunstancias en relación a la relación de instrucciones vista: 1. relación de instrucciones == 1: si no hay mejora de instrucciones no la hay de ciclos. 2. relación de instrucciones < 1: pese al éxito de la vectorización, la aplicación está limitada por memoria. Las aplicaciones se encontrarı́an adyacentes al segmento rojo. < 1: de nuevo se producen dos circunstancias: 1. la reducción de instrucciones y de ciclos siguen más o menos la misma lı́nea. Aquellas próximas al segmento morado cumplen este patrón. 8.1. RESULTADOS 67 2. pese a que se hayan reducido el número de instrucciones, esta reducción no se ha traducido en una reducción de ciclos equivalente. Se encontrarı́an dentro del área encerrada por los segmentos morado y verde. > 1: la vectorización provocó un aumento de ciclos. Volviendo a Polyhedron, la caracterización de las aplicaciones tfft, fatigue, doduct, mdbx, nf e induct, las hacı́a destacar como posibles candidatas para su análisis dado alto porcentaje de instrucciones escalares frente a las vectoriales. Entre ellas, la Figura 8.1 (pág. 66) nos destaca fatigue y, como novedad, gas dyn. La primera porque al vectorizar se ha obtenido un 3 % más de instrucciones que han supuesto un 9 % más de ciclos. La segunda porque en su caracterización pasaba desapercibida entre el resto, mientras que aquı́ destaca frente a la versión escalar, con un 90 % menos de instrucciones ejecutadas y un 86 % menos de ciclos. Por otro lado, aplicaciones como channel, linpk y test fpu, que eran las que mejor ı́ndice de vectorización mostraban en su caracterización, en esta gráfica se muestran como un claro ejemplo de estar limitadas por la memoria. Se observa que la reducción de instrucciones no se traduce en la misma medida en la de ciclos. En el caso de channel, por ejemplo, es un 79 % menos de instrucciones frente a un 9 % menos de ciclos. El resto de aplicaciones se encuentran o bien distribuidas a lo largo del segmento morado, el cual es un buen resultado ya que indica que la vectorización ha funcionado, o bien aglomeradas en el punto (1, 1), donde la vectorización no ha dado buenos resultados. Figura 8.2: Ciclos desglosados de las aplicaciones de Polyhedron La Figura 8.2 (pág. 67), que muestra en forma de ciclos las razones por las que los IPCs de las aplicaciones son inferiores a 1, sirve para confirmar que las aplicaciones channel, linpk y test fpu, están claramente penalizadas por los continuos accesos a la memoria principal. Las denominaremos memory bound. tfft, pese a que en su caracterización presentaba el peor ı́ndice de vectorización, 0,07 %, resulta que también es memory bound, con lo que una posible mejora en la vectorización podrı́a no traducirse en una mejora de ciclos. Las mejoras para abordar esta aplicación residirı́an en encontrar una configuración de memoria adaptada a ella, para luego identificar las deficiencias en su código. En el caso de capacita, que también entra en esta categorı́a, es la única que presenta un perfil semejante, ya que tiene un 38 % de ciclos a consecuencia de fallos en el segundo nivel de TLB. En este caso, parece viable probar a ejecutarla modificando el número de lı́neas en el TLB de segundo nivel. Siguiendo la lı́nea de las aplicaciones memory bound, tenemos a gas dyn como caso particular. Pese a su excelente resultado respecto a la versión escalar, su IPC es 0,19. Esto se debe a los fallos 68 CAPÍTULO 8. ESTUDIO EXPERIMENTAL en DL1, que se traducen en un porcentaje alto de aciertos en UL2. Por muy buen uso de la unidad vectorial que haga, el rendimiento general de la aplicación no es acorde. Ya que los responsables son los fallos en la DL1 y no en la UL2, serı́a interesante de analizar. Al lado derecho de la gráfica tenemos aplicaciones como induct, doduc, aermod y fatigue, que tienen los mayores porcentajes de ciclos ocasionados por dependencias sobre instrucciones escalares. Si se aumentara el número de vectoriales que sustituyeran a las escalares, se reducirı́an estos ciclos. Es posible que, como efecto colateral, se trasladaran las dependencias entre escalares a las vectoriales. Serı́a una solución aceptable pues habrı́a mejorado el uso efectivo de la unidad vectorial del procesador, pudiéndose plantear soluciones de otra ı́ndole. De todos modos, hay que recordar que el compilador conseguı́a vectorizar un 67 % de bucles de doduc, por lo que no da mucho margen de maniobra. No es ası́ con induct, fatigue y aermod, que se situaban a la cola. La de más interés parece ser aermod al tener mas de 2500 bucles registrados. Finalmente tenemos a protein, aplicación con un 96 % de instrucciones enteras. Fue descartada de entrada como candidata a ser analizada debido a que los esfuerzos deberı́an centrarse en la reducción de instrucciones escalares. Como las enteras tienen una latencia de un ciclo por defecto, provocan que el porcentaje de instrucciones que entran correctamente en el pipeline aumente, incrementando entonces el IPC y situándola a la cabeza. Recuérdese que si una instrucción entraba correctamente en el pipeline, el ciclo se clasificaba con el nombre Issue. Sin embargo, dejando a un lado este detalle, el lı́mite para que se obtenga un resultado mejor son los accesos a DL1. En realidad estos accesos son en su mayorı́a aciertos en la DL1, ya que no se ve la participación ni de la UL2 ni de memoria principal. Por tanto, estos ciclos implican dependencias de otras instrucciones sobre los datos cargados por loads. Representan un 47 % del total de ciclos. Este tipo de aplicaciones serı́a mejor englobarla fuera de las memory bound para clasificarlas en su lugar como cache dependence bound. Las candidatas a un análisis más profundo, según los razonamientos arriba expuestos, son: aermod: su objetivo es simular el modelo de dispersión de aire ISCST2. induct: su propósito es generar la entrada de PSpice que sirva para el modelado de las propiedades electromagnéticas de baja frecuencia de un sistema de comunicaciones res-q. fatigue: sirve para modelar la fatiga de los metales dúctiles. gas dyn: destinada a resolver ecuaciones de continuidad de masa, momento y energı́a con el objetivo de modelar el flujo de un gas es una dimensión. 8.1.2. Mantevo La caracterización de las aplicaciones de Mantevo invitaba a examinar algunas de ellas como Cloverleaf, ya que su ı́ndice de vectorización ascendı́a a un 73 % de las instrucciones ejecutadas del programa. El resto de aplicaciones con un ı́ndice inferior al 40 % y con poco peso en cuanto a instrucciones escalares, no parecı́an ser candidatas de interés. Entre ellas, la que obtenı́a peores resultados era CoMD, con un 0,21 % de instrucciones vectoriales frente a un 30 % de escalares, datos que parecı́an concordar con el informe del compilador. La Figura 8.3 (pág. 69), que se presenta con la comparación entre las versiones escalar y vectorial, muestra un Cloverleaf que presumiblemente va a estar limitado por la memoria principal. La reducción de ciclos es de un 19 % frente al 81 % sobre las instrucciones. Este mismo comportamiento lo presentan HPCCG, miniFE y miniGhost. CoMD se reitera como candidata a ser analizada porque se encuentra fuera del área encerrada por los segmentos. 8.1. RESULTADOS 69 Figura 8.3: Versión vectorizada vs no vectorizada de Mantevo El desglose de ciclos de la Figura 8.4 (pág. 69) confirma que Cloverleaf, HPCCG, miniFE y miniGhost se engloban dentro del grupo memory bound. Por otro lado, pese a que en CoMD se da la circunstancia mencionada sobre que se encuentra fuera del área en la primera figura, significando que la versión vectorial ha sido peor que la escalar, presenta aquı́ un interesante porcentaje de dependencias con instrucciones escalares sobre las que se puede centrar el análisis. Esto reafirma una vez más su candidatura. Figura 8.4: Ciclos desglosados de las aplicaciones de Mantevo Finalmente vemos que miniXyce es una aplicación con un comportamiento análogo al manifestado por protein de Polyhedron. Su caracterización mostraba un 69 % de instrucciones enteras de latencia 1, que se traducen aquı́ en un gran porcentaje de ciclos Issue que incrementan el IPC falseando su apariencia de buena candidata. La única candidata de este benchmark para ser analizada en profundidad, será: CoMD: es un proxy para computación en aplicaciones tipo Molecular dynamics. Todas las aplicaciones de este tipo tienen caracterı́sticas compartidas, como son los parámetros x, y, z 70 CAPÍTULO 8. ESTUDIO EXPERIMENTAL de cada partı́cula, operaciones en las que unas partı́culas interaccionan con otras, etc. 8.1.3. Sequoia Sequoia presentaba ya dos candidaturas para realizar un diagnóstico software: SPhotmk y Crystalmkm. En la Figura 8.5 (pág. 70) Crystalmk presenta un comportamiento a ser analizado por su empeoramiento respecto a la versión escalar, que asciende a un 13 % de empeoramiento en cuanto a instrucciones y un 15 % en ciclos. IRSmk y UMTmk presumiblemente estarán limitadas por memoria, pese a tener unos ı́ndices de vectorización del 54 % y 24 %, respectivamente. Este hecho se ve corroborado en la Figura 8.6 (pág. 70). Figura 8.5: Versión vectorizada vs no vectorizada de Sequoia Figura 8.6: Ciclos desglosados de las aplicaciones de Sequoia En el caso de SPhotmk y Crystalmk presentan altos porcentajes de dependencias ocurridas por instrucciones escalares, por tanto siguen siendo candidatas: SPhotmk: consiste en un conjunto seleccionado de pequeñas porciones de código de otros 8.1. RESULTADOS 71 paquetes mayores. No realiza cálculos fı́sicos per se, sino que engloba procesos como un solucionador de sistemas lineales por Cholesky, bucles con divisiones, bucles con llamadas a funciones matemáticas de tipo built.in, etc. SPhotmk: sirve para medir el rendimiento de la CPU y comprobar que los resultados que proporciona son correctos. 8.1.4. NPB Las aplicaciones que inicialmente se consideraron candidatas fueron UA, IS, BT y LU. En éstas, el grado de instrucciones escalares era del 68, 35, 71 y 50 %, respectivamente. El hecho de que el número de instrucciones escalares sea grande ayuda a que, localizando los bloques a los que pertenecen, se puedan concentrar los esfuerzos en ayudar al compilador a vectorizarlos. Figura 8.7: Versión vectorizada vs no vectorizada de NPB En el área delimitada por los segmentos rojo y morado de la Figura 8.7 (pág. 71), se encuentran las aplicaciones limitadas por la memoria principal, más próximas al segmento rojo que al morado. En este benchmark, a diferencia de los anteriores, nos encontramos con muy pocas aplicaciones en el punto (1,1), donde figurarı́an aquellas cuyos resultados de la versión vectorial se asemejarı́an a la versión escalar. En cambio, la mayorı́a se encuentra limitada por memoria en mayor o menor medida. Entre las destacadas por la limitación de memoria se encuentran MG, con un 78 % de reducción de instrucciones frente al 26 % de mejora en ciclos; FT, con un 63 % de reducción frente al 29 % de ciclos, SP con un 55 % y 16 %, respectivamente, y CG con un 51 % de instrucciones reducidas frente al 5 % de ciclos. De las mencionadas, FT fue la que presentaba el mejor ı́ndice de vectorización, pese a ser memory bound, con un 52 % de instrucciones vectoriales ejecutadas frente al 2 % de escalares. EP, por el contrario, tenı́a un ı́ndice de vectorización que competı́a con el de instrucciones escalares (33 % y 30 %, respectivamente). Sin embargo, aquı́ es la que presenta una mayor mejora, teniendo un 65 % de reducción de instrucciones y 53 % de ciclos. En el otro extremo tenemos DC. Su ı́ndice de vectorización mostraba un 97 % de instrucciones enteras ejecutadas. Aquı́ figura como que el intento de vectorización no ha hecho sino empeorar el número de instrucciones en un 2 %. La relación de ciclos se ha mantenido constante, presumiblemente por el gran número de instrucciones enteras. Por este motivo, no merece la pena pararse a 72 CAPÍTULO 8. ESTUDIO EXPERIMENTAL analizar las razones de esta situación, cuando en general no vamos a poder aumentar el número de instrucciones vectoriales de la misma. Figura 8.8: Ciclos desglosados de las aplicaciones de NPB En la Figura 8.8 (pág. 72) los ciclos desglosados presentan una fuerte tendencia hacia la limitación provocada por los accesos a memoria principal. MG, CG, SP y UA encabezan esta tendencia. En el caso de UA es importante saber que la reducción de instrucciones respecto a la versión escalar es pequeña, 17 %. Por tanto, aunque fuera una candidata a mejorar, si la limitación por memoria está presente, mejorar el ı́ndice de vectorización no va a erradicar su perfil memory bound. La aplicación IS es interesante porque presenta muchos accesos a memoria por culpa de fallos en ambos niveles de TLB. Estos ciclos, junto a aquellos por fallos en la UL2, hacen un total del 75 % de ciclos de memoria. Por tanto, pese a que esta aplicación se presentaba como una posible candidata, serı́a necesario aplicar mejoras sobre la jerarquı́a de memoria en primer lugar. La estadı́stica de EP, con un 35 % de ciclos por dependencias de instrucciones vectoriales, confirma su ı́ndice de vectorización del 33 %. Igualmente, no se trata de algo positivo porque no se consigue un buen IPC. Esta aplicación entonces se clasificarı́a como dependence bound. Finalmente tenemos la aplicación DC, dominada por instrucciones enteras, la cual repite un comportamiento que viene siendo habitual en todos los bechmarks cuando se identifican aplicaciones principalmente enteras, esto es, tener el mejor IPC. Sin embargo, esta vez ni es tan alto ni se refleja en un gran porcentaje de ciclos Issue debido a los fallos de TLB y en la jerarquı́a de cache. Igual no la podrı́amos clasificar como memory bound, porque ya se sabı́a que parte de los ciclos de DL1 que engordan su barra son ocasionados por dependencias con instrucciones de carga de datos. Por ello, se la clasificarı́a como memory dependence bound. Tras todo este análisis, las aplicaciones seleccionadas son: BT: de Block Tridiagonal, es una aplicación que presenta un solucionador de sistemas no lineales de ecuaciones con derivadas parciales usando matrices de bloques tridiagonales. LU: de Lower-Upper, es una aplicación que tiene el mismo objetivo que BT, es decir, la resolución de un sistema, pero aquı́ es realizada aplicando el método de factorización LU por Gauss-Seidel. 8.1.5. SPEC fp Las aplicaciones de más interés mencionadas durante la caracterización fueron namd, povray y milc, porque presentaban un ı́ndice de vectorización muy pequeño: 1 %, 2 % y 2,4 %, respectiva- 8.1. RESULTADOS 73 mente. Predominaba en todas ellas el porcentaje de instrucciones escalares: 58 %, 32 % y 84 %. Figura 8.9: Versión vectorizada vs no vectorizada de SPEC fp La Figura 8.9 (pág. 73) nos muestra que lbm, teniendo el mejor ı́ndice de vectorización, ascendiendo este a un 76 %, no se ve acompañada por una mejora semejante en el número de ciclos. Se clasifica por tanto en el conjunto de aplicaciones memory bound. Independientemente de este hecho, es una ventaja que el único frente que haya que abordar sean los accesos a memoria, ya que la mejora en el número de instrucciones es de un 77 % y el ı́ndice de vectorización no deja lugar a dudas de que es una aplicación que hace un uso efectivo de los procesadores vectoriales. Otro caso es el de leslie3d, que muestra una mejora semejante tanto a nivel de instrucciones como de ciclos. En este caso el ı́ndice de vectorización ascendı́a solo al 33 %. Figura 8.10: Ciclos desglosados de las aplicaciones de SPEC fp 2006 Zeusmp y cactusADM son interesantes porque ambas tenı́an un ı́ndice de vectorización semejante: 35 % y 37 %, respectivamente. Sin embargo, zeusmp no mejora tanto respecto al número de ciclos (36 %), categorizándose como memory bound. Por su parte, cactusADM presenta una mejora del 62 %. La aplicación Wrf, comparándola con zeusmp por proximidad, pese a presentar un ı́ndice de vectorización del 24 % obtiene mejor resultados respecto a la versión escalar: 73 % de reducción de instrucciones, frente al 72 % de zeusmp, y 40 % de reducción de ciclos, frente al 74 CAPÍTULO 8. ESTUDIO EXPERIMENTAL 36 % de zeusmp. Esto significa, que pese a que una aplicación tenga peores resultados respecto al número de instrucciones vectorizadas, su cómputo global respecto a la versión no vectorial puede ser mejor. Gromacs, al igual que cactusADM, tiene incluso una mejor relación entre reducción de ciclos e instrucciones, siendo de un 53 y un 55 % respectivamente. En el extremo del punto (1, 1) se observa la concentración del resto de aplicaciones, algunas más limitadas por memoria y otras sin cambios aparentes, pero en general con mejoras discretas. Fuera de estas, se observan namd y povray sin mejora alguna. Igualmente, este empeoramiento es pequeño de cara al número de instrucciones, siendo de un 0,49 % en namd y un 0,46 % en povray. Lo más significativo es el empeoramiento de povray respecto al número de ciclos, siendo un aumento de un 4,7 %. En la Figura 8.10 (pág. 73), en el extremo de las aplicaciones que no están limitadas por memoria, nos encontramos a calculix. Su elevado IPC era de esperar gracias a las instrucciones enteras que lo componen, suponiendo éstas un 92 % de las instrucciones ejecutadas por la aplicación. De las aplicaciones que le siguen, soplex también es una aplicación mayoritariamente entera, con un 81 % de instrucciones de este tipo. Pese a esto, las instrucciones escalares y vectoriales que aportan un 3 % y un 16 % respectivamente al total, provocan dependencias que impiden obtener un porcentaje de ciclos Issue mayor. Finalmente, comentar que el interés principal de esta gráfica radica en centrarnos en si aquellas que aportan mayor número de ciclos por culpa de dependencias de instrucciones escalares, tienen caracterı́sticas destacadas por el resto de gráficas para determinar su candidatura. Es el caso por ejemplo de namd y povray, que se mencionaron en la caracterización. Por tanto, las seleccionadas para su análisis serán: namd: es una aplicación que simula grandes sistemas biomoleculares. povray: realiza un tratamiento sobre una imagen de dimensiones 1280x1024 que contiene un paisaje con objetos abstractos, usando para ello un filtro de ruido denominado Perlin, por su autor Ken Perlin. 8.2. Diagnóstico Software El diagnóstico es un proceso en el que, conociendo a priori el comportamiento de la aplicación, tanto de cara al perfil estático que nos proporciona el compilador como durante su ejecución, nos adentramos en su interior para determinar qué bloques básicos son los que están impidiendo un uso más efectivo de la unidad vectorial. Estos bloques básicos serán los que al final nos den las claves para plantear una posible solución con el interés de incrementar el número de instrucciones vectoriales ejecutadas por la aplicación. La forma de operar en esta sección será la siguiente: se tomarán cada uno de las aplicaciones seleccionadas en la Sección 8.1 (pág. 65) y, para cada una, se consultarán los bloques básicos más ejecutados, se analizarán las porciones de código fuente que se corresponden con ellos en la búsqueda de bucles potencialmente vectorizables, se consultará el resultado del compilador para con los bucles encontrados y se tratará de proponer alguna solución que pudiera ser viable para su mejora. Respecto a las soluciones propuestas, no era objetivo de este trabajo implementarlas, sino dejarlas como una posible guı́a de cara a trabajos futuros. 8.2. DIAGNÓSTICO SOFTWARE 8.2.1. 75 Polyhedron Las aplicaciones del benchmark Polyhedron seleccionadas para un análisis en profundidad son: Fatique, Induct, Aermod y Gas dyn. Fatigue Recordando las caracterı́sticas que se presentaron de Fatigue durante la caracterización, tenı́a 91 bucles, de los cuales un 48 % no se habı́a vectorizado. La razón considerada por el compilador era que la vectorización, pese a ser posible, resultaba ineficiente. PC Función Instrucciones % Ciclos % 1 0x407a50 perdida m mp generalized hookes law 3.583.528.433 23,0 5.074.024.833 8,59 2 0x40238e MAIN 3.328.267.995 21,3 8.311.166.827 14,1 Tabla 8.1: Bloques 1 y 2 de la lista de bloques básicos más ejecutados en Fatigue, Polyhedron La Tabla 8.1 (pág. 75) muestra los dos bucles más ejecutados de la aplicación. Los campos de esta tabla son: posición del bucle en la lista, PC de la primera instrucción del bloque, nombre de la función, instrucciones ejecutadas, porcentaje respecto al total, ciclos contabilizados y porcentaje respecto al total de la aplicación. El primero de los bloques se corresponde con una función denominada generalized hookes que se encuentra dentro del fichero fatigue.f90 de la aplicación. 1110 ! - - - - - - - - - - - - FUNCTION G E N E R A L I Z E D _ H O O K E S _ L A W - - - - - - - - - - - - - - - - - - - - - 1111 1112 function g e n e r a l i z e d _ h o o k e s _ l a w ( strain_tensor , lambda , mu ) result (←stress_tensor ) Con una primera mirada al código de la función, vemos que está formado por multitud de operaciones realizadas sobre los elementos de un vector de dimensión 6 y de una matriz de dimensiones 6x6. A modo de ejemplo se muestran algunas lı́neas: 1158 1159 1160 1161 1162 1163 g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (: ,:) g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (1 ,1) g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (1 ,2) g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (1 ,3) g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (2 ,1) g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (2 ,2) = = = = = = 0.0 _LONGreal lambda + 2.0 _LONGreal * mu lambda lambda lambda lambda + 2.0 _LONGreal * mu Además, existe un pequeño bucle dentro que el compilador no ha vectorizado por considerar dicha vectorización como ineficiente. 1183 1184 1185 1186 do i = 1 , 6 g e n e r a l i z e d _ s t r e s s _ v e c t o r ( i ) = dot_product ( g e n e r a l i z e d _ c o n s t i t u t i v e _ t e n s o r (i ,:) , g e n e r a l i z e d _ s t r a i n _ v e c t o r (:) ) end do Pese a que 6 son pocas vueltas para un bucle, y que el número de datos en punto flotante es pequeño como para llegar a llenar los 16 que puede albergar un registro vectorial de 512 bits, 76 CAPÍTULO 8. ESTUDIO EXPERIMENTAL se podrı́a utilizar el pragma vector always como primera opción de mejora. Al mismo tiempo, se podrı́an convertir todos las lı́neas que operan sobre los vectores y las matrices en bucles. Después de revisar el flujo de ejecución del programa, vemos que el bloque correspondiente a esta función solo es accedido desde un único call. La única función que invoca a ésta se denomina perdida. Pese a que no tiene bucles, si se comprueba el bloque desde el que se invoca, descubrimos que en realidad se ha hecho inline en la función main. Este bloque en cuestión se corresponde además con el tercero más ejecutado. Véase la Tabla 8.2 (pág. 76). 3 PC Función Instrucciones % Ciclos % 0x401eb4 MAIN 2.873.120.235 18,4 7.140.131.627 12,1 Tabla 8.2: Bloque 3 de la lista de bloques básicos más ejecutados en Fatigue, Polyhedron A continuación se muestra una pequeña porción de la función main donde se invoca a perdida: 1435 do n = 1 , n u m b e r _ o f _ s a m p l e _ p o i n t s 1436 if (. not . failed ( n ) ) then 1437 call perdida ( dt , lambda , mu , yield_stress , R_infinity , b , 1438 gamma , eta , plastic_strain_threshold , stress_tensor (: ,: , n ) , 1439 strain_tensor (: ,: , n ) , p l a s t i c _ s t r a i n _ t e n s o r (: ,: , n ) , 1440 s t r a i n _ r a t e _ t e n s o r (: ,: , n ) , a c c u m u l a t e d _ p l a s t i c _ s t r a i n ( n ) , 1441 b a c k _ s t r e s s _ t e n s o r (: ,: , n ) , i s o t r o p i c _ h a r d e n i n g _ s t r e s s ( n ) , 1442 damage ( n ) , failure_threshold , c r a c k _ c l o s u r e _ p a r a m e t e r ) 1443 end if 1444 end do Sin embargo, el compilador indica que el bucle anterior no es un bucle interno: fatigue.f90(1435): (col. 10) remark: loop was not vectorized: not inner loop Revisando de nuevo el código de la función perdida, encontramos operaciones realizadas sobre todos los elementos de una matriz de este modo: 927 928 929 ... 940 941 ... 950 ... 955 stress_tensor (: ,:) = (1.0 _LONGreal - damage ) * g e n e r a l i z e d _ h o o k e s _ l a w ( strain_tensor (: ,:) p l a s t i c _ s t r a i n _ t e n s o r (: ,:) , lambda , mu ) stress_tensor (: ,:) = (1.0 _LONGreal - c ra c k_ pa ra m et er * damage ) / (1.0 _LONGreal - damage ) * stress_tensor (: ,:) d e v i a t o r i c _ s t r e s s _ t e n s o r (: ,:) = stress_tensor (: ,:) d a m a g e d _ d e v _ s t r e s s _ t e n s o r (: ,:) = d e v i a t o r i c _ s t r e s s _ t e n s o r (: ,:) / (1.0 _LONGreal - cr ac k _p ar am e te r * damage ) El operador (:, :) del código anterior, usado para operar sobre los elementos de una matriz, es una manera corta de indicar que en realidad la operación tiene que ser realizada sobre todos los elementos. Es por tanto un bucle simplificado. Tiene entonces sentido que el compilador notificara que el bucle que engloba estas lı́neas no es un bucle interno. En concreto, el segundo bloque con mayor número de instrucciones mostrado en la Tabla 8.1 (pág. 75) se corresponde con las lı́neas siguientes: 950 ... 955 d e v i a t o r i c _ s t r e s s _ t e n s o r (: ,:) = stress_tensor (: ,:) d a m a g e d _ d e v _ s t r e s s _ t e n s o r (: ,:) = d e v i a t o r i c _ s t r e s s _ t e n s o r (: ,:) / (1.0 _LONGreal - cr ac k _p ar am e te r * damage ) 8.2. DIAGNÓSTICO SOFTWARE ... 960 961 962 77 e q u i v a l e n t _ s t r e s s = sqrt (1.5 _LONGreal * sum (( d a m a g e d _ d e v _ s t r e s s _ t e n s o r (: ,:) - b a c k _ s t r e s s _ t e n s o r (: ,:) ) **2) ) yie ld_funct ion = e q u i v a l e n t _ s t r e s s - i s o t r o p i c _ h a r d e n i n g _ s t r e s s - yield_stress Para todas ellas el compilador indica lo siguiente: fatigue.f90(950): (col. but seems inefficient fatigue.f90(955): (col. but seems inefficient fatigue.f90(955): (col. but seems inefficient fatigue.f90(960): (col. but seems inefficient fatigue.f90(960): (col. but seems inefficient 7) remark: loop was not vectorized: vectorization possible 7) remark: loop was not vectorized: vectorization possible 7) remark: loop was not vectorized: vectorization possible 46) remark: loop was not vectorized: vectorization possible 46) remark: loop was not vectorized: vectorization possible Hasta ahora hemos encontrado dos puntos de interés en este análisis. El primero fue en la función generalized hookes donde habı́a un pequeño bucle y varios accesos a vectores y matrices completamente desenrollados. Para el bucle, con el pragma vector always podrı́a ser suficiente. Sin embargo, en el caso de los accesos a los vectores y matrices habrı́a que reescribirlos para convertirlos en bucles. El segundo punto de interés estaba en la función perdida y sus accesos a vectores a través del operador (:,:). En este caso, la solución propuesta consistirı́a en convertir todos accesos en bucles y usar de nuevo el pragma vector always. Induct Induct es una aplicación con reparto bastante homogéneo de las instrucciones en punto flotante. Consta de un 42,3 % de instrucciones vectoriales y un 48,66 % de instrucciones escalares. Estos datos son chocantes si tenemos en cuenta que solo un 16 % de sus 246 bucles habı́a sido vectorizado. Aparte, comparativamente respecto a la versión escalar de la aplicación, seguı́a a gas dyn en el segmento gracias a la mejora tanto en instrucciones como en ciclos. Sin embargo, el 36 % de ciclos de penalización de que constaba su simulación debido a las dependencias entre instrucciones escalares no podı́a quedar sin investigar. PC Función 1 0x408799 2 0x407cc9 4 0x405f18 5 0x406b96 mqc m mp mutual quad cir coil mqc m mp mutual quad cir coil mqr m mp mutual quad rec coil mqr m mp mutual quad rec coil Instrucciones % Ciclos % ind 2.851.200.000 26,1 12.700.800.000 31,4 ind 2.851.200.000 26,1 12.700.814.660 31,4 ind 950.400.000 8,69 4.233.614.640 10,5 ind 950.400.000 8,69 3.952.800.000 9,8 Tabla 8.3: Bloques 1, 2, 4 y 5 de la lista de bloques básicos más ejecutados en Induct, Polyhedron La Tabla 8.3 (pág. 77) muestra cuatro de los cinco bloques básicos más ejecutados. Las funciones asociadas se encuentran en el único fichero de la aplicación, llamado induct.f90. Vemos que hay dos bloques en cada una de estas funciones con el mismo número de instrucciones ejecutados. La cifra correspondiente a los ciclos no tiene por qué coincidir necesariamente, ya que existe el 78 CAPÍTULO 8. ESTUDIO EXPERIMENTAL factor dependencias que ocasiona variaciones en la cuenta. Al comprobar el código de la aplicación correspondiente a las funciones quad cir coil y quad rec coil, vemos que prácticamente eran iguales salvo en alguna lı́nea aislada que no influı́a en lo que buscábamos. El esquema general de ambas funciones consistı́a en 2 grupos de 3 bucles encadenados cada una. Hacı́an un total de 4 grupos, cada uno de ellos asociado a uno de los cuatro bloques de la Tabla 8.3 (pág. 77). A continuación se presenta el contenido de uno de los bucles internos: 1772 1773 1774 1775 ... 1779 1780 1781 ... 1785 1786 1787 1788 1789 1790 1791 do k = 1 , 9 q_vector (1) = 0.5 _longreal * a * ( x2gauss ( k ) + 1.0 _longreal ) q_vector (2) = 0.25 _longreal * b2 * ( y2gauss ( k ) + 1.0 _longreal ) q_vector (3) = 0.0 _longreal rot_q_vector (1) = dot_product ( rotate_quad (1 ,:) , q_vector (:) ) rot_q_vector (2) = dot_product ( rotate_quad (2 ,:) , q_vector (:) ) rot_q_vector (3) = dot_product ( rotate_quad (3 ,:) , q_vector (:) ) numerator = w1gauss ( j ) * w2gauss ( k ) * dot_product ( coil_current_vec , curre nt_vect or ) denominator = sqrt ( dot_product ( rot_c_vector - rot_q_vector , rot_c_vector - rot_q_vector ) ) l12_upper = l12_upper + numerator / denominator end do El informe del compilador indica que se han podido vectorizar los bucles internos presentes en cada uno de los cuatro bloques: induct.f90(1772): (col. 15) remark: LOOP WAS VECTORIZED induct.f90(1772): (col. 15) remark: remainder loop was not vectorized: low trip count induct.f90(1660): (col. 15) remark: LOOP WAS VECTORIZED induct.f90(1660): (col. 15) remark: remainder loop was not vectorized: low trip count induct.f90(2077): (col. 15) remark: LOOP WAS VECTORIZED induct.f90(2077): (col. 15) remark: remainder loop was not vectorized: low trip count induct.f90(2220): (col. 15) remark: LOOP WAS VECTORIZED induct.f90(2220): (col. 15) remark: remainder loop was not vectorized: low trip count Los bloques denominados reminder son aquellos que se generan después del bucle principal en caso de que el compilador, al vectorizar, no haya podido abarcar todos los elementos del vector. En este caso el número de iteraciones que realiza, 9, no es lo suficientemente alto como para generar un bloque reminder adicional. Veamos un pequeño fragmento del primero de los bloques básicos: 1 2 3 4 5 6 7 8 9 vaddsd xmm28 , k0 , xmm26 , xmm13 vaddsd xmm27 , k0 , xmm25 , xmm14 vmulpd zmm14 , k0 , zmm19 , qword ptr [ rax *8+0 x696340 ]{ b } vaddsd xmm30 , k0 , xmm24 , xmm15 vbroadcastsd zmm31 , k0 , xmm28 vbroadcastsd zmm1 , k0 , xmm27 vbroadcastsd zmm15 , k0 , xmm30 vsubpd zmm0 , k0 , zmm31 , zmm22 vsubpd zmm29 , k0 , zmm1 , zmm21 8.2. DIAGNÓSTICO SOFTWARE 10 11 12 13 14 15 16 79 vmulpd zmm1 , k0 , zmm14 , zmm17 ... vmulsd xmm14 , k0 , xmm29 , xmm29 vfmadd213sd xmm13 , xmm13 , xmm14 vfmadd213sd xmm15 , xmm15 , xmm13 vsqrtsd xmm15 , xmm15 , xmm15 vdivsd xmm13 , xmm3 , xmm15 Está bien vectorizado y, como es lógico, necesita intercalar algunas instrucciones escalares a las vectoriales para realizar operaciones intermedias. Esto significa que induct funciona bien y esta haciendo un uso efectivo de la unidad vectorial. Sin embargo, esto no se traduce en un buen IPC debido a las dependencias de tanto las instrucciones escalares como las vectoriales y, dado que hay abundantes escalares en los bloques ya vectorizados, lo que parecı́a un buen intento de mejorar la aplicación centrándose en estas instrucciones, no ha dado el resultado esperado. Una posible solución que se deja planteada consiste en hacer uso de la directiva unroll para indicar al compilador que desenrolle por 2. Si consigue hacer un reordenamiento adecuado, se reducirı́an las dependencias y mejorarı́a el IPC. Aermod Aermod, como vimos en la caracterización, es una aplicación con una gran cantidad de bucles, 2597, y con un ı́ndice de vectorización muy pequeño, 5,59 %. Al analizar el listado de bloques básicos con mayor número de instrucciones, vemos que éstos le confieren un perfil plano. Esto significa que el reparto de instrucciones ejecutadas es muy homogéneo no destacando ningún bloque por encima de los demás. Por este motivo, se tomó la decisión de visitar uno a uno todos los bloques que proporcionaban información útil, clasificados por las funciones que los relacionaban entre ellos. Véase a continuación el desglose. PC Función Instrucciones % Ciclos % 1 0x5bd009 powf.L 3.437.413.941 4,85 4.054.989.375 2,32 2 0x5bd0b0 powf.L 3.261.136.303 4,60 5.552.965.738 3,18 Tabla 8.4: Bloques 1 y 2 más ejecutados de Aermod, Polyhedron La función powf.L que se muestra en la Tabla 8.4 (pág. 79), es en realidad una función de librerı́a. Para este caso, se buscó la región desde donde se estaban realizando la mayor cantidad de llamadas a la misma. Con una de las herramientas internas que permitı́a visualizar el control de flujo de cada una de las funciones de la aplicación, era posible hacer una consulta sobre el primero de los bloques de la función para conocer aquellos que contenı́an llamadas a la misma. El resultado se muestra en la tabla siguiente: PC Función Instrucción Ejecuciones % 0x5ba120 sigz call 0x5ba120 25.157.031 28,5 Pese a que sea la función sigz la que se indica, después de analizar el código más a fondo se llegó a una función llamada szsfcl, de la que se habı́a hecho inline en sigz. Se muestra a continuación un fragmento de la misma. El operador ** que se visualiza en la función es la potencia: x**n = xn . Las lı́neas no mostradas son comentarios y declaraciones de variables: 80 CAPÍTULO 8. ESTUDIO EXPERIMENTAL 45207 SUBROUTINE SZSFCL ( XARG ) ... 45256 IF ( UNSTAB . AND . SURFAC ) THEN 45257 SZSURF = BSUBC *(1.0 -10.*( CENTER / ZI ) ) ** ALPHA1 *( USTAR / UEFFD ) 45258 *( USTAR / UEFFD ) *( XARG * XARG / ABS ( OBULEN ) ) 45259 45260 ELSEIF ( STABLE ) THEN 45261 45262 SZSURF = ( RTOF2 / RTOFPI ) * USTAR *( XARG / UEFF ) *(1.0+0.7* XARG / OBULEN ) 45263 **( -0.333333) 45264 45265 ELSE 45266 SZSURF = 0.0 45267 45268 ENDIF 45269 45270 CONTINUE 45271 END Esta función no tiene bucles que tratar, ni siquiera un acceso especial a algún vector o matriz. Asimismo, la función desde donde se invoca, sigz, tampoco tiene. Si continuamos con este mecanismo de comprobar quién invoca a quién, encontramos llamadas a sigz desde funciones que no tienen bucles. Un ejemplo es una función llamada adisz, desde donde se invocaba 26 millones de veces. A ésta también se la llamaba desde una función llamada iblval un total de 18 millones de veces. Ninguna de estas piezas de código contenı́a bucles, con lo que en principio parecen simples bloques que, por el número de instrucciones que contienen y por las veces que se las invoca desde diferentes funciones, tienen todas las papeletas para aparecer en el top de bloques básicos. Volviendo a los 10 bloques más ejecutados, encontramos tres de ellos pertenecientes a la misma función: anyavg. Véase la Tabla 8.5 (pág. 80). PC Función Instrucciones % Ciclos % 3 0x50db17 anyavg 2.256.034.787 3,18 8.790.756.239 5,03 5 0x50da34 anyavg 1.867.063.272 2,63 5.367.809.396 3.07 10 0x50d94c anyavg 1.307.726.462 1,84 1.307.726.462 0,748 Tabla 8.5: Bloques 3, 5 y 10 más ejecutados de Aermod, Polyhedron Estos bloques ni son bucles ni están contenidos dentro de un bucle mayor. Al realizar la búsqueda de los bloques desde donde se invoca a ésta función, encontramos que los más significativos se encuentran contenidos en la función iblval. Dado que iblval se mencionó anteriormente porque aparecı́a como función raı́z desde donde se acababa invocando a powf.L y que, dentro de los 10 bloques más ejecutados aparecen otros que también forman parte de ella, se analizarán para ver si guardan alguna relación con anyavg o powf.L. Véanse en la Tabla 8.6 (pág. 80). PC Función Instrucciones % Ciclos % 7 0x50d260 iblval 1.397.144.910 1,97 2.395.105.560 1,37 9 0x50d2af iblval 1.394.701.126 1,97 2.390.916.216 1,37 Tabla 8.6: Bloques 7 y 9 más ejecutados de Aermod, Polyhedron Todos ellos son idénticos en contenido de instrucciones y, al comprobar las lı́neas de código a las que pertenecen, se descubre que son de una función llamada Locate. Esto significa que, de nuevo, se ha hecho inline de una función desde diferentes puntos. En este caso todos los puntos forman parte de iblval. Véase a continuación un fragmento de la función: 8.2. DIAGNÓSTICO SOFTWARE 81 21698 SUBROUTINE LOCATE ( PARRAY , LVLBLW , LVLABV , VALUE , NDXBLW ) ... 21729 IMPLICIT NONE 21730 21731 INTEGER LVLABV , LVLBLW , NDXBLW , JL , JM , JU 21732 REAL PARRAY ( LVLABV ) , VALUE ... 21740 JL = LVLBLW - 1 21741 JU = LVLABV + 1 21742 21743 DO WHILE ( ( JU - JL ) . GT .1 ) 21744 21745 JM = ( JU + JL ) /2 21746 21747 IF ( VALUE . GE . PARRAY ( JM ) ) THEN 21748 JL = JM 21749 ELSE 21750 JU = JM 21751 ENDIF 21752 21753 ENDDO 21754 21755 NDXBLW = JL 21756 21757 CONTINUE 21758 END Después de encontrar el primer bucle, comprobamos que no se ha vectorizado dando como razón el compilador que no es una estructura soportada: aermod.f90(21743): (col. 7) remark: loop was not vectorized: unsupported loop structure En el bucle se está accediendo al vector parray(jm) dentro de una condición. El ı́ndice, jm, se calcula en cada una de las iteraciones, convirtiendo la vectorización en una tarea complicada, ya que habrı́a que calcular previamente todos los ı́ndices para poder acceder a varios elementos simultáneamente. Esto se podrı́a traducir en una instrucción de tipo gather que cargara cada uno de los valores del vector según los ı́ndices que se hubieran calculado previamente. Sin embargo, el hecho de que dicho cálculo necesite del acceso al vector previamente, impide la vectorización. Relacionando esto con los dos últimos bloques mostrados de la función iblval en la Tabla 8.6 (pág. 80), encontramos que las llamadas a la función Locate están ı́ntimamente relacionadas con las llamadas a la función anyavg. En multitud de ocasiones aparecen bloques en donde, previo a llamar a anyavg, se divisa el bloque correspondiente al inline de Locate. Esta estructura de llamadas se repite 4 veces en todo el código, estando tres de ellas en el interior de la función iblval. Las siguientes lı́neas muestran la disposición de las llamadas que tienen más peso de entre todo el conjunto de invocaciones del código: 21285 21286 21287 21288 21289 21290 21291 CALL LOCATE ( GRIDHT ,1 , MXGLVL , ZHI , NDXBHI ) CALL LOCATE ( GRIDHT ,1 , MXGLVL , ZLO , NDXBLO ) NDXALO = NDXBLO + 1 CALL ANYAVG ( MXGLVL , GRIDHT , GRIDWS , ZLO , NDXALO , ZHI , NDXBHI , UEFF ) CALL ANYAVG ( MXGLVL , GRIDHT , GRIDSV , ZLO , NDXALO , ZHI , NDXBHI , SVEFF ) CALL ANYAVG ( MXGLVL , GRIDHT , GRIDSW , ZLO , NDXALO , ZHI , NDXBHI , SWEFF ) CALL ANYAVG ( MXGLVL , GRIDHT , GRIDTG , ZLO , NDXALO , ZHI , NDXBHI , TGEFF ) Viendo que se invoca en cuatro ocasiones a anyavg, se propone el siguiente planteamiento: indicar al compilador que haga inline de las llamadas a anyavg con el pragma inline y comprobar su comportamiento. Otra opción seria rescribir el código englobando las llamadas en un bucle. Finalmente, se analizará el bloque mostrado en la Tabla 8.7 (pág. 82). 82 CAPÍTULO 8. ESTUDIO EXPERIMENTAL 8 PC Función Instrucciones % Ciclos % 0x480379 sigz 1.395.399.168 1,97 2.946.185.353 1,69 Tabla 8.7: Bloque 8 más ejecutado de Aermod, Polyhedron La función sigz ya se ha mencionado previamente porque se invocaba desde la función iblval. Sin embargo, se descubrió que el bloque mostrado no se corresponde con esta función en concreto, sino con Locate, la cual también se ha mencionado y de la que sabemos que se hace inline en diferentes puntos del código. Dado que en todos los casos iblval resulta ser la raı́z común de todo el análisis, podrı́amos seguir analizando los niveles superiores en la cadena de llamadas, para averiguar qué funciones la invocan y cómo se comportan. Dado que es algo que venı́amos haciendo desde el inicio, el panorama se presenta negativo porque de entre las funciones que la invocan ninguna contenı́a bucles que justifiquen tantas llamadas. Veamos a continuación algunas de las funciones donde se invoca iblval : PC Función Instrucción Ejecuciones % 0x47bf6f pwidth call 0x50b890 9.248.524 46,1 0x47d009 plumef call 0x50b890 8.797.813 43,9 0x4a948f aercalc call 0x50b890 1.010.878 5,04 0x4a0bcf volcalc call 0x50b890 986.280 4,92 Al llegar a este punto se cesó el análisis, ya que se convirtió más en una cruzada de buscar posibles bucles de interés en un código de 51.885 lı́neas, que en un estudio de cómo mejorar la aplicación para hacer un uso más efectivo de la unidad vectorial. Igualmente, llama la atención que siendo una aplicación con más de 2.000 bucles, se den circunstancias como que los bloques de mayor peso en la aplicación no contuvieran más que un bucle o ninguno. Si bien la mayorı́a de bucles que contenı́a resultaron ser pequeños y vectorizables, muchos de ellos no se llegaban ni siquiera a ejecutar. Por ende, aermod es una aplicación que pese a haber sido candidata, no se ha obtenido el análisis que se esperaba de ella. Gas dyn Gas dyn es una aplicación que tenı́a buen ı́ndice de vectorización, 39,04 %, y la versión vectorial frente a la escalar daba los mejores resultados de todas las aplicaciones simuladas. Sin embargo el IPC era únicamente 0,19. Por este motivo se tomó la decisión de proceder a su análisis. Instrucciones Enteras Escalares Vectoriales Total Versión Escalar % Versión Vectorial % 7.815.080.079 23.91 1.329.097.003 38,71 23.909.735.894 23,89 764.150.080 22,26 1.051.987.585 3,2 1.340.360.535 39,04 32.776.803.558 3.433.607.618 Tabla 8.8: Desglose de instrucciones de las versiones escalar y vectorial de Gas dyn, Polyhedron El desglose de instrucciones de las versiones vectorial y escalar se muestra en la Tabla 8.8 (pág. 82). Nótese que la versión escalar contiene 1 millón de instrucciones vectoriales. Tras realizar comprobaciones varias con el equipo del compilador, no se llego a una conclusión factible del 8.2. DIAGNÓSTICO SOFTWARE 83 comportamiento del compilador. Dejándose este detalle a un lado, si dividimos los 24 millones de instrucciones escalares de la versión escalar entre 16, para intentar realizar una aproximación a mano alzada de la reducción, obtenemos 1,5M de instrucciones vectoriales. La versión vectorial contenı́a 1,3M de instrucciones vectoriales. El objetivo no era realizar ninguna validación, sino ver como, al utilizar registros vectoriales de 512 bits que pueden contener hasta 16 datos en coma flotante de 4 bytes, se ven reducidas las instrucciones escalares y aumentadas las vectoriales. 1 3 PC Función Instrucciones % Ciclos % 0x409375 0x409060 eos eos 718.555.926 234.311.715 20,9 6,82 3.435.096.196 4.271.972.900 22,4 27,9 Tabla 8.9: Bloques 1 y 3 más ejecutados de Gas dyn, Polyhedron En la Tabla 8.9 (pág. 83) se muestran dos de los bloques más ejecutados de la aplicación, formando ambos parte de la función eos. El primer bloque se corresponde con la lı́nea 410 del siguiente código y el tercer bloque con las lı́neas 407 a 409. 360 361 ... 390 391 392 ... 406 407 408 409 410 411 ... 432 SUBROUTINE EOS ( NODES , IENER , DENS , PRES , TEMP , GAMMA , CS , SHEAT , & CGAMMA , WT ) & INTEGER NODES , I REAL SHEAT , CGAMMA , WT REAL , DIMENSION ( NODES ) :: IENER , DENS , PRES , TEMP , GAMMA , CS IF ( SHEAT >0.0 . AND . CGAMMA >0.0) THEN TEMP (: NODES ) = IENER (: NODES ) / SHEAT PRES (: NODES ) = ( CGAMMA - 1.0) * DENS (: NODES ) * IENER (: NODES ) GAMMA (: NODES ) = CGAMMA CS (: NODES ) = SQRT ( CGAMMA * PRES (: NODES ) / DENS (: NODES ) ) ELSE ENDIF Pese a que no veamos aparentemente un bucle, la utilización de :NODES para acceder a un vector indica implı́citamente que se va a realizar la operación a todos los elementos del vector, entendiéndolo el compilador como si fueran varios bucles uno a continuación del otro. Esta caracterı́stica R Cilk TM Plus y se utiliza tanto en el compilador ICC como IFORT[inte][intd]. se denomina Intel Al comprobar la compilación de esta porción de código, se aprecia que crean bloques prólogo, normal y epı́logo para las lı́neas 407 a 409 por un lado, y para la lı́nea 410 por otro. Para las lı́neas 407 a 409 creó un fused loop, esto es, fusionó el acceso a los vectores TEMP, PRES y GAMMA en un mismo bloque. Las lı́neas del informe del compilador son las siguientes: gas gas gas gas dyn.f90(410): dyn.f90(410): dyn.f90(410): dyn.f90(410): (col. (col. (col. (col. 11) 11) 11) 11) remark: remark: remark: remark: unroll factor set to 2 LOOP WAS VECTORIZED PEEL LOOP WAS VECTORIZED REMAINDER LOOP WAS VECTORIZED gas dyn.f90(407): (col. 11) remark: FUSED LOOP WAS VECTORIZED gas dyn.f90(407): (col. 11) remark: PEEL LOOP WAS VECTORIZED gas dyn.f90(407): (col. 11) remark: REMAINDER LOOP WAS VECTORIZED El hecho de que el compilador este generando código separado para los tres primeros vectores por un lado y para el último por otro, es una de las raı́ces del problema que estábamos viendo en la Sección 8.1.1 (pág. 65), sobre la gran cantidad de ciclos ocasionados por los accesos a la UL2. En la lı́nea 410 de la función se está accediendo a los vectores PRES y DENS después de haber sido accedidos en la lı́nea 408. Como la 410 tiene que trabajar con ellos, se traduce en que la vectorización no hace un buen aprovechamiento de la localidad temporal de la cache, en 84 CAPÍTULO 8. ESTUDIO EXPERIMENTAL detrimento del rendimiento global de la aplicación. La solución, que en este caso sı́ se implemento debido a su sencillez, consistió en rescribir el código transformando los accesos a los bucles usando :nodes por un bucle simple 1..n, manteniendo el desenrollamiento en factor 2 que generaba el compilador. Véase a continuación el código resultante y consúltense la directivas de Fortran en la Sección 4.5 (pág. 29). 411 ! DIR$ UNROLL (2) 412 DO 10 I = 1 , NODES 413 TEMP ( I ) = IENER ( I ) / SHEAT 414 PRES ( I ) = ( CGAMMA - 1.0) * DENS ( I ) * IENER ( I ) 415 GAMMA ( I ) = CGAMMA 416 CS ( I ) = SQRT ( CGAMMA * PRES ( I ) / DENS ( I ) ) 417 10 CONTINUE Al convertir el acceso a los vectores en un bucle explı́cito que itera uno a uno sobre todos los elementos, el compilador no generará dos secciones separadas para el bucle. Se llevó a la práctica y se obtuvieron los siguientes resultados de la Figura 8.11 (pág. 84) y el mensaje del compilador siguiente: gas gas gas gas dyn.f90(412): dyn.f90(412): dyn.f90(412): dyn.f90(412): (col. (col. (col. (col. 14) 14) 14) 14) remark: remark: remark: remark: unroll factor set to 2 PARTIAL LOOP WAS VECTORIZED PEEL LOOP WAS VECTORIZED REMAINDER LOOP WAS VECTORIZED Figura 8.11: Comparación entre las versiones :nodes y do de gas dyn En primer lugar se aprecia que la lı́nea del código a la que hace referencia es únicamente la 412 de inicio del bucle, aparte de que genera solamente tres bloques. En la Figura 8.11 (pág. 84) se presenta una reducción del 6,51 % en el total de ciclos. Esta reducción es debido a la disminución de accesos en la UL2 en un 16,09 %. Ahora sı́ se está realizando un aprovechamiento de la localidad temporal, mejorando por tanto el rendimiento de la aplicación. 8.2.2. Mantevo La aplicación del benchmark Mantevo seleccionada para un análisis en profundidad es CoMD. 8.2. DIAGNÓSTICO SOFTWARE 85 CoMD La caracterización y los resultados obtenidos por el simulador CMP$im modificado, muestran a CoMD como una aplicación complicada de abordar. Su 0,22 % de instrucciones vectoriales y su 30,43 % de instrucciones escalares da poco margen para realizar cambios. Aparte, el resultado de la comparación entre las versiones escalares y vectoriales mostraban que vectorizar la aplicación no servı́a sino para empeorar el rendimiento de la misma tanto en número de instrucciones ejecutadas como en ciclos consumidos. Estudiando los bloques con más peso en el programa, encontramos que la sección de código fundamental en donde se está realizando la mayor parte del cómputo son los siguientes bucles encadenados: 188 for ( ioff = ibox * MAXATOMS , ii =0; ii < nIBox ; ii ++ , ioff ++) { /* loop over atoms in ←ibox */ 189 int joff ; 191 s - > stress -= s - > p [ ioff ][0]* s - > p [ ioff ][0]/ s - > mass [ ioff ]; 192 int i = s - > id [ ioff ]; /* the ij - th atom in ibox */ 193 for ( joff = MAXATOMS * jbox , ij =0; ij < nJBox ; ij ++ , joff ++) { /* loop over atoms ←in ibox */ 194 int m ; 195 real_t dr [3]; 196 int j = s - > id [ joff ]; /* the ij - th atom in ibox */ 197 if ( j <= i ) continue ; 198 r2 = 0.0; 199 for ( m =0; m <3; m ++) { 200 dr [ m ] = drbox [ m ]+ s - > r [ ioff ][ m ] -s - > r [ joff ][ m ]; 201 r2 += dr [ m ]* dr [ m ]; 202 } 203 204 if ( r2 > r2cut ) continue ; ... 212 r2 =( real_t ) 1.0/ r2 ; 214 r6 = ( r2 * r2 * r2 ) ; 216 s - > f [ ioff ][3] += 0.5* r6 *( s6 * r6 - 1.0) ; 217 s - > f [ joff ][3] += 0.5* r6 *( s6 * r6 - 1.0) ; 218 etot += r6 *( s6 * r6 - 1.0) - eshift ; ... 221 fr = - 4.0* epsilon * s6 * r6 * r2 *(12.0* s6 * r6 - 6.0) ; 222 for ( m =0; m <3; m ++) { 223 s - > f [ ioff ][ m ] += dr [ m ]* fr ; 224 s - > f [ joff ][ m ] -= dr [ m ]* fr ; 225 } 226 s - > stress += 1.0* fr * dr [0]* dr [0]; ... 232 } /* loop over atoms in jbox */ 233 } /* loop over atoms in ibox */ Estos bucles se encargan de computar la interacción entre los átomos dentro de contenedores denominados boxes. El recorrido sobre todos los boxes se realiza con otros dos bucles encadenados que no se visualizan aquı́ pero que engloban a estos, haciendo un total de 4 bucles. El contenido completo de esta sección de código se puede consultar en el fichero ljForce.c de la aplicación. Los diversos bloques generados por el compilador presentan en su mayorı́a instrucciones enteras intercaladas con escalares, como por ejemplo el siguiente: 1 2 3 4 5 6 7 8 9 10 mov rbx , qword ptr [ rbp +0 x58 ] vaddss xmm8 , xmm3 , dword ptr [ rbx + r8 *1+0 x4 ] vaddss xmm1 , xmm4 , dword ptr [ rbx + r8 *1] vaddss xmm9 , xmm2 , dword ptr [ rbx + r8 *1+0 x8 ] lea r10 , ptr [ rdx + rbx *1] vsubss xmm8 , xmm8 , dword ptr [ r10 + r14 *1+0 x4 ] vsubss xmm1 , xmm1 , dword ptr [ r10 + r14 *1] vsubss xmm9 , xmm9 , dword ptr [ r10 + r14 *1+0 x8 ] vmulss xmm11 , xmm8 , xmm8 vfmadd231ss xmm11 , xmm1 , xmm1 86 11 12 13 14 CAPÍTULO 8. ESTUDIO EXPERIMENTAL vfmadd231ss xmm11 , xmm9 , xmm9 vcmpss k0 , k0 , xmm11 , xmm17 , 0 xe kortestw k0 , k0 jnz 0 x4049ab El problema que encuentra el compilador para vectorizar el código, son las dependencias entre iteraciones: ljForce.c(193): (col. 3) remark: loop was not vectorized: existence of vector dependence Estas dependencias se producen porque los boxes que se tratan podrı́an coincidir, es decir, cuando ibox == jbox. En este caso las variables ioff y joff se iniciarı́an con el mismo valor y el compilador lo está detectando como una dependencia. Una idea para solucionarlo radicarı́a en rescribir el código generando dos situaciones separadas: una en la que ibox fuera igual a jbox, en cuyo caso seguirı́a sin vectorizar porque se seguirı́an produciendo las mismas dependencias, y un else donde incluyéramos los pragmas ivdep y vector always, ya que sabrı́amos que no se producirı́an dependencias y es seguro usarlas. El esquema general serı́a el siguiente: if ( ibox == jbox ) for ( ioff = ibox * MAXATOMS , ii =0; ii < nIBox ; ii ++ , ioff ++) { /* loop over atoms in ibox */ ... for ( joff = MAXATOMS * ibox , ij =0; ij < nIBox ; ij ++ , joff ++) { /* loop over atoms in ibox */ ... else # pragma ivdep # pragma vector always for ( ioff = ibox * MAXATOMS , ii =0; ii < nIBox ; ii ++ , ioff ++) { /* loop over atoms in ibox */ ... for ( joff = MAXATOMS * jbox , ij =0; ij < nJBox ; ij ++ , joff ++) { /* loop over atoms in ibox */ ... A la hora de aplicar la solución, es necesario tener en cuenta que la aplicación no ofrece muchas más oportunidades para vectorizar. Cuando tratamos con aplicaciones como esta en la que más de la mitad del código está formado por instrucciones enteras, 69,35 %, las versiones escalares y vectoriales serán muy semejantes y el uso de la unidad vectorial, pese a que puede mejorar, no lo hará mucho. 8.2.3. Sequoia Las aplicaciones del benchmark Sequoia seleccionadas para un análisis en profundidad son: Crystalmk y SPhotmk. Crystalmk La caracterización mostraba que el 46,7 % de las escalares, frente al 5,8 % de las vectoriales, era un porcentaje interesante sobre el que centrarse de cara a su reducción. Además, en la Sección 8.1.3 (pág. 70) se vio que la versión vectorial respecto a la escalar no habı́a obtenido buenos resultados, ya que la relación de instrucciones y ciclos era de 1,13 y 1,15 % respectivamente. Por tanto, después de analizar el conjunto de bloques básicos más ejecutados, la principal diferencia entre ambas 8.2. DIAGNÓSTICO SOFTWARE 87 versiones radica en el procedimiento Crystal div del fichero Crystal div.c. Véase a continuación los bucles de las lı́neas 43, 49, 53, 61 y 65 del código de la función: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void Crystal_div ( int nSlip , double deltaTime , double slipRate [ M S _ X T A L _ N S L I P _ M A X ] , double dSlipRate [ M S _ X T A L _ N S L I P _ M A X ] , double tau [ M S _ X T A L _ N S L I P _ M A X ] , double tauc [ M S _ X T A L _ N S L I P _ M A X ] , double rhs [ M S _ X T A L _ N S L I P _ M A X ] , double dtcdgd [ M S _ X T A L _ N S L I P _ M A X ][ M S _ X T A L _ N S L I P _ M A X ] , double dtdg [ M S _ X T A L _ N S L I P _ M A X ][ M S _ X T A L _ N S L I P _ M A X ] , double matrix [ M S _ X T A L _ N S L I P _ M A X ][ M S _ X T A L _ N S L I P _ M A X ]) { double double double double double bor_array [ M S _ X T A L _ N S L I P _ M A X ]; sgn [ M S _ X T A L _ N S L I P _ M A X ]; rateFact [ M S _ X T A L _ N S L I P _ M A X ]; tauN [ M S _ X T A L _ N S L I P _ M A X ]; err [ M S _ X T A L _ N S L I P _ M A X ]; double double double double double rate_offset tauA tauH rate_exp bor_s_tmp = = = = = 1. e -6; 30.; 1.2; 0.01; 0.0; int n = 0; int m = 0; for ( n = 0; n < nSlip ; n ++) { sgn [ n ] = 1.0; rateFact [ n ] = 0.9 + (0.2 * n ) / M S _ X T A L _ N S L I P _ M A X ; } // ---- M S _ X t a l _ P o w e r T a y for ( n = 0; n < nSlip ; n ++) { bor_array [ n ] = 1 / ( slipRate [ n ]* sgn [ n ] + rate_offset ) ; } for ( n = 0; n < nSlip ; n ++) { tau [ n ] = tauA * rateFact [ n ] * sgn [ n ]; for ( m = 0; m < nSlip ; m ++) dtcdgd [ n ][ m ] = tauH * deltaTime * rateFact [ n ]; dtcdgd [ n ][ n ] += tau [ n ] * rate_exp * sgn [ n ] * bor_array [ n ]; } // ----- M S _ X t a l _ S l i p R a t e C a l c for ( n = 0; n < nSlip ; n ++) { bor_array [ n ] = 1/ dtcdgd [ n ][ n ]; } for ( n = 0; n < nSlip ; n ++) { tauN [ n ] = tau [ n ]; for ( m = 0; m < nSlip ; m ++) { bor_s_tmp = dtdg [ n ][ m ]* deltaTime ; tauN [ n ] += bor_s_tmp * dSlipRate [ m ] ; matrix [ n ][ m ] = ( - bor_s_tmp + dtcdgd [ n ][ m ]) * bor_array [ n ]; } err [ n ] = tauN [ n ] - tauc [ n ]; rhs [ n ] = err [ n ] * bor_array [ n ]; } } En la versión escalar, cuando el compilador genera el código para este procedimiento, identifica que todos los bucles iteran con la misma variable y por tanto condensa todas las operaciones en 88 CAPÍTULO 8. ESTUDIO EXPERIMENTAL el mismo bucle. Ası́ se consigue eliminar código de control que hubiese estado repetido en cada uno de los bucles. El bloque generado lo denomina fused loop y consta de 1.173 instrucciones. En la versión vectorial una parte de este bucle es vectorizada mientras que otra no, ocasionando que las instrucciones del bloque escalar ahora se repartan en tres bloques diferentes. El bloque más grande de los tres se corresponde con los bucles de las lı́neas 61 y 65. En el Capı́tulo 6 (pág. 39) comprobamos lo habitual de la situación en la que el compilador intenta vectorizar el bucle interno. Sabemos que si lo consigue indicará que el bucle externo no se ha vectorizado por no ser un bucle interno, not inner loop. En el caso del bucle de la lı́nea 65, que contiene un bucle interno que empieza en la lı́nea 67, ocurrı́a algo distinto. El compilador estaba intentando vectorizar desde el bucle externo: Crystal div.c(65): (col. 4) remark: loop was not vectorized: existence of vector dependence Esta dependencia era de esperar porque en el bucle interno se realizan tantos cálculos sobre el elemento n del vector tauN como indica la variable nSlip, antes de volver a ser utilizado en el bucle externo. La solución consistirı́a en indicar al compilador que vectorice el bucle interno con el pragma vector always, puesto que dentro del bucle interno no hay dependencias. SPhotmk El ı́ndice de vectorización de SPhotmk era bajo, 5,56 %, frente a un 50,5 % de instrucciones escalares. La comparativa entre las versiones escalar y vectorial mostraba que no se obtenı́a ni reducción de instrucciones ni de ciclos. En la Tabla 8.10 (pág. 88) figuran los bloques con mayor peso de cómputo. Entre ellos encontramos los de la librerı́a logaritmo pertenecientes a la función log.L. Cuando parte de los bloques más ejecutados provienen de librerı́as como ésta, un modo de intentar buscar una solución para vectorizar consistirı́a en conseguir que el compilador invoque la función de la librerı́a adaptada al cálculo sobre vectores, siempre y cuando estuviera disponible. 1 2 3 4 PC Función Instrucciones % Ciclos % 0x46dda3 0x405670 0x40581b 0x46dd20 log.L execute execute log.L 7.772.068 5.913.530 5.406.656 4.054.992 7,23 5,50 5,03 3,77 12.145.985 8.278.942 11.602.509 5.575.614 4,39 2,99 4,19 2,02 Tabla 8.10: Bloques 1, 2, 3 y 4 más ejecutados de SPhotmk, Sequoia Los bloques básicos situados en las posiciones 2 y 3 se corresponden con las funciones ranfmodmult del fichero random.f y execute del fichero execute.f respectivamente. Para ambos ficheros se presentan dos pequeños extracto a continuación: random . f 338 ... 352 ... 361 362 363 364 365 366 367 subroutine ranfmodmult ( A , B , C ) dimension A ( IRandNumSize ) , B ( IRandNumSize ) , C ( IRandNumSize ) a1 a2 a3 b1 b2 b3 j1 = = = = = = = A (1) A (2) A (3) B (1) B (2) B (3) a1 * b1 8.2. DIAGNÓSTICO SOFTWARE 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 89 j2 = a1 * b2 + a2 * b1 j3 = a1 * b3 + a2 * b2 + a3 * b1 j4 = a1 * B ( 4 ) + a2 * b3 + a3 * b2 + A ( 4 ) * b1 k1 k2 k3 k4 = = = = j1 j2 + k1 / 4096 j3 + k2 / 4096 j4 + k3 / 4096 C( C( C( C( 1 2 3 4 ) ) ) ) = = = = mod ( mod ( mod ( mod ( k1 , k2 , k3 , k4 , 4096 4096 4096 4096 ) ) ) ) return end execute . f 372 373 dist = - log ( ranf ( mySeed ) ) / sig ( ir , ig ) dcen = ( tcen - age ) * 2.99793 d10 ! distance to collision ! distance to census El fichero principal es excecute.f. Este contiene una serie de bucles encadenados que abarcan la mayor parte de la aplicación. Un código de estas caracterı́sticas dificulta la vectorización porque para todos los bucles el compilador indica que no se ha vectorizado ninguno por no ser bucles internos. Cuando al final llega al bucle más interno, resulta que no lo vectoriza debido a la existencia de dependencias. Éstas fueron muy difı́ciles de identificar: execute.f(271): dependence execute.f(268): execute.f(193): execute.f(154): execute.f(103): (col. 19) remark: loop was not vectorized: existence of vector (col. (col. (col. (col. 16) 16) 13) 10) remark: remark: remark: remark: loop loop loop loop was was was was not not not not vectorized: vectorized: vectorized: vectorized: not not not not inner inner inner inner loop loop loop loop Las dos lı́neas de código presentadas de excecute.f se encuentran en el interior del bucle más externo de dos bucles anidados. Contienen las funciones log y ranf. La función ranf es la encargada de invocar a la función ranfmodmult que mencionamos previamente. Por tanto, estas dos lı́neas son responsables de que los bloques más ejecutados sean los mostrados en el top de bloques básicos. La búsqueda de soluciones se dificulta cuando todos los datos tratados en la función ranfmodmult resultan ser enteros. Esto viene a decir que uno de los bloques con mayor número de instrucciones ejecutadas en el programa, participa dentro del 44 % de instrucciones enteras presentes en la aplicación. Por estos motivos, para mejorarla no basta simplemente con modificar ligeramente el código o incluir pragmas en los bucles más externos. En este caso se propone hacer un estudio más exhaustivo del código y proceder a reescribirlo en la mayor medida posible. 8.2.4. NPB Las aplicaciones del benchmark NPB seleccionadas para un análisis en profundidad son: BT y UT. BT La aplicación BT fue seleccionada por su elevado porcentaje de instrucciones escalares, 71,85 %. En la Tabla 8.11 (pág. 90) se encuentra el primer y único bloque básico de los bloques más 90 CAPÍTULO 8. ESTUDIO EXPERIMENTAL ejecutados que se va a analizar. Es un bloque importante porque supone un 56.2 % del montante total de instrucciones de la aplicación. 1 PC Función Instrucciones % Ciclos % 0x4060c0 binvcrhs 4.134.959.136 56,2 14.866.089.846 26,5 Tabla 8.11: Bloque 1 de los más ejecutados de BT, NPB El código generado por el compilador está formado por 6 instrucciones enteras, 596 escalares y 14 vectoriales, de las cuales solo 3 utilizan vectores de 512 bits. Su código pertenece enteramente a una función denominada binvcrhs que se encuentra en el fichero solve subs.f. Este fichero recoge todas las funciones de interés que se invocan desde el código contenido en los ficheros x solve vec.f, y solve vec.f y z solve vec.f para la versión vectorizada, y desde x solve.f, y solve.f y z solve.f para la no vectorizada. La peculiaridad de estos ficheros es que en las versiones vectorizadas los bucles que contienen las llamadas a binvcrhs traen ya el pragma ivdep incorporado. Véase en el fragmento de código siguiente: 344 ! dir$ ivdep 345 do i = 1 , grid_points (1) -2 346 call binvcrhs ( lhs (1 ,1 , bb ,i ,0) , 347 > lhs (1 ,1 , cc ,i ,0) , 348 > rhs (1 ,i ,j ,0) ) 349 enddo Sin embargo, no se obtiene el resultado esperado por parte del compilador: x solve vec.f(347): (col. 18) remark: vectorization support: call to function binvcrhs cannot be vectorized x solve vec.f(346): (col. 10) remark: loop was not vectorized: existence of vector dependence Analizando el código de la función vemos que no tiene ningún bucle sobre el que trabajar. El único bucle es el que aparece en el fragmento de código anterior desde el que se realiza la llamada. Véase la siguiente porción de la función binvcrhs: 206 ... 218 219 ... 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 subroutine binvcrhs ( lhs ,c , r ) dimension lhs (5 ,5) double precision c (5 ,5) , r (5) pivot = 1.00 d0 / lhs (1 ,1) lhs (1 ,2) = lhs (1 ,2) * pivot lhs (1 ,3) = lhs (1 ,3) * pivot lhs (1 ,4) = lhs (1 ,4) * pivot lhs (1 ,5) = lhs (1 ,5) * pivot c (1 ,1) = c (1 ,1) * pivot c (1 ,2) = c (1 ,2) * pivot c (1 ,3) = c (1 ,3) * pivot c (1 ,4) = c (1 ,4) * pivot c (1 ,5) = c (1 ,5) * pivot r (1) = r (1) * pivot coeff = lhs (2 ,1) lhs (2 ,2) = lhs (2 ,2) - coeff * lhs (1 ,2) lhs (2 ,3) = lhs (2 ,3) - coeff * lhs (1 ,3) lhs (2 ,4) = lhs (2 ,4) - coeff * lhs (1 ,4) lhs (2 ,5) = lhs (2 ,5) - coeff * lhs (1 ,5) c (2 ,1) = c (2 ,1) - coeff * c (1 ,1) c (2 ,2) = c (2 ,2) - coeff * c (1 ,2) 8.2. DIAGNÓSTICO SOFTWARE 244 245 246 247 c (2 ,3) c (2 ,4) c (2 ,5) r (2) = = = = c (2 ,3) c (2 ,4) c (2 ,5) r (2) - 91 coeff * c (1 ,3) coeff * c (1 ,4) coeff * c (1 ,5) coeff * r (1) Los diversos accesos a los vectores que se muestran representan bucles que se han desenrollado manualmente y de forma completa. La idea que se propone es generar bucles en todas aquellas secciones de código de la función binvcrhs donde sea posible. De este modo el compilador puede intentar vectorizar dichos bucles y, a partir de ese punto, se puede afrontar el análisis de acuerdo a las dificultades que haya encontrado el compilador para no vectorizar. LU La aplicación LU, al igual que BT, fue seleccionada por su porcentaje de instrucciones escalares, 50,4 %. En la Tabla 8.12 (pág. 91) se muestran únicamente los dos primeros bloques del conjunto de bloques básicos más ejecutados, porque destacan por encima de los demás al contener un 23 % y 21 % del total de instrucciones ejecutadas en el programa, respectivamente. 1 2 PC Función Instrucciones % Ciclos % 0x41751b 0x41c8c8 buts blts 2.968.107.121 2.717.028.573 23,2 21,2 13.636.253.898 10.258.244.038 11,0 8,30 Tabla 8.12: Bloques 1 y 2 de los más ejecutados de LU, NPB El primero contiene parte del código de la función buts del fichero buts vec.f y el segundo bloque parte de blts del fichero blts vec.f. Ambas funciones tienen la misma estructura, pero cada una se encarga de trabajar con una de las dos matrices U y L, respectivamente. El código se compone de dos secciones diferenciadas: una primera con varios bucles encadenados que el compilador consigue vectorizar, y una segunda que está desenrollada manualmente en un factor de 5. Esta segunda situación es semejante a la que vimos en BT. A continuación se muestra un pequeño fragmento esto último: 77 ! ! dir$ unroll 5 78 ! manually unroll the loop 79 ! do m = 1 , 5 80 tv ( 1 , i , j 81 > + omega * ( udy ( 1 , 1 , 82 > + udx ( 1 , 1 , 83 > + udy ( 1 , 2 , 84 > + udx ( 1 , 2 , 85 > + udy ( 1 , 3 , 86 > + udx ( 1 , 3 , 87 > + udy ( 1 , 4 , 88 > + udx ( 1 , 4 , 89 > + udy ( 1 , 5 , 90 > + udx ( 1 , 5 , 91 tv ( 2 , i , j 92 > + omega * ( udy ( 2 , 1 , 93 > + udx ( 2 , 1 , 94 > + udy ( 2 , 2 , 95 > + udx ( 2 , 2 , 96 > + udy ( 2 , 3 , 97 > + udx ( 2 , 3 , 98 > + udy ( 2 , 4 , 99 > + udx ( 2 , 4 , ... 135 ! end do ) = tv ( 1 , i , j ) i , j ) * v ( 1 , i , j +1 , i , j ) * v ( 1 , i +1 , j , i , j ) * v ( 2 , i , j +1 , i , j ) * v ( 2 , i +1 , j , i , j ) * v ( 3 , i , j +1 , i , j ) * v ( 3 , i +1 , j , i , j ) * v ( 4 , i , j +1 , i , j ) * v ( 4 , i +1 , j , i , j ) * v ( 5 , i , j +1 , i , j ) * v ( 5 , i +1 , j , ) = tv ( 2 , i , j ) i , j ) * v ( 1 , i , j +1 , i , j ) * v ( 1 , i +1 , j , i , j ) * v ( 2 , i , j +1 , i , j ) * v ( 2 , i +1 , j , i , j ) * v ( 3 , i , j +1 , i , j ) * v ( 3 , i +1 , j , i , j ) * v ( 4 , i , j +1 , i , j ) * v ( 4 , i +1 , j , k k k k k k k k k k ) ) ) ) ) ) ) ) ) ) ) k k k k k k k k ) ) ) ) ) ) ) ) 92 CAPÍTULO 8. ESTUDIO EXPERIMENTAL El compilador indica que parte de este código es vectorizado como bloques en vez de como bucle. buts vec.f(80): (col. 19) remark: BLOCK WAS VECTORIZED buts vec.f(312): (col. 13) remark: BLOCK WAS VECTORIZED blts vec.f(83): (col. 19) remark: BLOCK WAS VECTORIZED Del resto de partes del código que también se encuentran desenrolladas no muestra ningún mensaje de por qué no ha vectorizado. Por tanto, la idea que se plantea consiste en aplicar la misma solución que para BT, para comprobar si transformando el código desenrollado en un bucle y utilizando el pragma vector always, o el pragma ivdep, se consigue vectorizar. 8.2.5. SPEC fp Las aplicaciones del benchmark SPEC fp seleccionadas para un análisis en profundidad son: Namd y Povray. Namd La aplicación Namd se caracterizaba por tener unicamente un 1,02 % de instrucciones vectoriales. Además, su 58 % de instrucciones escalares generaba una gran cantidad de dependencias que reducı́an el IPC de la aplicación. En concreto, estas dependencias ocasionaban que un 48,33 % de los ciclos de la aplicación fueran por este motivo. En el código fuente de Namd existe un fichero denominado ComputeNonbondedBase2.h, sobre el que bascula gran parte del código generado. A continuación se muestra un pequeño fragmento inicial de dicho código: 7 EXCLUDED ( FAST ( foo bar ) ) 8 EXCLUDED ( MODIFIED ( foo bar ) ) 9 EXCLUDED ( NORMAL ( foo bar ) ) 10 NORMAL ( MODIFIED ( foo bar ) ) 11 12 for ( k =0; k < npairi ; ++ k ) { 13 14 const int j = pairlisti [ k ]; 15 register const CompAtom * p_j = p_1 + j ; 16 17 register const BigReal p_ij_x = p_i_x - p_j - > position . x ; 18 register BigReal r2 = p_ij_x * p_ij_x ; 19 register const BigReal p_ij_y = p_i_y - p_j - > position . y ; 20 r2 += p_ij_y * p_ij_y ; 21 register const BigReal p_ij_z = p_i_z - p_j - > position . z ; 22 r2 += p_ij_z * p_ij_z ; 23 ... 28 FAST ( 29 const LJTable :: TableEntry * lj_pars = 30 lj_row + 2 * mol - > atomvdwtype ( p_j - > id ) MODIFIED (+ 1) ; ... 35 SHORT ( 36 const BigReal * const fast_i = table_four + 16* table_i + 8; 37 BigReal fast_a = fast_i [0]; 38 ) 39 ) 40 FULL ( 41 const BigReal * const scor_i = table_four + 16* table_i + 8 SHORT (+ 4) ; 42 BigReal slow_a = scor_i [0]; 43 ) 44 45 r2f . i &= 0 xfffe0000 ; 8.2. DIAGNÓSTICO SOFTWARE 93 Véanse las macros FAST y FULL en este fragmento. Estas, además de otras macros que aparecen en el mismo bucle, determinan qué secciones de la función son compiladas dependiendo de donde se usen. En todas las regiones donde se hace uso de alguna de ellas se hace inline del bucle con el código correspondiente a la macro. Esto da lugar a que en el informe del compilador haya múltiples resultados registrados para este bucle en concreto. Cuando consultamos los bloques básicos con mayor número de instrucciones y ciclos, nos encontramos que muchos coincidı́an debido a lo mencionado sobre el inline de las macros en el bucle. Además, todos tenı́an instrucciones escalares. Consultando las razones del compilador para no vectorizar este bucle en ninguna de las ocasiones, encontramos una única razón: las dependencias. ComputeNonbondedBase2.h(12): (col. 5) remark: loop was not vectorized: existence of vector dependence ComputeNonbondedBase2.h(76): (col. 7) remark: vector dependence: assumed FLOW dependence between f j line 76 and p j line 21 ComputeNonbondedBase2.h(21): (col. 47) remark: vector dependence: assumed ANTI dependence between p j line 21 and f j line 76 ComputeNonbondedBase2.h(76): (col. 7) remark: vector dependence: assumed FLOW dependence between f j line 76 and f i line 76 ComputeNonbondedBase2.h(76): (col. 7) remark: vector dependence: assumed ANTI dependence between f i line 76 and f j line 76 Según el compilador, las dependencias se encuentran entre las instrucciones p j y f j, ası́ como f j y f i. En el caso de p j y f j, encontramos que p j era un puntero a un objeto de una clase llamada CompAtom, que tomaba su valor de la variable p 1. La variable f j era una referencia a un objeto de una clase llamada Force que tomaba su valor de f 1[j]. El problema radicaba en que se estaba leyendo la posición x de p j y también se estaba escribiendo la posición x de f j. Para el compilador existı́a una ambigüedad respecto a si las direcciones donde estaban apuntando ambas variables era la misma. register const CompAtom * p_j = p_1 + j ; register const BigReal p_ij_x = p_i_x - p_j - > position . x ; Force & f_j = f_1 [ j ]; register BigReal tmp_x = force_r * p_ij_x ; f_j . x -= tmp_x ; const CompAtom * p_1 = params - > p [1]; Force * f_1 = params - > ff [1]; La variable params es un puntero a la estructura de tipo nonbonded, la cual efectivamente tiene dos campos p y ff de tipos CompAtom y Force respectivamente. 21 struct nonbonded { 22 CompAtom * p [2]; 23 Force * ff [2]; 25 Force * fullf [2]; ... 41 }; Podemos pues asegurar que tanto p j como f j son dos variables que apuntan a zonas de memoria diferentes y no hay dependencia alguna. Entonces, para conseguir vectorizar se podrı́a hacer uso del pragma ivdep que indica que se ignoren las dependencias. También se podrı́a modificar el código de modo que el procesador entienda que tanto p j como f j son diferentes. Por ejemplo, se podrı́a encerrar el código bajo la condición de que ambas variables fueran diferentes, lo cual serı́a cierto siempre. 94 CAPÍTULO 8. ESTUDIO EXPERIMENTAL Povray Las instrucciones y ciclos de esta aplicación están muy repartidas entre todos los bloques que la componen. Pese a esto, el primer bloque del conjunto de bloques más ejecutados que se muestra en la Tabla 8.13 (pág. 94) presentaba un porcentaje mayor de instrucciones y ciclos que los siguientes en la lista. Por este motivo se procede a su análisis. 1 PC Función Instrucciones % Ciclos % 0x5fad40 pov::All Sphere Intersections (pov::Object Struct*, pov::Ray Struct*, pov::istack struct*) 145.789.254 7,84 399.197.976 6,22 Tabla 8.13: Bloques 1 y 2 de los más ejecutados de Povray, SPEC FP De este bloque fue muy útil consultar la información generada por otra de las herramientas internas disponibles. A continuación se presentan dichas instrucciones junto con el detalle de las lı́neas del código a las que pertenecen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 sub rsp , 0 x58 add qword ptr [ rip +0 x1e8d24 ] , 0 x1 mov r8 , rsi vmovsd xmm2 , qword ptr [ rdi +0 x80 ] xor eax , eax vmovsd xmm3 , qword ptr [ rdi +0 x78 ] vmovsd xmm0 , qword ptr [ rdi +0 x88 ] vmovsd xmm4 , qword ptr [ rdi +0 x90 ] vmulsd xmm7 , xmm4 , xmm4 vsubsd xmm16 , k0 , xmm2 , qword ptr [ r8 +0 x8 ] vsubsd xmm5 , xmm3 , qword ptr [ r8 ] vsubsd xmm9 , xmm0 , qword ptr [ r8 +0 x10 ] vmulsd xmm8 , k0 , xmm16 , xmm16 vmulsd xmm6 , k0 , xmm16 , qword ptr [ r8 +0 x20 ] vfmadd231sd xmm8 , xmm5 , xmm5 vmovsd xmm4 , qword ptr [ r8 ] vmovsd xmm3 , qword ptr [ r8 +0 x8 ] vmovsd xmm2 , qword ptr [ r8 +0 x10 ] vfmadd231sd xmm8 , xmm9 , xmm9 vfmadd231sd xmm6 , xmm5 , qword ptr [ r8 +0 x18 ] vmovsd xmm0 , qword ptr [ r8 +0 x20 ] vmovsd xmm1 , qword ptr [ r8 +0 x28 ] vcmpsd k0 , k0 , xmm8 , xmm7 , 0 xd vfmadd132sd xmm9 , xmm6 , qword ptr [ r8 +0 x28 ] kortestw k0 , k0 jz 0 x5fadf4 # # # # # # # # # # # # # spheres . cpp :123 frame . h :980 spheres . cpp :123 vector . h :90 spheres . cpp :129 vector . h :89 vector . h :91 spheres . cpp :131 vector . h :296 vector . h :90 vector . h :89 vector . h :91 vector . h :223 # # # # vector . h :89 vector . h :90 vector . h :91 vector . h :223 # spheres . cpp :288 # vector . h :223 # spheres . cpp :288 Las instrucciones terminadas en sd son escalares. Los ficheros que participan en el bloque son spheres.cpp, vector.h y frame.h. Véase primero un fragmento de la función All Sphere Intersections del fichero spheres.h: spheres . cpp 122 static int A l l _ S p h e r e _ I n t e r s e c t i o n s ( OBJECT * Object , RAY * Ray , ISTACK *←Depth_Stack ) 123 { 124 register int I n t e r s e c t i o n _ F o u n d ; 125 DBL Depth1 , Depth2 ; 126 VECTOR IPoint ; 127 SPHERE * Sphere = ( SPHERE *) Object ; 128 8.2. DIAGNÓSTICO SOFTWARE 129 130 131 95 I n t e r s e c t i o n _ F o u n d = false ; if ( I n t e rs e c t _ S p h e r e ( Ray , Sphere - > Center , Sqr ( Sphere - > Radius ) , & Depth1 , &←Depth2 ) ) 132 { 133 if (( Depth1 > D EP TH _T O LE RA NC E ) && ( Depth1 < Max_Distance ) ) 134 { 135 VEvaluateRay ( IPoint , Ray - > Initial , Depth1 , Ray - > Direction ) ; 136 137 if ( Point_In_Clip ( IPoint , Object - > Clip ) ) 138 { 139 push_entry ( Depth1 , IPoint , Object , Depth_Stack ) ; 140 141 I n t e r s e c t i o n _ F o u n d = true ; 142 } 143 } 144 145 if (( Depth2 > D EP TH _T O LE RA NC E ) && ( Depth2 < Max_Distance ) ) 146 { 147 VEvaluateRay ( IPoint , Ray - > Initial , Depth2 , Ray - > Direction ) ; 148 149 if ( Point_In_Clip ( IPoint , Object - > Clip ) ) 150 { 151 push_entry ( Depth2 , IPoint , Object , Depth_Stack ) ; 152 153 I n t e r s e c t i o n _ F o u n d = true ; 154 } 155 } 156 } 157 158 return ( I n t e r s e c t i o n _ F o u n d ) ; 159 } La lı́nea principal es la 131 porque contiene la llamada a la función Intersect Sphere, dentro del mismo fichero, la cual contiene las llamadas a funciones de los ficheros frame.h y vector.h que participan en el bloque. A continuación se muestra un fragmento de Intersect Sphere. 275 int I n t er s e c t _ S p h e r e ( RAY * Ray , VECTOR Center , DBL Radius2 , DBL * Depth1 , DBL ←* Depth2 ) 276 { 277 DBL OCSquared , t_Closest_Approach , Half_Chord , t _ H a l f _ C h o r d _ S q u a r e d ; 278 VECTOR O r i g i n _ T o _ C e n t e r ; 279 280 I n c r e a s e _ C o u n t e r ( stats [ R a y _ S p h e r e _ T e s t s ]) ; 281 282 VSub ( Origin_To_Center , Center , Ray - > Initial ) ; 283 284 VDot ( OCSquared , Origin_To_Center , O r i g i n _ T o _ C e n t e r ) ; 285 286 VDot ( t_Closest_Approach , Origin_To_Center , Ray - > Direction ) ; En la lı́nea 280 está la llamada a la función Increase Counter de frame.h. El compilador hizo inline de esta función. A continuación se muestra la única lı́nea que la compone: frame . h 978 inline void I n c r ea s e _ C o u n t e r ( COUNTER & x ) 979 { 980 x ++; 981 } Las funciones VSub y VDot de las lı́neas 282, 284 y 286 son funciones también inline del fichero vector.h. Ambas están sobrecargadas para diferente tipo de parámetros, por lo que todas comparten el mismo contenido. A continuación se muestran un par de ellas a modo de ejemplo: 96 CAPÍTULO 8. ESTUDIO EXPERIMENTAL vector . h 87 inline void VSub ( VECTOR a , const VECTOR b , const VECTOR c ) 88 { 89 a [ X ] = b [ X ] - c [ X ]; 90 a [ Y ] = b [ Y ] - c [ Y ]; 91 a [ Z ] = b [ Z ] - c [ Z ]; 92 } 221 inline void VDot ( DBL & a , const VECTOR b , const VECTOR c ) 222 { 223 a = b [ X ] * c [ X ] + b [ Y ] * c [ Y ] + b [ Z ] * c [ Z ]; 224 } Si echamos un vistazo a la función VSub de la lı́nea 87, vemos que las tres instrucciones que la componen se podrı́an condensar en un único bucle para invitar al compilador a vectorizarlas con el pragma vector. Además, este fichero contiene multitud de funciones que se rigen bajo el mismo patrón pese a que no participan en el bloque básico del que partió el análisis. El resultado de comprimir las tres operaciones en un bucle serı́a como se ve en el siguiente código: 87 88 89 90 91 92 inline void VSub ( VECTOR a , const VECTOR b , const VECTOR c ) { # pragma vector for ( unsigned int i = 0; i < 3; i ++) a [ i ] = b [ i ] - c [ i ]; } Una vez hecho esto se podrı́a volver a compilar y comprobar si el compilador ha vectorizado estos bucles. En caso de que sea ası́, se podrı́a traducir en un incremento de las instrucciones vectoriales en sustitución de las escalares que minaban el bloque principal. 8.3. Diagnóstico Hardware En esta sección se presentan los experimentos realizados sobre aquellas aplicaciones limitadas por memoria que clasificamos con la denominación memory bound. Cuando se mostraron los resultados de las simulaciones realizadas con CMP$im, habı́a aplicaciones fuertemente limitadas por memoria cuyo análisis fue descartado independientemente de su caracterización vectorial. En un 80 % de los casos se trataba de aplicaciones que presentaban un nivel de vectorización importante, pero debido a las limitaciones de memoria el IPC de la aplicación era deficiente. La siguiente lista indica aquellas que presentaban este perfil: channel y linpk de Polyhedron, Cloverleaf de Mantevo, IRSmk y UMTmk de Sequoia, MG, FT, SP y CG de NPB 470.lbm, 434.zeusmp y 437.leslie3d de SPEC fp. Las pruebas realizadas consistieron en doblar por un lado el tamaño de la cache unificada UL2, y por otro el tamaño del segundo nivel de TLB de datos. Se eligieron estos niveles de memoria porque querı́amos visualizar la mejora en caso de que hubiera más aciertos en los niveles de cache previos a la memoria principal. La latencia fue otro de los campos a considerar. Sin embargo, dado 8.3. DIAGNÓSTICO HARDWARE 97 que la latencia configurada para la memoria principal era 22x respecto al último nivel de cache, lo descartamos. El objetivo fue por tanto tener una visión general sobre qué ocurrirı́a cuando este tipo de aplicaciones no tienen que visitar tanto la memoria principal. 8.3.1. Incremento de UL2 Tomamos la decisión de simular solamente las aplicaciones en las que la limitación de memoria superaba un 30 % de los ciclos de toda la aplicación. A la hora de entender las hipotéticas mejoras experimentadas, hubo que tener en cuenta que, en la manipulación de porcentajes, no es lo mismo que se haya experimentado una reducción del 90 % de ciclos de memoria en una aplicación donde suponı́an un 5 % del total que en otra donde suponı́an un 50 %. Por otro lado, como nos interesaba ver únicamente las aplicaciones con mejor ı́ndice de mejora, en la Figura 8.12 (pág. 97) se visualizaron aquellas cuya mejora del IPC ascendı́a a más de un 1 %. El orden de las aplicaciones en el eje de abscisas está determinado por la mejora del IPC. Nótese que las mejoras en la memoria se presentan con un porcentaje negativo (menos es mejor). Esto se traduce en un incremento positivo del IPC. En la Tabla 8.14 (pág. 97) se muestran las aplicaciones que teniendo más de 30 % de ciclos de memoria en la aplicación, no han conseguido un mı́nimo de un 1 % de mejora en el IPC. Figura 8.12: Resultado de doblar la UL2 de 1024Kb a 2048Kb Benchmark Aplicación Polyhedron linpk nf Mantevo miniMD miniGhost miniFE CloverLeaf HPCCG Sequoia UMTmk IRSmk NPB FT CG IS MG SPEC fp 470.lbm Tabla 8.14: Aplicaciones con una mejora inferior al 1 % 98 CAPÍTULO 8. ESTUDIO EXPERIMENTAL La mejora de la aplicación SPEC fp/433.milc, visualizada en detalle en la Figura 8.13 (pág. 98), dobla su IPC. La reducción del 82,2 % de ciclos de memoria ha supuesto una reducción del 52,5 % de ciclos de la aplicación, que se traduce en un 110 % más de IPC. A la derecha se visualiza la distribución del porcentaje de ciclos de la aplicación antes y después. Dejó de ser una aplicación limitada exclusivamente por memoria principal, para pasar a ser una aplicación que, con un 87 % de instrucciones escalares, serı́a candidata a ser analizada para mejorar el ı́ndice de vectorización. Figura 8.13: Mejora de SPEC fp/433.milc al doblar la L2 Ténganse en cuenta casos como NPB/BT donde, pese a haber sufrido un decremento de ciclos de memoria del 71,95 %, no repercute más que un 28,54 % de mejora del IPC. Esto ocurre porque su porcentaje de ciclos de memoria estaba en el lı́mite del 30 % establecido como corte para el experimento. Entonces, aunque se hayan reducido los ciclos de memoria, no se puede reducir más el IPC. Otro caso caso particular es el de polyhedron/test fpu. En la gráfica se aprecia, junto al 25,2 % de mejora de ciclos de memoria, un 4 % en la UL2. Para analizar el resultado tenemos que tener en cuenta los dos conceptos fundamentales sobre: Ciclos de memoria: estos son el resultado de los fallos producidos en la cache unificada de nivel 2 y, debido a la fuerte penalización en ciclos de memoria principal, las instrucciones asociadas siempre aparecerı́an en el camino crı́tico. Ciclos de UL2: que estos ciclos formen parte del camino crı́tico depende de los fallos producidos en la caché de nivel 1. Sin embargo, como la penalización por un acierto en UL2 no es tan grande como la que generarı́a un fallo, si gracias a haber doblado el tamaño de la UL2 se aumentan sus aciertos, significa que estos ciclos podrı́an haber dejado de formar parte del camino crı́tico si la instrucción siguiente dependiera de una load-op anterior de fuerte latencia (ej. división). Véase la Figura 8.14 (pág. 99). Entonces, sabiendo que el número de accesos a la cache es obviamente el mismo y que no hemos doblado el tamaño de la DL1, la reducción del 4 % tiene que estar ı́ntimamente relacionada con que algunas instrucciones que antes formaban parte del camino crı́tico por fallo en la UL2, ahora con el acierto, ya no lo son. 8.3. DIAGNÓSTICO HARDWARE 99 Figura 8.14: Consecuencia posible por aumento de aciertos en L2 8.3.2. Incremento de TLB Análogamente, no para todas las aplicaciones tenı́a sentido simular su comportamiento con el doble de lı́neas en el segundo nivel de TLB. Dado que habı́a muy pocas aplicaciones con muchos ciclos de TLB, se bajó el umbral de selección a un 10 %. No se bajó más porque se considera que en una aplicación con menos del 10 % de ciclos de TLB es irrelevante aplicar una mejora costosa como es aumentar el número de lı́neas. Incluso ası́, solo cumplieron el requisito cuatro aplicaciones. Éstas se muestran en la Figura 8.15 (pág. 100) siguiendo la nomenclatura usada anteriormente. La aplicación NPB,IS presenta una mejora del 64,50 % de ciclos que repercuten en una mejora del IPC del 35,10 %. A su lado tenemos a NPB,DC que presenta el mismo comportamiento que veı́amos para Polyhedron,test fpu en el apartado anterior. En este caso, además del 93,82 % de mejora de los ciclos de memoria por fallos de TLB, también hay una reducción del 26,37 % en los ciclos de acceso al TLB de segundo nivel. En el modelo, un fallo de TLB que provoca un acceso a la tabla de páginas de la memoria principal acarrea una latencia importante. Si por aumentar el número de lı́neas se reducen estos accesos, es factible que, como en el caso anterior, las instrucciones que fueran responsables de dichos accesos hayan dejado de formar parte del camino crı́tico de la aplicación. De todos modos, pese al 93,82 % de mejora de los ciclos, como la reducción se aplica solamente sobre un 13,3 % de ciclos de TLB, al final el IPC solo mejora un 12,5 %. En la Figura 8.16 (pág. 100) se presenta en detalle el resultado de mejorar NPB,IS. En la parte izquierda de la Figura 8.16 (pág. 100), vemos el decremento en ciclos y el aumento de IPC de la aplicación IS de NPB. Pese a que es una mejora tı́mida, la gráfica derecha nos devuelve a la realidad porque nos recuerda que sigue siendo una aplicación limitada por memoria. Además, si recordamos la Tabla 8.14 (pág. 97), era una de las aplicaciones con una mejora inferior al 1 % cuando se dobló el tamaño de la caché unificada L2. Por tanto, para esta arquitectura en concreto, este tipo de mejoras es insuficiente y no deja otro camino que abrir el paso hacia una posible mejora de la arquitectura en sı́ como alternativa. El diagnóstico hardware realizado en este apartado quiso marcar el final de la búsqueda del modo de hacer un uso efectivo de la unidad vectorial. Hemos visto aquı́ que, pese a que el objetivo principal del trabajo se cumpla, el perfil de una aplicación al ser ejecutado sobre una máquina con una configuración de memoria concreta puede impedir que se obtengan las mejoras que se esperarı́an al hacer un buen uso de la unidad vectorial. En definitiva, la memoria no se puede ignorar y podrı́a ser necesario hacer un buen estudio previo sobre el uso de las estructuras de datos de la aplicación, para, o bien para maximizar el rendimiento de la aplicación sobre la máquina, o bien para mejorar la máquina. 100 CAPÍTULO 8. ESTUDIO EXPERIMENTAL Figura 8.15: Resultado de doblar las lı́neas de DTLB2 de 256 a 512 Figura 8.16: Mejora de IS de NPB al doblar la TLB Capı́tulo 9 Conclusiones La vectorización es una herramienta potente que puede proporcionar buenos resultados cuando se consigue usar plenamente. Teniendo esto presente, el estudio de la utilización efectiva de procesadores vectoriales presentado ha consistido en el proceso que se describe a continuación. En primer lugar se tomaron el conjunto de benchmarks Polyhedron, Mantevo, NPB, Sequoia y SPEC fp, de los cuales se seleccionaron, o bien todas las aplicaciones, o bien solo una muestra según el tamaño de los ficheros de entrada disponibles. Se compilaron con los compiladores R R IntelICC e IntelIFORT según el lenguaje en el que estuvieran escritas, y se realizó una primera caracterización de las mismas para conocer sus ı́ndices de vectorización y recopilar las causas proporcionadas por el compilador para no vectorizar. Este primer acercamiento mostró que, en un 21 % de las aplicaciones, el número de instrucciones vectoriales era igual o superior al 38 % del total. A continuación se incorporó al simulador de cache CMP$im una funcionalidad nueva: un modeR Xeon PhiTM . Con los resultados obtenidos lo simplificado de los núcleos del coprocesador Intel con el simulador modificado, y apoyándonos en la comparativa de la versión vectorizada frente a la no vectorizada, se descubrió que un 57 % de aplicaciones ejecutaban menos instrucciones que sus respectivas versiones no vectorizadas. Aunque podrı́a parecer un dato positivo, hay que tener en cuenta que solo un 21 % de aplicaciones tenı́a un 38 % o más de instrucciones vectoriales. Por este motivo, pese a que las versiones vectorizadas hubieran mejorado, el uso de la unidad vectorial es bajo en el 79 % de las aplicaciones. Además, a esto hay que añadir que un 32 % del mencionado 57 % de aplicaciones, mostraron un comportamiento fuertemente limitado por los accesos a la memoria principal. En este punto se tenı́a información suficiente sobre las aplicaciones no limitadas por memoria, para seleccionar las más significativas desde el punto de vista de la mejora del uso de la unidad vectorial. Se estudiaron tanto a nivel de bloques básicos como a nivel de código fuente. Los objetivos eran identificar las regiones más ejecutadas que el compilador no habı́a vectorizado, estudiar las causas y proponer una posible solución. Solo se implementó la solución en una de las aplicaciones porque era una tarea fuera de los objetivos de este trabajo. Finalmente, se decidió realizar dos pruebas sobre las aplicaciones que, estando limitadas por memoria, tenı́an un buen ı́ndice de vectorización: doblar el tamaño de la L2 y el número de lı́neas del segundo nivel de TLB. Se consideraron, para la primera prueba, aquellas cuyos ciclos de memoria principal suponı́an un 30 % o más de los ciclos de la aplicación. Se observó que solo un 45 % obtuvo una mejora superior al 1 %. De ese 45 %, un 50 % experimentó una mejora del IPC superior al 20 %. Para la segunda prueba se consideraron aquellas aplicaciones cuyos ciclos de TLB eran superiores o igual al 10 % del total. Solo se simularon 4 aplicaciones de las cuales 2 101 102 CAPÍTULO 9. CONCLUSIONES obtuvieron mejora. Sin embargo solo una de ellas mejoró en más de un 20 % el IPC. Todos los datos presentados muestran de forma clara la fuerte dependencia que existe entre la vectorización, la memoria, la programación y el compilador. La complejidad que puede llegarse a alcanzar para poder maximizar el aprovechamiento de la unidad vectorial de un procesador es fuerte. Además, el hecho de que el compilador sea muy bueno y proporcione multitud de funcionalidades, podrı́a resultar inútil si el código no está bien escrito, o si no lo está como el compilador lo espera. Podrı́a llegarse al punto en que hubiera que reescribirse el código completo. Esto hace recaer una gran parte de la responsabilidad en el programador y su habilidad para explotar las facilidades de que se dispongan para maximizar la vectorización de la aplicación. Las limitaciones de memoria, por su parte, resultaron ser un motivo importante que impidió obtener buen rendimiento en aplicaciones que hacı́an ya un buen uso de la unidad vectorial. El diagnóstico hardware quiso demostrar precisamente que la memoria no se puede ignorar y que podrı́a ser necesario estudiar bien el uso de las estructuras de datos de la aplicación para saber qué configuración de memoria le vendrı́a mejor. Por tanto, el estudio de la utilización efectiva de la unidad vectorial realizado ha servido para afirmar finalmente lo siguiente: La vectorización es posible pero en ocasiones muy difı́cil de conseguir. El compilador es una herramienta crucial en todo el proceso. La penalización de los accesos a memoria resulta ser en muchos casos un cuello de botella importante en el balance final del cómputo de la aplicación, pese a la vectorización. 9.1. Trabajo Futuro Entre los trabajos futuros que han quedado pendientes en el presente estudio, y que podrı́an por tanto conferirle una mayor completitud, se encuentran los siguientes: Implementar las propuestas presentadas en el Sección 8.2 (pág. 74) para cada una de las aplicaciones seleccionadas: • Usar el pragma vector always pese a que las iteraciones de un bucle sean pequeñas. • Reescribir el código en caso de que con pragmas sea insuficiente o porque el código sea muy complejo. • Usar el pragma ivdep en caso de que las dependencias no sean tales debido a ambigüedades de los punteros. • Usar el pragma unroll para invitar al compilador a reordenar instrucciones en caso de problemas por dependencias. • Generar bucles en aquellas regiones donde el acceso a cada uno de los elementos de un vector o matriz se realizó con instrucciones sueltas. Completar CMP$im para simular aplicaciones multi-threading y ver el comportamiento haR Xeon PhiTM . ciendo uso de todos los núcleos del coprocesador Intel Añadir etapas y lı́mite de recursos a CMP$im para obtener resultados más detallados. Estudiar diferentes configuraciones de memoria, ası́ como su viabilidad, para mejorar el rendimiento de las aplicaciones limitadas por memoria. Bibliografı́a [Bar07] Blaise Barney. Introduction to parallel computing. 2007. [Bar08] Miquel Barceló. Una historia de la informática. Editorial UOC, 2008. [Dı́06] Domingo Benı́tez Dı́az. Arquitectura de Computadores. Manual de teorı́a. Universidad de Las Palmas de Gran Canaria. Vicerrectorado de Planificacion y Calidad, 2006. [fly] Flynn’s taxonomy. http://en.wikipedia.org/wiki/Flynn’s_taxonomy. [Fly72] Michael J. Flynn. Some computer organizations and their effectiveness. IEEE Transactions on Computers, C-21(9):948–960, 1972. [HP02] John L. Hennessy and David A. Patterson. Computer architecture a quantitative approach. Morgan Kaufmann, tercera edición edition, 2002. [inta] Avx-512 instructions. https://software.intel.com/en-us/blogs/2013/avx-512-instructions. [intb] R xeon phiTM coprocessor - the architecture. Intel https://software.intel.com/en-us/articles/intel-xeon-phi-coprocessor-\ codename-knights-corner. [intc] R software documentation library. Intel http://software.intel.com/en-us/intel-software-technical-documentation/. [intd] R cilkTM plus. Introduction to Intel http://software.intel.com/en-us/videos/introduction-to-intel-cilk-plus/. [inte] R c++ compiler. An introduction to vectorization with the Intel http://d3f8ykwhia686p.cloudfront.net/1live/intel/An_Introduction_to_ Vectorization_with_Intel_Compiler_021712.pdf. [JCLJ06] Aamer Jaleel, Robert S. Cohn, Chi-Keung Luk, and Bruce Jacob. Cmp$im: A binary instrumentation approach to modeling memory behavior of workloads on cmps. 2006. [JCLJ08] Aamer Jaleel, Robert S. Cohn, Chi-Keung Luk, and Bruce Jacob. Cmp$im: A pinbased on-the-fly multi-core cache simulator. 2008. [LCM+ 05] Chi-Keung Luk, Robert Cohn, Robert Muth, Harish Patil, Artur Klauser, Geoff Lowney, Steven Wallace, Vijay Janapa Reddi, and Kim Hazelwood. Pin: Building customized program analysis tools with dynamic instrumentation. In Proceedings of the 2005 ACM SIGPLAN conference on Programming language design and implementation, pages 190–200, 2005. [LPG13] Dominic Orchard Leaf Petersen and Neal Glew. Automatic simd vectorization for haskell. 2013. 103 104 BIBLIOGRAFÍA [man] Mantevo benchmarks. http://mantevo.org/. [MS03] Pratyusa Manadhata and Vyas Sekar. Vector processors. 2003. [nas] Nas parallel benchmarks. http://www.nas.nasa.gov/publications/npb.html. [nvi] Nvidia’s cuda toolkit: Parallel thread execution isa version 4.0. http://docs.nvidia.com/cuda/parallel-thread-execution/index.html. [pin] Pin - a dynamic binary instrumentation tool. http://pintool.org/. [Pip12] R fortran compiler. 2012. Chuck Piper. An introduction to vectorization with the intel [pol] Polyhedron fortran benchmarks. www.polyhedron.com. [seq] Asc sequoia benchmark codes. https://asc.llnl.gov/sequoia/benchmarks/. [SLA05] Rodric Rabbah Samuel Larsen and Saman Amarasinghe. Exploiting vector parallelism in software pipelined loops. 2005. [SMP11] Maria J. Garzarán Tommy Wong Saeed Maleki, Yaoquing Gao and Daivd A. Padua. An evaluation of vectorizing compilers. 2011. [spe] Spec cpu 2006. http://www.spec.org/cpu2006/. Apéndice A R ICC Specific Pragmas Intel R Specific Pragmas[intc] son pragmas desarrollados especı́ficamente para el compilaLos Intel R El listado se puede comprobar en la Tabla A.1. dor ICC de Intel. alloc section Asigna una variable a una sección especı́fica. cilk grainsize Especifica el número máximo de iteraciones que se ejecutaran en serie en un cilk for. Es un tipo especial de bucle que permite que varias conjuntos de iteraciones se ejecuten paralelamente, pero las iteraciones de cada uno de esos conjuntos se ejecutan en serie. Por tanto, el grainsize es precisamente el número máximo de loops en cada conjunto a ejecutar paralelamente. distribute point Indica al compilador que en la región indicada se prefiere distribución de bucles. inline Indica al compilador la preferencia de que una llamada a un procedimiento o función concretos se haga inline. intel omp task Sirve para indicar una unidad de trabajo, ejecutada probablemente por un hilo distinto, de cara a la delegación de tareas. intel omp taskq Define el entorno en el que se encolarán cada una de las unidades de trabajo especificadas, de cara a la delegacion de tareas. ivdep Indica al compilador que ignore las dependencias vectoriales que ha detectado en el bucle que le sigue en el código. loop count Indica el número de veces que el bucle que le sigue se va a ejecutar. 105 R ICC SPECIFIC PRAGMAS APÉNDICE A. INTEL 106 nofusion Impide que el bucle siguiente se fusione con otros bucles adyacentes. novector Indica que el bucle siguiente no debe ser vectorizado. offload La instrucción siguiente al pragma se ejecutara en el R MIC target especificado. Aplicable solo sobre Intel Architecture. offload attribute Sirve para especificar que las variables y funciones declaradas a continuacion del pragma, estarán dispoR nibles en el coprocesador. Aplicable solo sobre Intel MIC Architecture. offload transfer Para iniciar transferencias de datos ası́ncronas o iniciar y completar transferencias de datos sı́ncronas. R MIC Architecture Aplicable solo sobre Intel offload wait Establece un punto de espera para actividades ası́ncronas iniciadas previamente. Aplicable solo soR MIC Architecture. bre Intel omp atomic Sirve para asegurar que la actualización de una determinada posición de memoria sea atómica, con el objetivo de impedir la posibilidad de que los hilos realicen lecturas y escrituras múltiples o simultáneas. omp task Define la región de una tarea. omp taskyield Habilita o deshabilita optimizaciones sobre determinadas funciones. Es en cierto grado compatible con la implementación de MicrosoftTM del pragma optimize. omp taskwait Especifica un punto de espera para la finalización de tareas generadas desde el comienzo de la ejecución de la tarea actual. optimize Habilita o deshabilita optimizaciones sobre todo el código escrito a continuación del pragma, hasta que se encuentre con otro pragma optimize o con el fin de la unidad de compilación. optimization level Restringe el nivel de optimización de una función concreta. Mientras que para todo el programa se puede haber indicado -O3 desde la lı́nea de compilación, con #pragma optimization 1 se indica que a la función que le acompaña se le aplicará -O1. optimization parameter Indica al compilador la tarea de generar código especı́fico, a nivel de función, para un tipo de procesador. Es semejante a la opción de compilación -m(arch). 107 parallel/ noparallel Sirve para indicar al compilador que resuelva dependencias de bucles mediante la auto-paralelización del bucle situado inmediatamente a continuación. En el caso de noparallel, impide la auto-paralelización del bucle inmediatamente a continuación. prefetch/ noprefetch Invita al compilador a emitir prefetches de datos a memoria. En el caso de noprefetch deshabilita R el prefetching de datos. Aplicable solo sobre Intel MIC Architecture. simd Sirve para forzar al compilador a vectorizar el bucle sobre el que se defina. unroll/ nounroll Indica al compilador el número de veces que tiene que desenrollar un bucle. En el caso de nounroll, le impide aplicar esta técnica. unroll and jam/ nounroll and jam Estos pragmas invitan o impiden que el compilador desenrolle y fusione bucles. Estos bucles solo pueden ser de tipo FOR. unused Indica variables que no se van a usar con el objetivo de impedir la generación de warnings durante la compilación. vector Indica al compilador que el bucle siguiente deberı́a ser vectorizado de acuerdo a los siguientes parámetros: always, aligned, unaligned, nontemporal, temporal. R ICC Specific Pragmas Tabla A.1: Intel Apéndice B R ICC Supported Pragmas Intel R Supported Pragmas[intc] son un conjunto desarrollado por fuentes externas que son Las Intel mantenidas en estos compiladores por razones de compatibilidad. Muchas de ellas se encuentran en la documentación de los entornos de programación ofertados por MicrosoftTM . Se puede consultar una descripción breve de ellas en la Tabla B.1 alloc text Indica la sección de código donde tienen que estar las definiciones de función especificada. auto inline Aquellas funciones sobre las que se indique el parámetro off, serán excluidas como candidatas para expandirse automaticamente (inline). bss seg Especifica al compilador el segmento dentro del fichero .obj donde las variables no inicializadas tienen que residir. check stack Si se especifica como parámetro on, se habilitará el check de pila para las funciones inmediatamente a continuación. En el caso de que sea off, se deshabilitara. code seg Especifica una sección de código donde se situarán las funciones. comment Inserta un registro de comentarios en un fichero objeto o ejecutable. component Sirve para controlar la generación de información como pueden ser las dependencias o la denominada browse information, que incluye aspectos como definiciones, referencias, macros, etc. a partir de los fuentes de la aplicación. conform Especifica el comportamiento en tiempo de ejecución de la opción de compilación /Zc:forScope (Force Conformance in FOR Loop Scope) incluida en el entorno de Visual Studio de MicrosoftTM . 109 R ICC SUPPORTED PRAGMAS APÉNDICE B. INTEL 110 const seg Especifica el segmento donde se alojarán las funciones el fichero .obj. data seg Sección especı́fica para la inicialización de datos. deprecated Indica para una función, tipo o cualquier otro identificador, que puede no estar soportado en futuras versiones o que no deberı́a usarse más. poison Sirve para etiquetar los identificadores que se deberı́an eliminar de la aplicación, de manera que cuando se compile, aquellos identificadores marcados con este pragma provocarán un error de compilación. float control Especifica que una función tiene operaciones en punto flotante. fp contract Habilita o deshabilita la implementación para fusionar expresiones. include directory Incorpora la cadena pasada como argumento, a la lista de sitios donde buscar por ficheros de inclusión. init seg Especifica la sección que contiene inicializaciones en C++ para las unidades de compilación generadas tras el preprocesador. message Muestra la cadena especificada como parámetro en la salida estándar. optimize Especifica las optimizaciones a realizar sobre las funciones bajo el este pragma o hasta el siguiente pragma del mismo tipo. También se encuentra en la anR con terior lista de aquellos desarrollados por Intel, el objetivo de soportar la implementación del de MicrosoftTM . options Pragma compatible con el compilador GCC de MacOS. Configura el alineamiento de los campos en las estructuras de datos. pointers to members Especifica si un puntero a un miembro de una clase se puede declarar antes de la definición de la clase en cuestión. También es usado para controlar el tamaño del puntero y el código que se necesitara para interpretarlo. pop macro Configura el valor de una macro en concreto al valor que se encuentre en la cima de la pila asociada a dicha macro. push macro Salva el valor de una macro en concreto en la cima de la pila asociada a esta macro. 111 region/endregion Sirve para especificar un segmento de código en el MicrosoftTM Visual Studio 2005 Code Editor, que se despliega o contrae utilizando las caracterı́sticas propias de esta herramienta de trabajo. section Crea una sección en un fichero .obj. Una vez que esta seccion es definida, permanece válida durante el resto de la compilación. start map region Se usa junto con stop map region. stop map region Se usa junto con start map region fenv access Sirve para informar a una implementación, que un programa podrı́a testear los flags de estado o ejecutar o ejecutarse bajo un modo distinto al modo por defecto. vtordisp Si el parámetro es on, se habilita la generación de miembros vtordisp ocultos. Para el caso de off se deshabilita. warning Permite la alteración selectiva del comportamiento de los mensajes de aviso, warnigs, del compilador. weak Sirve para indicar que un sı́mbolo es weak, es decir que en caso de no encontrarse la definición para ese sı́mbolo en el tiempo de linkado, no se lanza ningún error. R ICC Supported Pragmas Tabla B.1: Intel Apéndice C R Fortran Directives Intel R Fortran proporciona algunas directivas de compilación de propósito El compilador Intel general para permitir configurar algunas tareas a realizar durante la compilación[intc]. El listado de la tabla Tabla C.1 presenta el conjunto de directivas disponibles. ALIAS Sirve para especificar un nombre alternativo para los subprogramas externos a los que se haga referencia. ASSUME ALIGNED Especifica que una entidad se encuentra alineada en memoria. ATTRIBUTES Permite definir propiedades sobre objetos y procedimientos. DECLARE and NODECLARE Activa o desactiva los warnings del compilador en el caso de que haya variables que se hayan usado pero que no se hayan definido. DEFINE and UNDEFINE Sirve para definir o eliminar la definición de variables simbólicas cuya existencia o valor pueda ser testeadas durante compilación condicional. DISTRIBUTE POINT Sirve para sugerir al compilador los puntos donde un bucle DO puede partirse. FIXEDFORMLINESIZE Establece la longitud de la lı́nea para el código fuente de tipo fixed-form. Por ejemplo, que en la columna 1 de cada fila es donde se pone el sı́mbolo *, c ó ! para indicar que es un comentario. También es el caso de la columna 6, que sirve para indicar con un sı́mbolo que la lı́nea anterior continúa en la lı́nea siguiente. FREEFORM and NOFREEFORM La primera indica que el código fuente se corresponde con formato free-form. La segunda indica que se corresponde con formato fixed-form. IDENT Especifica una cadena que identifica al un módulo objeto. 113 R FORTRAN DIRECTIVES APÉNDICE C. INTEL 114 IF and IF DEFINED Sirve para especificar aquellas secciones de código que son de compilación condicional. INLINE, FORCEINLINE, NOINLINE Indica el tipo de inlining que el compilador tiene que llevar a cabo para rutinas o bucles DO. INTEGER Sirve para establecer el número de bytes por defecto que serán asignados a los enteros. IVDEP Indica al optimizador del compilador que asuma que las dependencias se producen en la misma dirección que su aparición. LOOP COUNT Especifica el número de iteraciones que va a realizar un bucle DO. MESSAGE Indica la cadena que se enviará a la salida estándar durante la primera pasada del compilador. NOFUSION Impide que un bucle se fusione con otros bucles adyacentes en caso de ser posible. OBJCOMMENT Especifica la ruta de búsqueda de una librerı́a en el fichero objeto. Se puede especificar más de una directiva de este tipo para configurar diferentes rutas a distintas librerı́as en un mismo código fuente. OPTIMIZE and NOOPTIMIZE Habilita o deshabilita optimizaciones sobre la unidad de programa. Una unidad de programa puede ser el programa principal, una subrutina externa o función. OPTIONS Afecta al alineamiento de datos y a los warnings producidos por ello. PACK Especifica el alineamiento en memoria de tipos derivados o estructuras tipo struct. PARALLEL and NOPARALLEL Facilita la auto-paralelización ayudando al análisis de dependencias del compilador sobre el bucle DO siguiente. En el caso de NOPARALLEL, se impide. PREFETCH and NOPREFETCH Habilita o deshabilita las pistas para el compilador de cara a realizar prefetching de datos de memoria. PSECT Modifica las caracterı́sticas de un bloque común. Si la unidad de programa cambia una o más caracterı́sticas de uno de estos bloques, todas las unidades que lo referencien deben también cambiar. REAL Sirve para establecer el número de bytes por defecto que serán asignados a los reales. SIMD Requiere y controla la vectorización SIMD de los bucles. 115 STRICT and NOSTRICT STRICT habilita caracterı́sticas del lenguaje no encontradas en el estándar especificado en lı́nea de comandos (Fortran 2008, 2003, 95 o 90). Por el contrario, NOSTRICT las deshabilita. UNROLL and NOUNROLL La primera indica al optimizador del compilador cuantas veces hay que desenrollar el bucle DO al que afecta. La segunda impide el desenrollamiento de ese bucle. UNROLL AND JAM and NOUNROLL AND JAM Habilitan o deshabilitan, respectivamente, la posibilidad del compilador de desenrollar y fusionar. Solo se pueden aplicar a bucles DO iterativos. VECTOR and NOVECTOR Sobrescriben las heurı́sticas por defecto para la vectorización de bucles DO. R Fortran Directives Tabla C.1: Intel Apéndice D Mensajes del compilador Los mensajes del compilador más significativos son los siguientes: Low trip count Bucle con un número de iteraciones pequeña. Vectorization possible but seems inefficient Bucle demasiado largo con elevado uso de recursos, ej. registros. Unsupported loop structure El compilador no es capaz de determinar el número de iteraciones, por ejemplo si se usan punteros como contadores. Not inner loop El bucle tratado no es un bucle interno. Existence of vector dependence existencia de dependencias entre los punteros del bucle. Nonstandard loop is not a vectorization candidate El bucle contiene más de un punto de salida. También puede ocurrir que haya una llamada en bucle que se haya pasado como parámetro a la función que contiene el bucle y que no puede resolver. Unsupported reduction no soporta alguna de las reducciones que se estén realizando dentro del bucle Conditional assignment to a scalar El bucle tiene una operación de asignación a una variable escalar, o un campo de una estructura, dentro de una condición. Statement cannot be vectorized El uso de expresiones especı́ficas de Cilk o alguna herramienta multi-hilo como OpenMP dan lugar a este mensaje. Tabla D.1: Mensajes del compilador 117