Implementación de una herramienta de optimización iterativa para códigos OpenCL

Tamaño: px
Comenzar la demostración a partir de la página:

Download "Implementación de una herramienta de optimización iterativa para códigos OpenCL"

Transcripción

1 FACULTADE DE INFORMÁTICA Departamento de Electrónica e Sistemas Proxecto de Fin de Carreira de Enxeñaría Informática Implementación de una herramienta de optimización iterativa para códigos OpenCL Autor: Directores: Jorge Fernández Fabeiro Diego Andrade Canosa Basilio B. Fraguela Rodríguez A Coruña, 29 de xuño de 2012

2

3 Especificación Título: Implementación de una herramienta de optimización iterativa para códigos OpenCL Clase: Investigación y desarrollo Autor: Jorge Fernández Fabeiro Directores: Diego Andrade Canosa Basilio Bernardo Fraguela Rodríguez Tribunal: Fecha de lectura: Calificación: i

4

5 Al carro de la cultura española le falta la rueda de la ciencia Santiago Ramón y Cajal iii

6

7 Agradecimientos En primer lugar, me gustaría agradecer a los profesores Diego Andrade y Basilio B. Fraguela el haber vuelto a darme la oportunidad de trabajar con ellos proponiéndome llevar a cabo el presente proyecto, así como toda la ayuda prestada a lo largo de estos meses. Así mismo, este proyecto no habría llegado a buen puerto sin los consejos de la gente del laboratorio del Grupo de Arquitectura de Computadores, en especial de Diego Darriba (siempre aguantándome cuando alguna cosa se torcía), Iván Cores (dando cuando uno menos se lo espera ese toque de humor tan necesario) y Moisés Viñas (siempre dispuesto a echar un cable con todas las prácticas y trabajos del Máster). Llegar hasta aquí tampoco habría sido posible sin la compañía de Jorge, Sergio y Víctor. Sin ellos, estos tres años que he necesitado para terminar el segundo ciclo no habrían sido lo mismo. Tampoco me olvido de los compañeros (ellos ya saben quienes son) que han compartido conmigo prácticamente toda mi vida universitaria desde aquel principio de curso de octubre de Ya para terminar, y en absoluto por ello menos importante, nunca podré agradecer lo suficiente a mis padres que siempre hayan estado ahí, apoyando lo que hago y animándome a continuar con ello (si es que los recortes no acaban con la Universidad Pública...). A todos vosotros, de nuevo, muchas gracias. v

8

9 Resumen La evolución de la arquitectura de computadores ha permitido que, en la actualidad, cualquier ordenador cuente con capacidades de procesamiento paralelo gracias a que contiene un procesador multinúcleo y una tarjeta gráfica con capacidades GPGPU. En este contexto aparece un nuevo paradigma conocido como computación heterogénea, que busca explotar simultáneamente todos esos recursos para obtener el máximo rendimiento posible. Dentro de dicho paradigma, OpenCL parece ser la opción mejor posicionada para la programación de este tipo de arquitecturas. Sin embargo, OpenCL cuenta con el inconveniente de que la portabilidad de código entre arquitecturas no se ve reflejada de forma efectiva en el rendimiento: un código optimizado para una determinada plataforma puede ser ejecutado en otra diferente, pero difícilmente se conseguirá el mismo rendimiento que en la original para la que fue optimizado. La herramienta OCLOptimizer implementada en este Proyecto Fin de Carrera busca dotar a un usuario-programador experto de todo lo necesario para conseguir esta portabilidad efectiva. La herramienta recibe como entradas un kernel OpenCL anotado con una serie de directivas y un fichero de configuración en el que especifica información como los parámetros de entrada del kernel, el tipo de dispositivo sobre el que se ejecutará, etc. Las directivas deben ser introducidas por el usuario e indican a la herramienta qué optimizaciones se deben probar sobre qué partes del código. La salida se compone de un código de host autogenerado y una versión del kernel optimizada para una determinada plataforma. En esta primera versión de la herramienta se ha implementado como única técnica a aplicar por la herramienta el desenrollamiento de bucles. Los resultados experimentales muestran que del uso de la herramienta se pueden vii

10 derivar aceleraciones de hasta un 200 % para GPUs y un 15 % para CPUs. No se puede obviar tampoco el beneficio que supone la generación automática de códigos de host asociados a cada kernel, liberando así al programador del tedioso y repetitivo trabajo que supone su escritura. Palabras clave HPC, Paralelismo, OpenCL, GPGPU, Optimización, Clang.

11 Índice general 1. Introducción Motivación Objetivos Estado del arte Procesadores multinúcleo Surgimiento y popularización de la computación GPGPU Herramientas de computación GPGPU y heterogénea Técnicas de optimización automática Herramientas de análisis y transformación de código Metodología de desarrollo Planificación del trabajo Estructura del documento El estándar OpenCL Introducción Comunidad de desarrollo Evolución y situación actual del proyecto Implementaciones disponibles La arquitectura OpenCL Modelo de plataforma Modelo de ejecución Modelo de memoria ix

12 x ÍNDICE GENERAL Modelo de programación Programación de aplicaciones con OpenCL Desarrollo de un código de host Desarrollo de un código de kernel Comentarios LLVM y Clang La infraestructura de compilación LLVM Inciativas surgidas desde LLVM El frontend Clang Motivación Objetivos Características Arquitectura Análisis y transformación de código con Clang Introducción Análisis de código Transformación de código Comentario sobre los tutoriales La herramienta OCLOptimizer Descripción y funcionamiento de la herramienta Preprocesado Optimización Evaluación Ejemplo completo de ejecución Código original y código anotado Fichero de configuración Descripción del proceso de optimización iterativa Estructura Optimización implementada: Loop unrolling

13 ÍNDICE GENERAL xi Introducción teórica Implementación en la herramienta Detalles de diseño e implementación Interacción con Clang Modelado de las optimizaciones Resultados experimentales Producto matriz-vector Convolución de imágenes Resumen Conclusiones Resumen del trabajo realizado Objetivos alcanzados Líneas futuras A. Manual de usuario 115 A.1. Instalación A.1.1. Consideraciones previas A.1.2. Instalación de LLVM y Clang A.1.3. Instalación de la herramienta A.2. Uso de la herramienta A.2.1. Anotación del código A.2.2. Fichero de configuración B. Códigos del ejemplo completo de ejecución 121 B.1. Ficheros de entrada B.1.1. Códigos de kernel B.1.2. Fichero de configuración B.2. Ficheros de salida B.2.1. Versión óptima del kernel B.2.2. Código del host asociado a la versión óptima

14 xii ÍNDICE GENERAL B.3. Otros datos de interés B.3.1. Salida por pantalla de la herramienta B.3.2. Versión intermedia subóptima C. Contenido del disco 137

15 Índice de figuras 1.1. Representación de un pipeline gráfico genérico Pipeline gráfico compatible con OpenGL 4 y Direct3D Situación de OpenCL en las técnicas de paralelización actuales Miembros del OpenCL Working Group Eje temporal de la evolución del proyecto OpenCL Diagrama del modelo de plataforma de OpenCL Espacio bidimensional de work-items de un dispositivo OpenCL Ejemplo de identificación de work-items en un espacio bidimensional Diagrama de ejecución en orden y fuera de orden en una cola OpenCL Organización de las regiones de memoria en un dispositivo OpenCL Arquitectura de alto nivel de Clang Proceso general de optimización iterativa implementado en la herramienta Diagrama general de funcionamiento de OCLOptimizer Proceso de optimización iterativa ejecutado en el ejemplo Diagrama de clases de la herramienta Esquema de ejecución de un pipeline de 5 etapas Ejemplo de desenrollamiento de bucles con factor Ejemplo de desenrollamiento de bucles con factor Diagrama de clases del patrón Método Factoría original Detalle del diagrama de clases sobre el modelado de las optimizaciones. 98 xiii

16 xiv ÍNDICE DE FIGURAS 5.1. Aceleraciones obtenidas en GPU para multiplicaciones matriz-vector Aceleración en GPU para una multiplicación matriz-vector Convolución para una imagen de entrada 8 8 y máscara Aceleraciones obtenidas para la convolución de imágenes en GPU Aceleración en GPU para una convolución Aceleraciones obtenidas para la convolución de imágenes en CPU Aceleraciones obtenidas para una convolución en CPU

17 Índice de tablas 2.1. Tipos de reserva de memoria en OpenCL Visibilidad del acceso a memoria en OpenCL Tipos de dispositivos recogidos en el estándar OpenCL Tiempos de referencia de la multiplicación matriz-vector en GPU Tiempos de OCLOptimizer para multiplicaciones matriz-vector Tiempos de referencia de la convolución de imágenes en GPU Tiempos de referencia de la convolución de imágenes en CPU Tiempos de ejecución de OCLOptimizer para convolución de imágenes. 108 xv

18

19 Índice de listados de código 2.1. Ejemplo de kernel OpenCL: suma de vectores Ejemplo de definición de workspaces OpenCL para una suma de vectores Ejemplo de obtención de plataformas en un host OpenCL Ejemplo de obtención de dispositivos en un host OpenCL Ejemplo de creación de un contexto OpenCL Ejemplo de creación de una cola de comandos OpenCL Ejemplo de creación de buffers OpenCL para una suma de vectores Ejemplo de transferencia de datos para una suma de vectores Ejemplo de carga y compilación de un kernel OpenCL Paso de argumentos y ejecución del kernel OpenCL vecsum Ejemplo de recogida de datos para una suma de vectores Ejemplo de liberación de recursos OpenCL Ejemplo de instanciación de un Preprocessor de Clang Ejemplo de envío de ficheros de código a un Preprocessor de Clang Ejemplo de separación en tokens de un fichero de código C con Clang Ejemplo de diagnóstico para un fichero de cabeceras no encontrado Ejemplo de definición de opciones de búsqueda de cabeceras de Clang Definiciones necesarias para llamar a clang::parseast() Ejemplos de declaraciones de variables Ejemplo de sobreescritura de la función HandleTopLevelDecl Constructor de la clase MyRewriter Método parse() del ejemplo MyRewriter xvii

20 xviii ÍNDICE DE LISTADOS DE CÓDIGO Implementación del método Initialize() de MyConsumer Implementación del método HandleTopLevelDecl() de MyConsumer Implementación del método MyConsumer::HandleTopLevelSingleDecl() Implementación del método MyConsumer::HandleTranslationUnit() Método VisitStmt() de MyConsumer Expresión regular de formato de las directivas #pragma oclopts Ejemplo de uso de las anotaciones de optimización Ejemplo de transformación a función de las anotaciones de optimización Extracto del kernel de ejemplo anotado con directivas #pragma oclopts Desenrollamiento óptimo propuesto para el primer bucle Desenrollamiento óptimo propuesto para el segundo bucle Implementación del método UnrollAnnotation::ApplyAnnotation() Esquema del método UnrollingAnnotation::UnrollIncrement() Esquema del método UnrollingAnnotation::AdaptLoopCondition() Esquema de implementación de UnrollAnnotation::UnrollBody() Esquema del código de UnrollingAnnotation::UnrollStatement() Implementación de la clase AnnotationCreator Pseudocódigo del algoritmo de convolución B.1. Código original del kernel de ejemplo B.2. Código del kernel de ejemplo anotado con directivas #pragma oclopts. 123 B.3. Configuración de los parámetros generales de la ejecución del ejemplo. 124 B.4. Configuración de los parámetros de compilación del ejemplo B.5. Configuración de las dimensiones del espacio de trabajo del ejemplo B.6. Configuración del argumento de salida del ejemplo B.7. Configuración de argumentos de la primera multiplicación del ejemplo. 125 B.8. Configuración de argumentos de la segunda multiplicación del ejemplo. 125 B.9. Configuración del argumento de tamaño del problema del ejemplo B.10.Desenrollamientos propuestos en la versión seleccionada como óptima. 127 B.11.Código de host: definición del espacio de trabajo del ejemplo B.12.Código de host: definición e inicialización de buffers de usuario

21 ÍNDICE DE LISTADOS DE CÓDIGO xix B.13.Código de host: definición del contexto de trabajo y la cola de comandos 129 B.14.Código de host: definición de los buffers de transferencia de OpenCL B.15.Código de host: construcción del programa B.16.Código de host: paso de argumentos al kernel y ejecución B.17.Código de host: transferencia de datos a buffers de usuario B.18.Código de host: liberación de recursos de OpenCL B.19.Salida por pantalla: información de inicio B.20.Salida por pantalla: información del primer nivel de versiones B.21.Salida por pantalla: tiempos del primer nivel de versiones intermedias. 133 B.22.Salida por pantalla: información del segundo nivel de versiones B.23.Salida por pantalla: tiempos del segundo nivel de versiones B.24.Versión intermedia subóptima del kernel del ejemplo

22

23 Capítulo 1 Introducción La Ley de Moore, formulada por el co-fundador de Intel, Gordon Earl Moore, en 1965, predecía que el número de transistores que sería posible integrar en un mismo circuito se duplicaría cada dos años [1]. Este planteamiento, que gobernó y sigue gobernando los procesos de fabricación de semiconductores, sentó asimismo las bases de la arquitectura de computadores, habiendo permitido elevar sistemáticamente el nivel de integración de transistores en una misma oblea de silicio, y con él la capacidad de cómputo de los diferentes procesadores. Sin embargo, el mantener este crecimiento del nivel de integración a la vez que se aumenta la frecuencia de reloj de funcionamiento de los circuitos ha terminado por revelar las limitaciones de los materiales semiconductores actuales (problemas de disipación de calor, consumo excesivo de energía, etc.). Estas limitaciones han llevado a los fabricantes a reorientar su trabajo hacia nuevos diseños, como la replicación de núcleos con un nivel de integración elevado, pero a la vez con una frecuencia de funcionamiento que, aún siendo alta, no requiera de unos sistemas de disipación de calor tan complejos. Este paradigma de diseño y construcción ha dado lugar a lo que actualmente se conoce como procesadores multicore o multinúcleo. Hoy en día, la mayoría de los ordenadores cuentan con capacidades de procesamiento paralelo gracias al procesador de varios núcleos que incorporan y, en una porción cada vez creciente, también a las tarjetas gráficas con capacidades GPGPU. Con la 1

24 2 1. Introducción tecnología en esta situación, es altamente interesante contemplar la posibilidad de aunar los recursos convencionales de computación de los procesadores multinúcleo con las capacidades de propósito general de las GPUs, dando lugar a la llamada computación heterogénea, nuevo paradigma que trata de explotar de forma simultánea y coordinada las capacidades de todos estos recursos de naturaleza tan diversa [2]. OpenCL, que es un estándar industrial abierto [3][4][5] ideado para la programación de plataformas tan diferentes como CPUs, GPUs, DSPs o FPGAs, goza de creciente relevancia en este contexto. A continuación se comentará la motivación que ha llevado al desarrollo del presente proyecto (sección 1.1), así como los objetivos propuestos (sección 1.2). Tras ello se realizará un breve recorrido por el estado del arte de las tecnologías más relevantes (sección 1.3). Finalmente, se especificará la metodología y planificación seguidas para el desarrollo del proyecto (secciones 1.4 y 1.5) y se detallará la estructura en capítulos del presente documento (sección 1.6) Motivación Como se ha dicho en el apartado anterior, OpenCL es un estándar que permite la programación de dispositivos de muy variada índole (CPUs, GPUs, DSPs, FPGAs...), ofreciendo para sus códigos portabilidad funcional entre los distintos tipos de plataformas compatibles con el estándar. Sin embargo, esta portabilidad funcional no se ve reflejada en una portabilidad del rendimiento: un código que haya sido expresamente optimizado para su ejecución en una determinada plataforma muy probablemente obtendrá un rendimiento muy por debajo del óptimo en otra diferente. El trabajo de experimentación consistente en la escritura y prueba de distintas versiones de un código aplicando múltiples optimizaciones con distintos parámetros para generar las versiones óptimas de un código OpenCL para diferentes plataformas resulta especialmente pesado, sobre todo si éste tiene que ser realizado a mano por el programador. La motivación principal del presente proyecto es facilitar este trabajo al programador automatizando en la medida de lo posible este proceso de optimización. Para ello es interesante que

25 1.1. Motivación 3 el programador pueda guiar el proceso especificando qué optimizaciones desea aplicar sobre ciertas partes del codigo. La herramienta debería evaluar todas las posibilidades sugeridas automáticamente, proporcionando como salida una versión modificada del código de entrada de acuerdo a las optimizaciones planteadas. Los compiladores actuales son capaces de realizar optimizaciones automáticas de los códigos que procesan. Sin embargo, las transformaciones que realizan suelen ser de tipo source-to-binary, es decir, el compilador baraja diferentes optimizaciones y las aplica directamente al generar el binario correspondiente, que suele estar programado en código máquina (como es el caso de GCC, por ejemplo) o utilizando algún tipo de representación intermedia (por ejemplo, LLVM-IR). Así, con estos compiladores, el programador no es consciente en ningún momento de qué cambios concretos se han realizado en su código ni puede modificar fácilmente el resultado a posteriori. Sin embargo, sería interesante que al final del proceso el usuario obtenga un código optimizado, lo que le permitirá conocer qué partes del código han sido transformadas y de qué manera, para poder aplicar a posteriori otras optimizaciones adicionales de forma manual sobre el mismo si así lo desea. Por otra parte, una vez aplicada una transformación sobre cierta parte del código, al generarse el binario correspondiente, resultaría complejo mantener la información del resto de optimizaciones a probar y las porciones de código sobre las que serían aplicadas. Esto es especialmente cierto dado que muchas optimizaciones se refieren a transformaciones de código expresado sobre estructuras de control de alto nivel, cuyas semánticas se pierden o diluyen al transformarlas en código ejecutable. Por estos motivos, resulta especialmente importante que el usuario manipule en todo momento código de alto nivel. Así mismo, los trabajos actuales en este campo suelen centrarse en la aplicación de técnicas de optimización automática en problemas muy concretos, como operaciones SpMV 1 [6] o FFT 2 [7]. Lo que se pretende con la herramienta a implementar en este proyecto es proporcionar al prorgamador una forma rápida y accesible de realizar el proceso de optimización iterativa de cualquier kernel OpenCL. 1 Sparse Matrix-Vector Multiplication, multiplicación matriz dispersa-vector 2 Fast Fourier Transform, Transformada Rápida de Fourier

26 4 1. Introducción 1.2. Objetivos El objetivo principal que se pretende alcanzar con el presente proyecto es proporcionar a la comunidad de desarrolladores de OpenCL un mecanismo de asistencia a la optimización de sus códigos, intentando reducir al máximo el tiempo invertido en el proceso de aplicación de las distintas transformaciones sobre los mismos. Dicho mecanismo se basa en dos puntos fundamentales: por una parte, la generación del código de kernel óptimo a partir de la combinación de las optimizaciones propuestas, y por otra, la generación automática de los códigos de host necesarios para ejecutar dichos kernels. Para la generación automática de las diferentes versiones de prueba, el usuario determinará mediante una serie de directivas de compilación definidas a tal efecto qué optimizaciones desea probar sobre qué fragmentos de código, de modo que la herramienta localizará la aparición de las mismas en el kernel a optimizar y realizará sobre dichos fragmentos las transformaciones de código necesarias para aplicar las optimizaciones solicitadas. En cuanto a la generación automática de códigos de host, el usuario solamente debe especificar mediante un fichero de configuración, junto con otros parámetros de funcionamiento (variables de entrada y salida, dimensiones del problema, tamaño de los grupos de trabajo global y local...), en qué tipo de dispositivo desea probar su programa, generándose así para cada kernel un código de host adaptado exactamente a las condiciones establecidas. De esta forma se consigue eximir al programador de una tarea muy tediosa y repetitiva pero imprescindible para poder ejecutar cualquier kernel en dispositivos compatibles con OpenCL. Para poder alcanzar este objetivo, en primer lugar resulta imprescindible realizar un estudio de las diferentes técnicas de optimización que pueden ser de aplicación en códigos de kernel de OpenCL, siendo de especial relevancia los siguientes dos tipos: Técnicas de optimización secuencial, como pueden ser el desenrollamiento de bucles, con el que se busca explotar al máximo el paralelismo a nivel de instrucción intrínseco a cualquier procesador actual. Técnicas de optimización relacionadas con la organización de los elementos de

27 1.3. Estado del arte 5 procesado y la jerarquía de memoria de las tarjetas gráficas, como el cambio de granularidad de las tareas o el aprovechamiento de la memoria local. Por otra parte, para poder realizar las transformaciones asociadas a las diferentes optimizaciones, es necesario conocer la estructura del código de partida y poder manipularla en consecuencia. Para ello resulta también imprescindible estudiar las distintas opciones existentes para la realización de tareas de análisis y transformación de código. Así mismo, también resulta de vital importancia que el proceso de optimización iterativa implementado en la herramienta sea lo más eficiente y flexible posible Estado del arte A continuación se realiza una breve descripción de la evolución de las tecnologías en las que se basa el presente proyecto, haciendo especial hincapié en los cambios que ha experimentado la arquitectura de computadores en lo que respecta tanto a los procesadores de propósito general como a las tarjetas gráficas y su conversión en plataformas de computación, así como en los diferentes mecanismos de análisis, transformación y optimización de código disponibles Procesadores multinúcleo Tal y como se acaba de comentar en la introducción del presente capítulo, la explotación de las capacidades de los dispositivos semiconductores mediante el aumento sistemático tanto del nivel de integración de transistores como de la frecuencia de reloj de funcionamiento terminó por revelar sus limitaciones técnicas, surgiendo problemas derivados principalmente del elevado consumo de energía que comenzaban a experimentar los circuitos, la imposibilidad de disipar de ellos la gran cantidad de calor generada y la problemática de sincronización de la información entre sus componentes debido a las altas frecuencias de trabajo. Las primeras aproximaciones a la solución de este problema consistieron en el diseño

28 6 1. Introducción de nuevos procesadores que integraban, en un mismo chip, varias réplicas de procesadores completos conocidas como núcleos o cores, dando lugar a los llamados procesadores multinúcleo o multicore. Ejemplos muy conocidos de esto son las gamas de procesadores fabricados por Intel Core Duo, Core 2 Duo, Xeon o, más recientemente, Core i3, Core i5 y Core i7. Sin embargo, en los últimos años los distintos fabricantes están comenzando a explorar nuevas vías que les permitan aplicar esta idea de incluir varios núcleos en un mismo procesador sin necesidad de que dichos núcleos estén dedicados a las mismas tareas. Así han surgido diseños como las gamas de APUs 3 de AMD Fusion y, más recientemente, Heterogeneous, o las diferentes versiones de los procesadores Tegra de NVIDIA, en los que en un mismo chip se integran diferentes tipos de circuitería dedicados a diferentes tareas: computación de propósito general (CPU), procesamiento gráfico (GPU), decodificación dedicada de audio y vídeo, etc. En lo que respecta a AMD, ha orientado más sus desarrollos en este campo hacia su instalación en ordenadores completos, mientras que NVIDIA los ha enfocado más hacia el mercado de dispositivos móviles (tablets, smartphones, etc.). Por otra parte, inspirándose en todo el desarrollo que ha experimentado la computación de propósito general en GPUs (conocida como computación GPGPU ), el cual será comentado con cierto detalle en el apartado 1.3.2, Intel ha presentado recientemente la arquitectura MIC, acrónimo de Many Integrated Core. Con esta arquitectura masivamente paralela, aún en pleno desarrollo, es posible integrar en un dispositivo que se conecta a un ordenador mediante una conexión PCI-Express más de 50 núcleos de propósito general y que además son compatibles con las herramientas tradicionales de programación paralela para sistemas x Surgimiento y popularización de la computación GPGPU En sus inicios, a finales de la década de 1960 y principios de la de 1970, la finalidad de las tarjetas gráficas no era otra que realizar el trabajo necesario para poder mostrar 3 Accelerated Processing Unit, en inglés, Unidad de Procesamiento Acelerado.

29 1.3. Estado del arte 7 textos y formas sencillas en la pantalla de un ordenador, siendo responsabilidad de la CPU toda la computación previa necesaria. Con el paso del tiempo, las necesidades de la industria del software en lo que a capacidades gráficas se refería fue creciendo espectacularmente, las cuales fueron siendo satisfechas poco a poco añadiendo a las tarjetas circuitos específicos que acelerasen ciertas fases del procesamiento gráfico, liberando así a la CPU de dichos trabajos. Dichas fases constituyen lo que se conoce como un pipeline gráfico, del cual puede verse un ejemplo sencillo en la figura 1.1. En las primeras implementaciones de estos pipelines, todas las fases realizaban un trabajo previamente establecido en el hardware, no siendo posible ningún tipo de reprogramación de las mismas. Estos pipelines se fueron sofisticando hasta alcanzar las tecnologías disponibles actualmente, que permiten la programación de algunas de sus fases, como es el caso del ejemplo mostrado en la figura 1.2. El objetivo de la computación GPGPU es aprovechar las capacidades de ese tipo de unidades de procesamiento para tareas de propósito general, en lugar de limitarlas a su uso tradicional. Figura 1.1: Representación de un pipeline gráfico genérico Uno de los ejemplos más conocidos de este tipo de plataformas de computación es la familia de dispositivos Tesla fabricados por NVIDIA, en la cual es posible encontrar desde tarjetas gráficas domésticas con capacidades GPGPU adicionales hasta

30 8 1. Introducción Figura 1.2: Representación de un pipeline gráfico compatible con OpenGL 4 y Direct3D 11 pequeños clusters ya ensamblados y exclusivamente dedicados a este tipo de computación. Además, de entre los 10 primeros supercomputadores de la edición de junio de 2012 del TOP500 [8], varios de ellos cuentan con dispositivos de este tipo como recursos de computación Herramientas de computación GPGPU y heterogénea Como se acaba de comentar en el apartado anterior, es posible reprogramar algunas de las unidades del pipeline de las tarjetas gráficas utilizando APIs específicas para gráficos 2D y 3D como OpenGL o Direct3D. Sin embargo, este tipo de APIs se basan en las abstracciones gráficas que se manejan en las diferentes fases del pipeline, como pueden ser texturas, geometrías o proyecciones. Estos conceptos resultan imprescindibles en el tratamiento de gráficos, pero pueden resultar complicados de aplicar en tareas de computación de propósito general. Con el objetivo de ocultar parcialmente estos deta-

31 1.3. Estado del arte 9 lles o, al menos, ayudar al programador a su gestión, diferentes fabricantes y grupos de trabajo han ido liberando al mercado diversas propuestas de lenguajes o herramientas para la programación de dispositivos GPGPU. Entre ellas se pueden destacar algunas como Microsoft DirectCompute, BrookGPU, CUDA o OpenCL. Estas plataformas se sirven de diferentes lenguajes para la programación de los dispositivos compatibles con las mismas, de entre los cuales las extensiones de C para OpenCL y CUDA son los que actualmente han alcanzado una posición de relevancia en el mercado. Comparativamente, se puede atribuir a la extensión de CUDA para C cierta ventaja sobre OpenCL en lo que a rendimiento respecta, en tanto en cuanto se trata de un lenguaje especialmente diseñado para el aprovechamiento de las capacidades de los dispositivos fabricados por NVIDIA compatibles con CUDA. Sin embargo, OpenCL no presenta estas restricciones de compatibilidad y permite desarrollar programas para su ejecución ya no sólo en gran cantidad de tarjetas gráficas (independientemente de su fabricante), sino también, como ya se ha comentado, en otros dispositivos como CPUs, DSPs o FPGAs. La figura 1.3 ilustra este concepto representando OpenCL como la plataforma de trabajo que cubre la intersección entre procesadores multinúcleo capaces de ejecutar una gran cantidad de procesos de forma simultánea, y por otra, las capacidades existentes en las GPUs actuales para tratar de forma paralela grandes volúmenes de datos. De esta forma, OpenCL es un estándar de computación heterogénea [3][4][5] que permite ejecutar un mismo código sobre diferentes plataformas, obteniendo de este modo una auténtica portabilidad funcional. Sin embargo, el rendimiento de dicho código puede no ser portable, de forma que varíe notablemente entre plataformas, y siendo por tanto tarea del programador intentar ajustar su programa a las características de cada una de ellas. En el capítulo 2 se proporciona más información acerca del estándar OpenCL Técnicas de optimización automática Se procede a continuación a comentar el estado actual de las diferentes técnicas de optimización de código.

32 10 1. Introducción Figura 1.3: Situación de OpenCL en las técnicas de paralelización para CPUs y GPUs Optimización directa Es habitual que los compiladores actuales cuenten con multitud de opciones que permitan al desarrollador probar diferentes optimizaciones sobre el código original a compilar. Ejemplos muy conocidos y ampliamente utilizados de ello son los conjuntos de optimizaciones ofrecidos por compiladores como GNU C Compiler [9] o Intel C++ Compiler [10]. Con estas optimizaciones, el compilador intenta mejorar el rendimiento y/o el tamaño del programa resultante a expensas de una mayor duración del proceso de compilación o de poder depurar el programa mediante depuradores como GDB. El nivel de detalle que se puede alcanzar a la hora de aplicar las diferentes optimizaciones proporcionadas por alguno de estos compiladores puede llegar a ser muy elevado, dependiendo siempre del dominio que el programador tenga del lenguaje y de las posibilidades que éste le ofrece. Por regla general, los compiladores ofrecen paquetes de optimizaciones, como por ejemplo los asociados a las opciones -O1, -O2, -O3, etc. de GCC, que permiten a los desarrolladores mejorar sus códigos sin realizar un estudio exhaustivo de las posibles optimizaciones a aplicar. Junto a estos paquetes de optimizaciones, los compiladores ofrecen opciones para aplicar optimizaciones concretas [11] como desenrollamientos o vectorizaciones de bucles, inlining de funciones, información acerca del no solapamiento de regiones de memoria accedidas por determinados punteros...

33 1.3. Estado del arte 11 Optimización iterativa Los compiladores hacen un uso extensivo de técnicas directas de optimización para conseguir mejorar el rendimiento de los programas. Sin embargo, la aplicación de dichas optimizaciones suele estar basada en análisis estáticos del código a partir de modelos simplificados de las máquinas o en heurísticas simples que a menudo se muestran insuficientes. El problema de este enfoque reside en la incompletitud intrínseca del análisis estático de código [12], lo que supone que, pese a afinar bastante el rendimiento, los compiladores basados en estas técnicas de optimización no puedan determinar de forma directa cuál es la mejor optimización aplicable a un código para obtener el mejor rendimiento posible en una plataforma concreta. La solución que se ha venido planteando en los últimos años como alternativa para suplir estas carencias ha sido la llamada compilación iterativa. Esta técnica consiste en la aplicación explícita de sucesivas transformaciones de código sobre un programa dado, las cuales son evaluadas a fin de seleccionar la mejor o las mejores antes de pasar a experimentar con otras transformaciones sobre ellas. El proceso de selección se realiza mediante mediante la ejecución real de las diferentes versiones generadas, o, de forma más sofisticada, analizando y prediciendo su comportamiento mediante diferentes modelos de rendimiento o heurísticas. La principal desventaja de esta técnica reside en un incremento notable del tiempo de compilación, debido al alto coste de evaluar todas las versiones generadas para seleccionar la óptima. Los grandes costes de la compilación iterativa la han relegado tradicionalmente al campo de la computación embebida, donde el tamaño de los programas suele ser reducido y el proceso de compilación se limita a las fases de desarrollo del producto, de modo que una vez establecida la versión óptima para un programa y un dispositivo dados, no debería de ser necesaria la repetición del proceso. Evidentemente, el uso de esta técnica en grandes códigos basados en paradigmas tradicionales de programación se seguirá viendo afectado por este importante inconveniente. Sin embargo, la aplicación exitosa durante años de esta técnica en códigos embebidos anima a probarla con códigos de propósito general, pero que cuentan con la ventaja de ser de reducido ta-

34 12 1. Introducción maño como, por ejemplo, los kernels que se utilizan en lenguajes orientados a GPGPU como son CUDA u OpenCL. Otro motivo que pone en valor el uso de técnicas de optimización iterativa en entornos GPGPU es el continuo cambio en el diseño de este tipo de arquitecturas, lo que hace especialmente complicado mantener y publicar, con el tiempo suficiente, compiladores que puedan explotar al máximo las nuevas características que dichos dispositivos van incorporando Herramientas de análisis y transformación de código Debido a la elevada complejidad que ello comporta, se ha descartado el desarrollo desde cero de un mecanismo de análisis y transformación de código. Así, se procedió a la búsqueda de herramientas alternativas para la realización de este tipo de tareas, comenzando por aquellas de uso más habitual, como, por ejemplo, GNU C Compiler. Si bien el uso más conocido de GCC es como herramienta de compilación de código, existe también la posibilidad de utilizar los componentes que lo forman para construir herramientas que realicen diversas transformaciones de código. Sin embargo, GCC ha sido construido como un compilador monolítico y estático, lo que complica bastante su integración en otras herramientas. Así mismo, tanto la evolución histórica como la política actual que gobierna su diseño hacen complicado desacoplar el frontend del resto del compilador [13]. Una alternativa que actualmente está tomando una posición importante en este campo es la infraestructura de compilación LLVM [14]. De todas formas, el uso directo de este compilador no sería lo más adecuado para este proyecto, ya que funciona siguiendo un paradigma source-to-binary. Sin embargo, Clang, frontend para C, C++ y Objective- C del compilador LLVM, permite realizar operaciones de tipo source-to-source con el código. Si bien su finalidad principal es la misma que la de GCC, es decir, su uso como herramienta de compilación, en este caso sí está documentado su uso alternativo como mecanismo de análisis y transformación de código. Por otra parte, como ya se

35 1.4. Metodología de desarrollo 13 ha comentado, cuenta con un diseño más apropiado para su integración en otras herramientas a través de una API. Estos dos motivos fueron resultaron decisivos para el uso de Clang en el desarrollo de la herramienta Metodología de desarrollo La metodología de desarrollo empleada se basa en un proceso incremental basado en prototipos. Se ha elegido este enfoque de trabajo gracias, principalmente, a la flexibilidad que éste permite en caso de ser necesario corregir decisiones erróneas o modificar funcionalidades procedentes de incrementos anteriores. De hecho, desde el principio se consideró que la probabilidad de tener que hacer frente a este tipo de problemáticas era considerable debido a la elevada complejidad asociada al uso a bajo nivel de las capacidades de análisis léxico y sintáctico de código C proporcionadas por Clang. En una fase inicial se realizó una toma de contacto con las herramientas básicas de trabajo (Clang y OpenCL), implementando un prototipo capaz de procesar un kernel OpenCL y de utilizar las funciones correspondientes de Clang para conocer su estructura. En fases posteriores, y tomando como base dicho prototipo, se han ido incorporando diferentes funcionalidades hasta, finalmente, obtener una implementación completamente funcional de la herramienta, capaz de aplicar a un kernel las optimizaciones especificadas por el usuario y evaluar las versiones generadas a partir de las mismas Planificación del trabajo Las características de un proyecto de investigación y desarrollo como éste hacen especialmente complicado el establecimiento de una planificación tradicional del trabajo, realizada a priori. En concreto, la falta de experiencia con gran parte de las tecnologías a utilizar, así como la necesidad de estudiar la viabilidad de la realización de los objetivos marcados utilizando dichas tecnologías, imposibilitó el establecimiento a priori de una planificación temporal fiable, por lo que se decidió prescindir de ella.

36 14 1. Introducción 1.6. Estructura del documento Junto con este primer capítulo introductorio, la presente memoria se compone de los siguientes capítulos, cuyo contenido se resume a continuación: Capítulo 2 Este capítulo se dedica íntegramente a la presentación del estándar de computación heterogénea OpenCL, comentando sus principales características y diseccionando punto por punto su arquitectura. Capítulo 3 En este capítulo se presenta la infraestructura de compilación LLVM, así como la estructura y capacidades de análisis y transformación de código del frontend para C/C++ Clang. Capítulo 4 En este capítulo se describe el resultado final del proceso de desarrollo de esta herramienta de optimización iterativa, la cual constituye el principal objetivo del presente proyecto. Capítulo 5 En este capítulo se detallarán los resultados experimentales obtenidos en las pruebas de optimización realizadas con la herramienta para diferentes problemas implementados en OpenCL, tanto sintéticos como reales. Capítulo 6 Este capítulo, con el que se cierra el presente documento, establece las conclusiones extraídas del trabajo realizado y planteando posibles líneas futuras de investigación y desarrollo. Así mismo, se incluyen los siguientes apéndices: Apéndice A Este apéndice recoge un breve manual de usuario de la herramienta, explicando el formato de las anotaciones de código y el proceso de instalación. Apéndice B En este apéndice se incluyen los códigos y ficheros de configuración más relevantes que intervienen en un ejemplo completo de ejecución de la herramienta. Apéndice C En este apéndice se lista el contenido del soporte de almacenamiento óptico adjunto.

37 Capítulo 2 El estándar OpenCL El presente capítulo pretende servir de introducción al estándar de computación heterogénea OpenCL ya citado y brevemente comentado en apartados anteriores. Se comenzará en la sección 2.1 con una introducción de dicho proyecto, para después entrar en profundidad en la sección 2.2 en detalles como la arquitectura de la plataforma y los modelos que la componen. Finalmente se describirá paso a paso en la sección 2.3 el procedimiento a seguir para la implementación de un sencillo kernel OpenCL y su posterior ejecución en un dispositivo compatible con el estándar Introducción OpenCL (acrónimo de Open Computing Language) es un estándar industrial abierto [4] ideado para la programación de plataformas tan heterogéneas como CPUs, GPUs e incluso otros procesadores como DSPs 1 o FPGAs 2. Bajo el estándar OpenCL se define un framework de programación paralela compuesto por un lenguaje de programación basado en C99, una API, diversas librerías y una plataforma de ejecución. De esta manera, OpenCL proporciona una abstracción de bajo nivel que permite acceder, a través del framework, a una gran cantidad de detalles específicos del hardware subyacente. 1 Digital Signal Processor, procesador digital de propósito específico para señales. 2 Field Programmable Gate Array, dispositivo semiconductor de lógica programable. 15

38 16 2. El estándar OpenCL Comunidad de desarrollo Como ya se ha comentado, OpenCL es un estándar abierto mantenido por el consorcio tecnológico sin ánimo de lucro Khronos Group. Sin embargo, el responsable inicial de su desarrollo y evolución fue Apple, a quien después acompañaron otras empresas de la talla de AMD, IBM, Intel o NVIDIA. En la figura 2.1 puede apreciarse la diversidad de firmas dentro de los múltiples sectores de la investigación y la industria tecnológica que participan de uno u otro modo en el OpenCL Working Group: fabricantes de procesadores y FPGAs, desarrolladores de middleware y de aplicaciones, instituciones universitarias, laboratorios y centros de investigación... Figura 2.1: Miembros del OpenCL Working Group Evolución y situación actual del proyecto A continuación se comenta el proceso de desarrollo del proyecto OpenCL desde sus orígenes con la formación de un grupo de trabajo al respecto en el seno del Khronos Group hasta la publicación en noviembre de 2011 de la especificación de la que será la próxima versión, OpenCL 1.2. La figura 2.2 resume en un eje temporal los hitos principales de la evolución del proyecto OpenCL, la cual se comenta, a continuación, versión por versión. Figura 2.2: Eje temporal de la evolución del proyecto OpenCL

39 2.1. Introducción 17 OpenCL 1.0 En junio de 2008 fue constituido el correspondiente grupo de trabajo, contando el mismo con la participación de compañías de los sectores de CPUs, GPUs, procesadores embebidos y software. Este primer grupo trabajó durante 5 meses hasta concretar los detalles técnicos de la especificación de OpenCL 1.0 [3] en noviembre de Un mes más tarde, una vez revisado, se aprobó su lanzamiento al público, siendo incorporado por Apple a su sistema operativo Mac OS X Snow Leopard. OpenCL 1.1 La versión 1.1 de OpenCL [4] fue ratificada por el Khronos Group en junio de 2010, y con ella se añadieron mejoras de cara al rendimiento y la flexibilidad de programación de los dispositivos, como por ejemplo: Nuevos tipos de datos como vectores de 3 componentes o formatos de imágenes. Operaciones sobre regiones completas de un buffer, tales como lectura, escritura y copia de regiones rectangulares en 1D, 2D y 3D. Uso mejorado de los eventos para gestionar y controlar la ejecución de comandos. OpenCL 1.2 En noviembre de 2011, el Khronos Group anunció la especificación de la versión 1.2 de OpenCL [5], que viene a complementar todavía más las funcionalidades añadidas en versiones anteriores, sobre todo en lo que a rendimiento y programación paralela se refiere. Algunas de estas nuevas características son las siguientes: Particionado de dispositivos, de modo que sea posible dividir un dispositivo en subdispositivos a los que asignar tareas como si de unidades individuales de computación se tratase. Esto resulta especialmente útil para reservar zonas con-

40 18 2. El estándar OpenCL cretas de los dispositivos y así reducir la latencia de operaciones que sean críticas en tiempo. Separación de la compilación y enlazado de objetos, de manera que sea posible compilar OpenCL en librerías externas para su inclusión en otros programas. Built-in kernels que implementan funcionalidades específicas o no programables propias de los diferentes dispositivos subyacentes, como por ejemplo, codificación y decodificación de vídeo o procesado digital de señales Implementaciones disponibles Aunque se trate de un estándar mantenido por el Khronos Group, esta organización simplemente se encarga de gobernar el proceso de desarrollo y evolución de las diferentes especificaciones de OpenCL, dejando en manos de la industria la implementación del estándar para su uso en los diferentes dispositivos compatibles. Han sido precisamente algunos de los miembros del OpenCL Working Group quienes han desarrollado sus propias implementaciones de OpenCL, orientándolas y optimizándolas en cada caso de acuerdo con sus propios objetivos empresariales. Ejemplo de ello son AMD (especialmente orientada hacia sus APUs Heterogeneous y Fusion), NVIDIA (como capa de abstracción sobre la arquitectura CUDA), Intel (para las últimas generaciones de sus procesadores) o IBM (para sus procesadores Power, habituales en los equipos de supercomputación que comercializan) La arquitectura OpenCL Las ideas que subyacen de la definición del estándar OpenCL se organizan de acuerdo a una arquitectura basada en una jerarquía de modelos compuesta por los modelos de plataforma (Platform Model), ejecución (Execution Model), memoria (Memory Model) y programación (Programming Model).

41 2.2. La arquitectura OpenCL Modelo de plataforma OpenCL plantea un modelo que pueda servir de abstracción sobre la arquitectura concreta de las diferentes plataformas de computación compatibles con el estándar. Según este modelo, que pretende ilustrar la figura 2.3, toda plataforma compatible se compone de un host conectado a uno o más dispositivos OpenCL. Cada dispositivo OpenCL se divide en una o más unidades de computación (CUs, Computing Units), las cuales a su vez están divididas en uno o más elementos de procesado (PEs, Processing Elements), siendo en este último nivel del dispositivo donde se realiza el trabajo de computación. Figura 2.3: Diagrama del modelo de plataforma de OpenCL Una aplicación OpenCL se ejecuta en un host teniendo en cuenta las características específicas del mismo. Desde dicho host, la aplicación envía comandos a los elementos de procesado de los dispositivos, de modo que aquellos que se encuentran agrupados en una misma unidad de computación ejecutan un mismo flujo de instrucciones, bien como unidades SIMD, bien como unidades SPMD, según las necesidades de la aplicación Modelo de ejecución La ejecución de un programa OpenCL tiene lugar en dos partes: kernels que se ejecutan en uno o más dispositivos y un programa host que se ejecuta sobre el mismo y define el contexto de trabajo de los kernels y gestiona su ejecución.

42 20 2. El estándar OpenCL Organización del trabajo El núcleo del modelo de ejecución de OpenCL viene definido por cómo se ejecutan los kernels. Cuando el host envía un kernel para su ejecución, se define un espacio de trabajo, de modo que se ejecuta una instancia de dicho kernel para cada punto del espacio previamente definido. Esta instancia recibe el nombre de work-item (elemento de trabajo) y se identifica de forma global por el punto que lo representa en el espacio de trabajo, el cual se define como una matriz de varias dimensiones. Cada work-item ejecuta el mismo código, aunque el camino concreto de cada ejecución y los datos que en ésta se manipulan pueden variar entre los diferentes work-items. A su vez, los work-items pueden organizarse en work-groups, los cuales también se identifican por su posición en el espacio de trabajo e, internamente, se organizan como matrices de varias dimensiones formando un espacio local. Las instancias asociadas a los work-items de un work-group dado se ejecutan concurrentemente en los elementos de procesado de una misma unidad de computación. La figura 2.4 muestra cómo se organiza un espacio de trabajo bidimiensional en OpenCL, pudiendo verse en la figura 2.5 un ejemplo de sencillo de identificación de work-items en un espacio bidimensional de tamaño (8, 12) subdividido en grupos de tamaño (4, 4). Figura 2.4: Espacio bidimensional de work-items de un dispositivo OpenCL

43 2.2. La arquitectura OpenCL 21 Figura 2.5: Ejemplo de identificación de work-items en un espacio bidimensional Contexto de ejecución Una de las principales finalidades del host es definir y gestionar el contexto en el que se ejecutarán los diferentes kernels. Este contexto es creado y manipulado por el host mediante un código de host construido utilizando las funciones del API de OpenCL. El contexto gestiona, entre otras cosas, los dispositivos OpenCL presentes en la plataforma, los kernels que se ejecutarán sobre los dispositivos, los objetos de programa (ejecutables de los kernels) y los objetos de memoria (buffers de transferencia de datos entre host y kernel). Colas de comandos El host crea una estructura de datos llamada cola de comandos para coordinar la ejecución de los kernels en los diferentes dispositivos, de modo que el host envía comandos a dicha cola para que éstos sean planificados sobre los dispositivos incluidos en el contexto. Dichos comandos pueden dividirse de acuerdo a la siguiente clasificación: Comandos de ejecución de kernel: Ejecutan un kernel sobre los elementos de procesado de un dispositivo. Comandos de memoria: Transfieren datos a, desde, o entre objetos de memoria, o bien reservan y liberan el espacio asociado a los mismos en el host.

44 22 2. El estándar OpenCL Comandos de sincronización: Determinan el orden de ejecución del resto de comandos. Como ya se ha comentado, la cola de comandos planifica la ejecución de los mismos en un dispositivo. Después, éstos se ejecutan de manera asíncrona entre dicho dispositivo y el host de acuerdo a alguno de los siguientes modos cuyo funcionamiento ilustra la figura 2.6 y que a continuación se relacionan: Figura 2.6: Diagrama de ejecución en orden y fuera de orden en una cola OpenCL Ejecución en orden: Los comandos son lanzados y completados en el orden en que han sido encolados, de modo que un comando que aparezca primero en la cola siempre se completará antes de que comience el siguiente. Como puede deducirse, este modo supone la serialización de todos los comandos encolados. Ejecución fuera de orden: Los comandos son lanzados en orden, pero no se espera a la finalización de un comando anterior para la ejecución de otro posterior. En este caso, es responsabilidad expresa del programador utilizar los comandos de sincronización pertinentes para que la ejecución del código de host sea la esperada. Los comandos de memoria y de ejecución de kernel encolados generan los llamados objetos de evento, los cuales pueden ser utilizados para controlar la ejecución entre comandos y para coordinar la ejecución entre el host y los dispositivos. Así mismo, es posible asociar múltiples colas bajo un mismo contexto. Utilizando este modo de trabajo, las colas se ejecutan de forma concurrente pero independientemente, sin que OpenCL garantice ningún mecanismo explícito de sincronización entre las mismas.

45 2.2. La arquitectura OpenCL 23 Categorías de kernels El modelo de ejecución de OpenCL soporta dos categorías diferentes de kernels. Por una parte, kernels de OpenCL, escritos utilizando la extensión de C para OpenCL y compilados con el compilador de OpenCL. Todas las implementaciones del estándar han de soportar este tipo de kernels, si bien éstas pueden proporcionar otros mecanismos para su creación. Por otra se encuentran los llamados kernels nativos, que son accedidos mediante un puntero a función y son encolados para su ejecución en un dispositivo con el resto de kernels OpenCL, con los que además comparten los objetos de memoria. Ejemplos de ello serían funciones definidas en un código de aplicación o procedentes de una librería. Nótese que esta funcionalidad es meramente opcional y que la semántica de los kernels nativos es dependiente de cada implementación, de modo que lo único que se incluye en el API de OpenCL son funciones para consultar las capacidades de un dispositivo y determinar si alguna concreta está soportada Modelo de memoria Los work-items encargados de la ejecución de un kernel pueden acceder a cuatro regiones de memoria diferentes organizadas de acuerdo al esquema de la figura 2.7 y que a continuación se citan ordenadas de menor a mayor velocidad de acceso: Figura 2.7: Organización de las regiones de memoria en un dispositivo OpenCL

46 24 2. El estándar OpenCL Memoria global: Esta región de memoria permite accesos de lectura-escritura a todos los work-items de todos los work-groups, pudiendo leer o escribir datos en cualquier elemento de un objeto de memoria. Así mismo, dependiendo de las capacidades del dispositivo, las lecturas y escrituras en memoria global podrían almacenarse en caché. Memoria constante: Es una región de la memoria global que permanece constante durante la ejecución de un kernel, siendo el host el encargado de reservar e inicializar los objetos de memoria almacenados en esta región. Memoria local: Es una región local respecto de un work-group, la cual puede ser utilizada para reservar variables compartidas por todos los work-items del grupo. En algunos dispositivos se implementa como una región exclusiva, mientras que en otros se utilizan secciones de la memoria global dedicadas. Memoria privada: Una región de memoria privada para cada work-item, de modo que las variables definidas en la misma no son visibles por los demás items. Las tablas 2.1 y 2.2 describen en qué casos en que tanto el kernel como el host pueden reservar memoria de una región concreta, en qué forma pueden hacerlo (estática o en tiempo de compilación, diánimca o en tiempo de ejecución) y qué tipo de acceso les está permitido (sólo lectura, lectura-escritura o acceso prohibido). Global Constante Local Privada Host Dinámica Dinámica Dinámica Sin reserva Kernel Sin reserva Estática Estática Estática Tabla 2.1: Tipos de reserva de memoria en OpenCL Global Constante Local Privada Host R-W R-W Sin acceso Sin acceso Kernel R-W Sólo lectura R-W R-W Tabla 2.2: Visibilidad del acceso a memoria en OpenCL

47 2.2. La arquitectura OpenCL 25 El código de host se sirve del API de OpenCL para crear objetos de memoria en la región global, así como para encolar aquellos comandos de memoria que operen sobre dichos objetos. Los modelos de memoria del host y del dispositivo OpenCL son, en su mayoría, independientes entre sí. Esto resulta necesario dado que el host está definido fuera de OpenCL. Sin embargo, ambas partes tienen que poder interactuar, lo cual sucede de una de las dos siguientes formas: copiando datos explícitamente o asignando y desasignando regiones de un objeto de memoria. Para copiar datos de forma explícita, el host encola comandos para transferir los datos entre el objeto de memoria y la suya propia, pudiendo ser estos comandos tanto bloqueantes como no bloqueantes. La llamada a la función de OpenCL encargada de realizar una transferencia bloqueante de memoria finaliza una vez los recursos de memoria asociados al host pueden ser reutilizados de forma segura. Para una transferencia no bloqueante, la función de OpenCL finaliza tan pronto como el comando es encolado, sin importar si es seguro o no reutilizar la memoria del host. El método de asignación/desasignación permite al host asociar una región del objeto de memoria a su espacio de direcciones. De la misma forma que sucede con el método de copia explícita, en este caso el comando de memoria correspondiente también puede ser bloqueante o no bloqueante. Una vez se ha realizado la asociación entre el objeto de memoria y el host, este último puede leer o escribir en esa región. El propio host se encarga de desasignar la región una vez se hayan completado los accesos (lecturas y/o escrituras) a la misma. Consistencia de memoria OpenCL se basa en un modelo de consistencia relajada de memoria, de modo que no se garantiza que el estado de la memoria visible para un work-item sea consistente en todo momento para el resto de work-items. Dentro de cada work-item sí se cumple una consistencia de carga/almacenamiento (load/store consistency). La memoria local es consistente para todos los work-items dentro del mismo grupo cuando la ejecución alcanza una barrera que afecte a dicho grupo, lo cual también se cumple para la memoria

48 26 2. El estándar OpenCL global. Sin embargo, para este tipo de memoria no existen garantías de consistencia entre grupos diferentes. Finalmente, la única forma de garantizar la consistencia de memoria entre objetos compartidos por distintos comandos encolados es introducir un punto de sincronización en el momento que se desee comprobar el estado de la memoria Modelo de programación Como ya se introdujo en apartados anteriores, el modelo de ejecución de OpenCL soporta los modelos de programación paralela basados tanto en datos como en tareas, así como versiones híbridas de los mismos. Aunque sea compatible con ambos, el diseño de OpenCL ha sido realizado siguiendo el modelo de paralelismo de datos. Paralelismo de datos Este modelo parte de la definición de operación como una secuencia de instrucciones aplicada simultáneamente a múltiples elementos de un objeto de memoria. El espacio de índices asociado al modelo de ejecución de OpenCL define los work-items y la asociación del conjunto de datos a los mismos. Si se aplicase el paralelismo de datos de forma estricta, existiría siempre una relación uno a uno entre cada work-item y cada elemento del objeto de memoria sobre el que se ejecutará, en paralelo, el kernel. OpenCL, en cambio, implementa una versión relajada del modelo que no siempre requiere de esta asociación. OpenCL proporciona un modelo jerárquico de programación paralela de datos, habiendo dos formas para especificar esta subdivisión jerárquica. En el modelo explícito el programador define el número total de work-items que trabajarán en paralelo, así como la agrupación de éstos para formar work-groups. En el modelo implícito, el programador solamente especifica el número total de work-items, dejando que sea la implementación de OpenCL quien gestione la división en work-groups.

49 2.2. La arquitectura OpenCL 27 Paralelismo de tareas El paralelismo basado en tareas implementado en OpenCL se basa en un modelo en el que se ejecuta una sola instancia de un kernel independientemente del espacio de índices. Lógicamente, equivale a ejecutar un kernel en una unidad de computación que cuente con un work-group formado por un único work-item. Bajo este modelo, los usuarios pueden extraer paralelismo utilizando los tipos de datos vectoriales implementados por el dispositivo, encolando varias tareas o encolando kernels nativos desarrollados usando un modelo de programación ortogonal a OpenCL. Sincronización OpenCL ofrece dos posibilidades diferentes para la inserción de puntos de sincronización, bien entre los work-items de un mismo work-group, bien entre los comandos encolados bajo un mismo contexto. La sincronización entre work-items dentro de un mismo work-group se consigue mediante una barrera de work-group que forzará que todos los items la alcancen antes de que sea posible continuar con la ejecución más allá de la misma. Nótese que no es posible la definición de barreras parciales de este tipo, de modo que la ejecución solamente proseguirá si está definida y la alcanzan todos los items, o bien si directamente no se encuentra definida. Así mismo, no existe mecanismo alguno de sincronización entre grupos. En lo que respecta a la sincronización entre comandos de una misma cola, existen dos posibilidades: Barrera de cola de comandos: Este tipo de barrera asegura que todos los comandos previamente encolados han finalizado su ejecución y que todas las actualizaciones de los objetos de memoria implicados serán visibles para los comandos subsiguientes antes de que comiencen a ejecutarse. Esta barrera solamente puede usarse para sincronizar comandos dentro de una misma cola. Espera por un evento: Todas las funciones de la API que resultan en la inserción de comandos en la cola devuelven un evento que identifica al comando y a

50 28 2. El estándar OpenCL los objetos de memoria que modifica. Si un comando subsiguiente espera por la aparición de dicho evento, queda garantizada la visibilidad de las modificaciones previas sobre los objetos de memoria antes de su ejecución Programación de aplicaciones con OpenCL Tal y como se ha ido comentando a lo largo del presente capítulo, para poder programar cualquier tipo de aplicación utilizando OpenCL es necesario establecer el comportamiento de las dos principales partes de cualquier plataforma de computación compatible con el estándar: el host y los dispositivos. Para la programación de los dispositivos, encargados de realizar las tareas de computación, es necesario desarrollar lo que se conoce como código de kernel. Este código es el que implementará las funciones propiamente dichas, y se escribe utilizando la extensión de C para OpenCL. Por su parte, el host debe ejecutar lo que se conoce como código de host, cuya función principal es definir y gestionar del contexto de ejecución utilizando las funciones del API que OpenCL proporciona a tal efecto. A continuación se explica, para un ejemplo sencillo (una suma de vectores), los pasos a seguir para desarrollar ambos tipos de código. El listado 2.1 contiene la implementación de un kernel que realiza la suma de dos vectores con valores de tipo float y longitud N. Aunque a simple vista se asemeja bastante a una función convencional escrita en C, existen ciertas características que serán aclaradas posteriormente. 1 kernel void vecsum ( global const float *a, global const float *b, global float *c, unsigned int N) 2 { 3 unsigned int xid = get_ global_ id (0) ; 4 if (xid <N) 5 c[ xid ] = a[ xid ]+b[ xid ]; 6 } Listado 2.1: Ejemplo de kernel OpenCL: suma de vectores

51 2.3. Programación de aplicaciones con OpenCL 29 De todas formas, para poder explicar de forma clara los detalles del código mostrado del listado, es necesario comentar antes los pasos que es necesario seguir para implementar el código de host que cree y gestione el contexto adecuado para la ejecución del kernel Desarrollo de un código de host Para poder definir el contexto adecuado para ejecutar el kernel del listado, es necesario desarrollar un código de host que realice las siguientes operaciones: Definición de los espacios de trabajo Tal y como se ha comentado en el apartado 2.2.2, OpenCL distribuye la ejecución de los kernels de acuerdo a un espacio de trabajo que modela la organización de los diferentes núcleos de los dispositivos de la plataforma. En este caso, al tratarse la suma de vectores de una operación extremadamente sencilla, la definición de los espacios de trabajo (listado 2.2) no reviste complejidad alguna: se necesitarán tantos work-items como elementos tengan los arrays que representan a los vectores A y B (línea 1), y cada uno de dichos items realizarán su propio cálculo de C i = A i + B i correspondiente a una componente del vector suma C (línea 2). 1 size_t global_ work_ size =N; 2 size_t local_ work_ size =1; Listado 2.2: Ejemplo de definición de workspaces OpenCL para una suma de vectores Obtención de las plataformas OpenCL a usar La primera operación a realizar en un código de host OpenCL es comprobar la existencia de plataformas compatibles con el estándar para, posteriormente, seleccionar aquella o aquellas que se desean utilizar para ejecutar sobre ellas el kernel. La función que permite obtener esta información es clgetplatformids, la cual es utilizada en

52 30 2. El estándar OpenCL primer lugar para saber cuántas plataformas hay disponibles en el sistema (línea 3 del listado 2.3). Una vez conocido el número de platformas (almacenado en la variable nplatforms), se reserva memoria para almacenar los identificadores de las mismas (línea 4) y se vuelve a llamar a clgetplatformids (línea 5) para obtener dichos datos y volcarlos en la estructura pltfrmids. 1 cl_uint nplatforms ; 2 cl_ platform_ id * pltfrmids ; 3 clgetplatformids (0, NULL, & nplatforms ); 4 pltfrmids = malloc ( sizeof ( cl_ platform_ id ) * nplatforms ); 5 clgetplatformids ( nplatforms, pltfrmids, NULL ); Listado 2.3: Ejemplo de obtención de plataformas en un host OpenCL Obtención de los dispositivos OpenCL a usar Una vez conocidas las plataformas disponibles, se ha de elegir qué dispositivos de las mismas se desean utilizar. De forma similar a como sucede para las plataformas, en el caso de los dispositivos también resulta necesario llamar dos veces a clgetdeviceids, función que permite obtener sus identificadores. En primer lugar (línea 1 del listado 2.4) se obtiene, para un identificador de plataforma dado, el número de dispositivos disponibles. Una vez reservado el espacio necesario (línea 2), se obtienen los identificadores correspondientes (línea 3). Nótese que para obtener tanto el número de dispositivos como la lista de identificadores, es necesario especificar qué tipo de dispositivos se desean utilizar (véase tabla 2.3). En este caso, el dispositivo en cuestión es una GPU, cuyo identificador de tipo es CL DEVICE TYPE GPU. 1 clgetdeviceids ( pl_id, CL_DEVICE_TYPE_GPU,0, NULL,& ngpus ); 2 cldevs = malloc ( sizeof ( cl_device_id ) * ngpus ); 3 clgetdeviceids ( pl_id, CL_DEVICE_TYPE_GPU, ngpus, cldevs, NULL ); Listado 2.4: Ejemplo de obtención de dispositivos en un host OpenCL

53 2.3. Programación de aplicaciones con OpenCL 31 Identificador CL DEVICE TYPE CPU CL DEVICE TYPE GPU CL DEVICE TYPE ACCELERATOR CL DEVICE TYPE DEFAULT CL DEVICE TYPE ALL Descripción CPU del host GPU Acelerador dedicado (un blade, por ejemplo) Dispositivo por defecto de la plataforma Todos los dispositivos Tabla 2.3: Tipos de dispositivos recogidos en el estándar OpenCL Creación de un contexto para los dispositivos Una vez seleccionados plataforma y dispositivo kernel, se procede a crear el contexto que servirá de base para la ejecución del kernel. Previamente (línea 1 del listado 2.5) a la creación del contexto se define un array cps de propiedades en el que se indica qué plataforma se va a usar. A partir de dicho array se crea el contexto context llamando a la función clcreatecontextfromtype (línea 2) especificando, además de las propiedades, el tipo de dispositivo a usar. También sería posible especificar un puntero a función que será invocada por OpenCL para informar de cualquier error sucedido en el seno del contexto creado y gestionarlo según establezca el usuario. 1 cl_ context_ properties cps [3] = { CL_ CONTEXT_ PLATFORM,( cl_ context_ properties ) platforms [ selected_ platform ], 0}; 2 context = clcreatecontextfromtype (cps, CL_DEVICE_TYPE_GPU,NULL,NULL,& err ); Listado 2.5: Ejemplo de creación de un contexto OpenCL Creación de colas de comandos Con el contexto creado, es el momento de establecer la cola o colas que servirán al host para enviar diferentes tipos de comandos al dispositivo. Para crear una cola basta llamar a la función clcreatecommandqueue (listado 2.6) indicando el contexto para el que se está creando la cola, a qué dispositivo se enviarán los comandos que en ella se inserten y el modo o modos de ejecución de la misma. Existen dos posibles modos de ejecución: CL QUEUE PROFILING ENABLE, que indica que la cola admitirá operaciones de

54 32 2. El estándar OpenCL profiling de la ejecución del kernel, y CL QUEUE OUT OF ORDER EXEC MODE ENABLE, que habilita la ejecución de comandos fuera de orden (véase el apartado 2.2.2). 1 cl_ command_ queue command_ queue = clcreatecommandqueue ( context, devices [ selected_ device ], CL_ QUEUE_ PROFILING_ ENABLE, NULL ); Listado 2.6: Ejemplo de creación de una cola de comandos OpenCL Creación de buffers de datos En el listado 2.7 puede verse la definición (línea 1) y creación (líneas 2-4) de los buffers necesarios para la realización de la suma de vectores de ejemplo del listado 2.1. Para cada una de las estructuras, se realiza una llamada a la función clcreatebuffer, que recibe como parámetros el contexto de ejecución, el modo de acceso del kernel (sólo lectura, sólo escritura y lectura-escritura) al buffer y el tamaño del mismo. En este caso, los vectores A y B están representados por los buffers mema y memb, bastando con el acceso de sólo lectura. En cambio, memc, que representa al vector suma C, sí necesita poder ser accedido por el kernel para escritura, aunque no para lectura. 1 cl_mem mema, memb, memc ; 2 mema = clcreatebuffer ( context, CL_ MEM_ READ_ ONLY, N* sizeof ( cl_ float ), NULL, NULL ); 3 memb = clcreatebuffer ( context, CL_ MEM_ READ_ ONLY, N* sizeof ( cl_ float ), NULL, NULL ); 4 memc = clcreatebuffer ( context, CL_ MEM_ WRITE_ ONLY, N* sizeof ( cl_ float ), NULL, NULL ); Listado 2.7: Ejemplo de creación de buffers OpenCL para una suma de vectores Transferencia de datos de entrada Una vez definidos los buffers a los que tendrá acceso el kernel, es necesario transferir a los mismos los datos de entrada necesarios para realizar la suma de vectores del ejemplo. Estas transferencias han de realizarse insertando comandos de escritura en

55 2.3. Programación de aplicaciones con OpenCL 33 buffer en la cola de ejecución. Dicha inserción se realiza mediante llamadas a la función clenqueuewritebuffer, las cuales pueden verse en las líneas 1 y 2 del listado 2.8. Estas llamadas reciben como parámetros la cola en la que se insertarán los comandos, el buffer destino de los datos, el carácter bloqueante (CL TRUE) o no (CL FALSE) de la escritura, un posible offset (en este caso, 0), el tamaño del buffer y la variable de origen del host (los vectores A y B). No se deje el lector inducir a error por el nombre de clenqueuewritebuffer. Aunque desde el punto de vista del kernel estos buffers solamente se utilizarán para lectura (de ahí su creación como CL MEM READ ONLY), antes es necesario que el host escriba en ellos los datos de entrada necesarios para la operación implementada en el kernel, de ahí que el comando sea clenqueuewritebuffer, es decir, encolar una escritura en buffer. 1 clenqueuewritebuffer ( command_queue, mema, CL_TRUE, 0, N* sizeof ( cl_ float ), A, 0, NULL, NULL ); 2 clenqueuewritebuffer ( command_queue, memb, CL_TRUE, 0, N* sizeof ( cl_ float ), B, 0, NULL, NULL ); Listado 2.8: Ejemplo de transferencia de datos para una suma de vectores Carga y compilación de un kernel El listado 2.9 muestra el proceso de carga de un kernel desde el fichero de código correspondiente y la posterior compilación del mismo en un programa. En primer lugar asocia el fichero a un flujo ifstream de C++ y se vuelca su contenido a una cadena de caracteres (líneas 1 a 3). Una vez se tiene el código de kernel en la cadena, se obtiene su longitud (línea 4) y se crea el programa con la llamada a clcreateprogramwithsource (línea 5), que recibe como parámetros el contexto, el número de cadenas de texto y las cadenas que conforman el código y el tamaño del mismo. Finalmente, se compila el programa para los dispositivos seleccionados (línea 6).

56 34 2. El estándar OpenCL 1 std :: ifstream file ("./ vecsum.cl") 2 std :: string sourcestring ( std :: istreambuf_ iterator < char >( file ),( std :: istreambuf_iterator <char >() )); 3 const char * source = sourcestring. c_str (); 4 size_t sourcesize []={ strlen ( source ) }; 5 cl_ program program = clcreateprogramwithsource ( context, 1, & source, sourcesize, & err ); 6 clbuildprogram ( program, num_devices, devices,null,null, NULL ); Listado 2.9: Ejemplo de carga y compilación de un kernel OpenCL Paso de argumentos y ejecución A partir del programa compilado y del nombre de la función a ejecutar se crea un kernel mediante la llamada a la función clcreatekernel (línea 1 del listado 2.10). Una vez creado el kernel, con clsetkernelarg se van pasando los argumentos en el mismo orden en que aparecen en la firma de la función (líneas 2 a 5). Finalmente (línea 6), con clenqueuendrangekernel, se encola la ejecución del kernel, que se realizará de acuerdo con los espacios de trabajo global y local definidos en el listado cl_kernel kernel = clcreatekernel ( program," vecsum ",& err ); 2 clsetkernelarg ( kernel,0, sizeof ( cl_mem ),& mema ); 3 clsetkernelarg ( kernel,1, sizeof ( cl_mem ),& memb ); 4 clsetkernelarg ( kernel,2, sizeof ( cl_mem ),& memc ); 5 clsetkernelarg ( kernel,3, sizeof ( cl_uint ),&N); 6 err = clenqueuendrangekernel ( command_queue, kernel,1, NULL,& global_ work_ size,& local_work_size,0, NULL, NULL ); Listado 2.10: Paso de argumentos y ejecución del kernel OpenCL vecsum Recogida de datos de salida De la misma forma que antes de comenzar la ejecución es necesario volcar los datos de entrada de los vectores A y B del host a los buffers accesibles por el kernel para

57 2.3. Programación de aplicaciones con OpenCL 35 su lectura, una vez finalizado el cálculo de la suma de vectores es también lógico que haya que copiar el resultado del buffer de escritura al vector suma C. Para ello es necesario encolar un comando de lectura de buffer, tarea que se realiza en la línea 1 del listado de código Nótese que en este caso sucede algo similar a lo que se advirtió para la escritura de los datos de entrada en sus respectivos buffers: el buffer memc ha sido definido como de sólo escritura para el kernel, pero es necesario copiar los datos escritos en él al array resultado del host, motivo por el que se ha de llamar a la función clenqueuereadbuffer, que encola un comando de lectura de buffer. 1 clenqueuereadbuffer ( command_queue,memc, CL_TRUE, 0,N* sizeof ( cl_float ),C,0, NULL, NULL ); Listado 2.11: Ejemplo de recogida de datos para una suma de vectores Liberación de recursos El código de host finaliza con la el cierre de la cola de comandos (línea 1 del listado 2.12) y la liberación del espacio utilizado para la definición del kernel, el programa a partir del cual se creó, la cola y el contexto (líneas 2 a 5). 1 clfinish ( command_queue ); 2 clreleasekernel ( kernel ); 3 clreleaseprogram ( program ); 4 clreleasecommandqueue ( command_ queue ); 5 clreleasecontext ( context ); Listado 2.12: Ejemplo de liberación de recursos OpenCL Desarrollo de un código de kernel A continuación se describirá el proceso de desarrollo de un código de kernel utilizando como ejemplo la implementación para OpenCL de la suma de vectores del listado 2.1. Nótese que un kernel OpenCL tiene la estructura habitual de cualquier función de

58 36 2. El estándar OpenCL C, esto es, una firma (con nombre de función, parámetros de entrada/salida y tipo de retorno) y un cuerpo encerrado entre llaves ({...}). En lo que a la firma respecta, existen ciertas características que diferencian la firma de una función C estándar de la de un kernel OpenCL: Modificador kernel: Este modificador, incluido dentro de la extensión de C para OpenCL, marca a la función como kernel OpenCL. Tipo de retorno void: Un kernel OpenCL siempre debe tener void como tipo de retorno, utilizándose para la salida de datos la escritura en objetos de memoria definidos a tal efecto. Modificadores de memoria: En el ejemplo del listado 2.1, los vectores de entrada a y b y el vector suma c se encuentran afectados por el modificador global, cuya finalidad es indicar al kernel que dichas estructuras de datos se encuentran alojadas en la región de memoria global del dispositivo (véase apartado 2.2.3). Como puede verse, este tipo de modificadores son perfectamente compatibles (recuérdese que se está trabajando con una extensión de C, por lo que todas las funcionalidades originales del lenguaje permanecen disponibles) con otros como const, que indica, para los vectores de entrada a y b, que se tratan de estructuras de datos de sólo lectura. Una rápida revisión del cuerpo de la función (líneas 2-6 del listado 2.1, al principio de la presente sección 2.3) permite apreciar bastante bien el paralelismo de datos soportado por OpenCL y el proceso de generación de instancias del kernel: en la línea 3 se realiza la llamada get global id(0), que almacena en xid el identificador global en la dimensión 0 (en este caso, al ser un problema unidimensional, es la única) de cada work-item del dispositivo. Así, cada instancia computa su componente de la suma mediante la instrucción de la línea 5. Además, dado que se crean tantas instancias del kernel como work-items tenga el dispositivo, pero solamente interesa que se ejecuten tantas como el tamaño de la única dimensión del espacio global (variable global work size del listado 2.2), es necesario introducir una estructura de control (línea 4) que compruebe si el work-item en cuestión está operando sobre una componente válida del vector o no.

59 2.3. Programación de aplicaciones con OpenCL Comentarios Como puede apreciarse, el trabajo de preparación del entorno necesario para poder ejecutar un kernel aparentemente tan sencillo como una suma de vectores puede llegar a resultar bastante tedioso. Sin embargo, en realidad el proceso se reduce a introducir los parámetros necesarios en las correspondientes definiciones de estructuras y llamadas a funciones del API de OpenCL, de modo que resulta bastante mecánico. La herramienta implementada intenta aprovechar esta característica de los códigos de host para proporcionar al programador un mecanismo que le permita obtener una versión funcional de dichos códigos, introduciendo los parámetros citados a lo largo de los apartados anteriores en un fichero de configuración. Las características de este generador de códigos de host serán explicadas en detalle, junto con el resto de funcionalidades de la herramienta, en el capítulo 4 de la presente memoria.

60

61 Capítulo 3 LLVM y Clang En este capítulo se realizará, en primer lugar, una breve introducción a la infraestructura de compilación LLVM (sección 3.1), comentando sus principales componentes y características, para, posteriormente, estudiar en detalle el frontend C/C++ Clang (sección 3.2), centrándose especialmente en sus funcionalidades de análisis y transformación de código C y C++. Así mismo, al final del capítulo (sección 3.3 se incluye un pequeño tutorial acerca de dichas capacidades básicas de análisis y transformación de código La infraestructura de compilación LLVM La infraestructura de compilación LLVM, nacida como proyecto de investigación de la Universidad de Illinois, consiste en una colección de tecnologías toolchain modulares y reusables. Por toolchain se entiende todo el conjunto de herramientas necesarias para el desarrollo, producción y mantenimiento de un programa, tales como compiladores, ensambladores, depuradores, etc. El objetivo principal del proyecto es el de reunir un conjunto de componentes modulares de toolchaining (compilación, depuración, etc.) capaces de soportar lenguajes y arquitecturas muy variadas, de modo que a partir de los mismos, tanto la comunidad de 39

62 40 3. LLVM y Clang desarrolladores de LLVM como cualquier usuario interesado en este tipo de actividades, pueda construir nuevas herramientas de compilación verdaderamente adaptadas a sus necesidades [15] Inciativas surgidas desde LLVM Con el paso del tiempo, LLVM ha ido creciendo hasta servir de paraguas para diversas iniciativas, muchas de las cuales han sido o están siendo utilizadas en la producción de una amplia variedad de proyectos tanto comerciales como abiertos, además de en investigación académica [16]. De entre los diferentes proyectos surgidos a partir de LLVM, resulta especialmente interesante destacar los siguientes: Las librerías LLVM Core, que proporcionan un optimizador de código moderno e independiente de la máquina de destino o máquina target, así como funcionalidades de generación de código para gran cantidad de CPUs. Estas librerías están construidas basándose en una representación intermedia de código propia de LLVM asimilable a un lenguaje de tipo ensamblador llamada LLVM IR (LLVM Intermediate Representation). Clang, que es el frontend para compilación de códigos C, C++ y Objective-C a través de LLVM, y cuyo primer objetivo es mejorar la experiencia de compilación para el desarrollador, acelerando el proceso y proporcionando gran cantidad de avisos y mensajes de error. Con las funcionalidades ofrecidas por Clang es posible construir potentes herramientas de análisis y transformación de código. Dragonegg, que integra los optimizadores y generadores de código de LLVM con los parsers de GCC 4.5, lo que permite a LLVM compilar código de los diversos lenguajes soportados por los diferentes frontends del compilador GCC, así como acceder a determinadas características de C no soportadas por Clang, como por ejemplo, directivas OpenMP. El proyecto LLDB, construido a partir de las librerías de LLVM y Clang y que busca proporcionar un depurador de código nativo para toda la plataforma.

63 3.2. El frontend Clang 41 Los proyectos libc++ y libc++ ABI, que buscan proporcionar una implementación estándar y de alto rendimiento de la C++ Standard Library. El proyecto libclc, que tiene por objetivo proporcionar una implementación de la librería estándar de OpenCL. Junto a estos y otros componentes oficiales de LLVM, existe una amplia variedad de proyectos que utilizan partes de LLVM para las más variadas tareas, destacando sobre todo diferentes implementaciones de compiladores para lenguajes como Ruby, Python, Haskell, Java, D, PHP, Pure, Lua y muchos otros El frontend Clang Una vez presentadas las principales características de la infraestructura LLVM, a continuación se comentan en detalle diferentes aspectos relevantes del frontend Clang, para posteriormente introducir sus funcionalidades de análisis y transformación de código, las cuales constituyen uno de los pilares fundamentales del presente proyecto Motivación Clang es (y está siendo, dado que se trata de un proyecto libre y colaborativo que se encuentra en constante evolución) el resultado del trabajo de diversas comunidades de usuarios tanto del propio proyecto LLVM como de equipos de desarrollo de Apple, para quienes GCC adolecía de determinadas carencias o inconvenientes. Entre dichos inconvenientes se encuentran algunos como la dificultad de uso del frontend de GCC, cuya curva de aprendizaje resulta demasiado pronunciada para muchos desarrolladores [17], la carencia de determinadas funcionalidades extremadamente útiles para su integración con un IDE (indexado de variables, refactorización, etc.) o su elevado consumo de memoria [18].

64 42 3. LLVM y Clang Objetivos Entre los principales objetivos que pretenden alcanzar la comunidad de desarrolladores de Clang podrían destacarse los siguientes [18]: Crear un parser unificado para los lenguajes basados en C (C/C++ y Objective C/C++), compatible con GCC y capaz de proporcionar información detallada de diagnóstico ante los diferentes errores de sintaxis. Construir una arquitectura basada en librerías, las cuales sean fácilmente accesibles, extensibles e integrables en otras herramientas en forma de APIs escritas en lenguaje C++. Implementación de una herramienta multipropósito, capaz de realizar tareas como indexado de variables, análisis estático, generación de código o transformaciones source-to-source. Alto rendimiento a través de evaluación de expresiones lazy, reducido consumo de memoria y soporte para ejecución multithreading. Nótese cómo algunos de estos objetivos se ajustan claramente a las necesidades de la herramienta implementada en el presente proyecto, especialmente aquellos relacionados con la posibilidad de integrar Clang en otros entornos mediante el uso de una API escrita en C++ y con las diferentes tareas de análisis y transformación de código soportadas Características Las diferentes características que definen al compilador pueden agruparse en las siguientes categorías [19]: Características orientadas al usuario final, tales como compatibilidad con GCC, consumo reducido de memoria o generación de diagnósticos expresivos. Caracterísicas de utilidad e integración con otras aplicaciones, como su arquitectura basada en librerías o su distribución bajo licencia LLVM BSD.

65 3.2. El frontend Clang 43 Características de diseño interno e implementación, buscando una total conformidad con C/C++ y Objective C/C++ y sus variantes para poder ofrecer un parser unificado para todas ellas, así como un compilador de calidad en disposición de ser utilizado para la producción de software Arquitectura A continuación se realiza un breve repaso de la arquitectura de Clang, estudiándola desde dos perspectivas diferentes: una visión de alto nivel separada en capas, y otra estructurada en torno a al conjunto de librerías en que se recogen las diferentes funcionalidades de la herramienta. Enfoque basado en capas La arquitectura de alto nivel de Clang, cuya representación gráfica puede observarse en la figura 3.1, se compone de las siguientes tareas, ordenadas de forma descendente [17]: Figura 3.1: Arquitectura de alto nivel de Clang Transformación del AST en lenguaje intermedio IR de LLVM. Generación del AST a partir de entradas de código válidas. Análisis sintáctico de los diferentes componentes reconocidos.

66 44 3. LLVM y Clang Análisis léxico y preprocesado del código, a partir de las distintas tablas de identificadores, tokens, macros, literales... Tareas de manipulación de ficheros: localización y obtención de los rangos de líneas de cada fragmento de código, descripción de las arquitecturas de origen y destino... Soporte básico de funcionamiento a través de diversas funcionalidades de LLVM. Enfoque basado en librerías Un concepto fundamental en el diseño de Clang es el uso de una arquitectura basada en librerías. Con este tipo de diseño, las diferentes partes del frontend pueden ser divididas de forma clara en librerías separadas que, posteriormente, podrán ser combinadas por los usuarios de cara a satisfacer sus necesidades o usos [19]. Además, esta aproximación basada en librerías potencia el uso de buenas interfaces en el desarrollo de toda la infraestructura y facilita enormemente a los nuevos usuarios involucrarse en el proceso, dado que, para empezar, basta con que comprendan aquellas pequeñas partes que les resulten necesarias para resolver sus problemas concretos. Actualmente, las clases que componen el proyecto Clang se encuentran repartidas entre las siguientes librerías: libsupport: Soporte básico, proporcionado mediante LLVM. libsystem: Librería de abstracción del sistema, proporcionada igualmente mediante LLVM. libbasic: Gestión de diagnósticos, buffers de ficheros de entrada, localización de fragmentos de código, etc. libast: Compuesta por diversas clases que pretenden representar el AST de un código C, el sistema de tipos del lenguaje, las funciones builtin, así como por diferentes asistentes para el análisis y manipulación de ASTs (visitadores, impresión por pantalla, etc.).

67 3.2. El frontend Clang 45 liblex: Análisis léxico y preprocesado, gestión de la tabla hash de identificadores, pragmas, tokens y expansión de macros. libparse: Dedicada exclusivamente a labores de parsing mediante la invocación de acciones o actions de grano grueso, como por ejemplo, llamar a las funciones pertinentes de libsema para construir el AST, pero sin conocer nada sobre ésta u otras estructuras de datos específicas del cliente. libsema: Dedicada al análisis semántico, proporciona un conjunto de acciones de parsing para construir ASTs estandarizados para los programas. libcodegen: Transformación del AST a representación intermedia de LLVM (LLVM IR) para posteriores labores de optimización y generación de código. librewrite: Edición de buffers de texto, necesaria para tareas de reescritura de código tales como la refactorización de variables. Las funcionalidades implementadas en esta librería son de vital importancia para la realización de este proyecto. libanalysis: Soporte para análisis estático de código. Todas estas librerías, y por extensión, las clases que las integran, pueden ser accedidas de forma diferente según el uso que se desee hacer de las mismas. En caso de que se desee utilizar Clang como una herramienta para compilar o analizar código, puede hacerse uso del ejecutable clang, que funciona como cliente de las librerías a varios niveles. En cambio, si lo que se busca es construir una nueva herramienta adaptada a las necesidades del usuario mediante el uso de las diferentes librerías y clases, Clang permite el acceso a las mismas mediante su API implementada en C++. Como puede deducirse, ha sido este segundo método el utilizado para integrar en la herramienta implementada en el presente proyecto las funcionalidades de Clang necesarias para su desarrollo. Así mismo, al tratarse de una API escrita en C++, basta la importación de las clases necesarias mediante las directivas #include correspondientes para empezar a utilizarlas.

68 46 3. LLVM y Clang 3.3. Análisis y transformación de código con Clang En el presente apartado se introduce el concepto de árbol de sintaxis abstracta como punto de partida para el comentario de las principales funcionalidades de análisis y transformación de código de Clang Introducción Como ya se ha comentado en apartados anteriores, el objetivo del proyecto Clang es crear un nuevo front-end de C/C++ y Objective C/C++ para la infraestructura de compilación LLVM. Un front-end no es más que una herramienta que, dado un fragmento de código, lo transforma de texto plano a una representación del programa estructurada en forma de árbol llamada árbol de sintaxis abstracta o AST 1. Una vez se tiene la representación del programa en forma de AST, es posible realizar muchas operaciones que, de otra forma, resultarían bastante complicadas. Por ejemplo, renombrar una variable sin la ayuda de un AST es muy complejo, además de peligroso para la integridad y la semántica del código: no es suficiente buscar el nombre antiguo y reemplazarlo por el nuevo, ya que esto introduciría en el código una gran cantidad de cambios no deseados, como el renombrar variables que compartan nombre con la reemplazada pero que se encuentren en namespaces diferentes. Con un AST, la cosa se vuelve bastante más sencilla: en principio, basta con cambiar el campo correspondiente al nombre en el nodo del AST que representa la declaración de la variable en cuestión y regenerar el código volcándolo de nuevo a texto Análisis de código A continuación se presenta un pequeño tutorial con el que se pretende introducir al lector en las funcionalidades de análisis de código presentes en Clang. 1 Del inglés, Abstract Syntax Tree

69 3.3. Análisis y transformación de código con Clang 47 Fundamentos básicos Todo proceso de análisis de código comienza realizando un análisis léxico del mismo a través de lo que se conoce como un lexer. Un lexer o analizador léxico convierte el flujo de caracteres de entrada en un flujo de tokens. Por ejemplo, en Clang, la cadena de entrada while sería descompuesta en sus cinco caracteres w, h, i, l y e, generando el token kw while. La clase Preprocessor representa la interfaz principal con el analizador léxico, siendo necesaria en prácticamente cualquier programa que trabaje con el API de Clang. Esta será la primera tarea del tutorial, construir una instancia de la clase Preprocessor. El listado 3.1 muestra el código necesario para crear un preprocesador de Clang junto con los argumentos mínimos necesarios para su instanciación: DiagnosticsEngine: Esta clase es usada por Clang para informar al usuario sobre errores y warnings. Un objeto de esta clase puede contar, a su vez, con una instancia de la clase DiagnosticsConsumer, clase responsable de mostrar dichos mensajes al usuario. En este ejemplo se usará la clase TextDiagnosticPrinter ya incluida en Clang, que muestra los errores y warnings por pantalla. Este es el mismo tipo de DiagnosticsConsumer que utiliza el ejecutable clang ya comentado. LangOptions: Esta clase permite configurar si se desea compilar código C o C++, así como qué extensiones del lenguaje se desean activar (por ejemplo, para la herramienta implementada, OpenCL). TargetInfo: Con esta clase se define la arquitectura para la que Clang deberá compilar el código, que por defecto es la correspondiente con la máquina en que se ha compilado la infraestructura LLVM, aunque existe la posibilidad de realizar cross-compiling estableciendo la arquitectura objetivo adecuada. Este objeto resulta necesario para que el preprocesador pueda añadir al código defines específicos, como por ejemplo, APPLE. Este objeto debe liberarse al final del programa.

70 48 3. LLVM y Clang 1 clang :: DiagnosticOptions diagnosticoptions ; 2 clang :: TextDiagnosticPrinter * ptextdiagnosticprinter = new clang :: TextDiagnosticPrinter ( llvm :: outs (), diagnosticoptions ); 3 llvm :: IntrusiveRefCntPtrclang :: DiagnosticIDs pdiagids ; 4 5 clang :: DiagnosticsEngine * pdiagnosticsengine = 6 new clang :: DiagnosticsEngine ( pdiagids, ptextdiagnosticprinter ); 7 8 clang :: LangOptions languageoptions ; 9 clang :: FileSystemOptions filesystemoptions ; 10 clang :: FileManager filemanager ( filesystemoptions ); 11 clang :: SourceManager sourcemanager ( 12 * pdiagnosticsengine, filemanager ); 13 clang :: HeaderSearch headersearch ( filemanager, * pdiagnosticsengine ); clang :: TargetOptions targetoptions ; 16 targetoptions. Triple = llvm :: sys :: getdefaulttargettriple (); clang :: TargetInfo * ptargetinfo = 19 clang :: TargetInfo :: CreateTargetInfo ( 20 * pdiagnosticsengine, targetoptions ); 21 clang :: CompilerInstance compinst ; clang :: Preprocessor preprocessor ( 24 * pdiagnosticsengine, languageoptions, 25 ptargetinfo, sourcemanager, 26 headersearch, compinst ); Listado 3.1: Ejemplo de instanciación de un Preprocessor de Clang

71 3.3. Análisis y transformación de código con Clang 49 SourceManager: Es usado por Clang para cargar y almacenar en caché los ficheros de código. Su constructor toma un DiagnosticsEngine para la gestión de los posibles errores y un FileManager para la gestión de los ficheros tanto en disco como en caché. HeaderSearch: El constructor de esta clase también necesita sendas instancias de DiagnosticsEngine y FileManager. HeaderSearch se utiliza para configurar dónde debe buscar Clang los ficheros de cabeceras. ModuleLoader: Es una clase abstracta cuya implementación concreta ayuda a resolver nombres de módulos. En este caso, se creará una instancia de la clase CompilerInstance, que será incluida como ModuleLoader por defecto. Procesando un fichero Una vez instanciado un preprocesador, el siguiente paso consiste en pasarle al mismo ficheros de código C, para, como se verá en pasos posteriores del tutorial, poder analizar su sintaxis. El listado 3.2 muestra el código necesario para esta operación: con estas tres líneas se asigna un identificador de fichero a la entrada, se almacena ésta como fichero principal en el objeto SourceManager y se ordena al preprocesador que acceda a dicho fichero. Una vez cargado en memoria un fichero de código fuente, el preprocesador puede recorrerlo y obtener los diferentes tokens de entrada preprocesados. 1 const clang :: FileEntry * pfile = filemanager. getfile (" foo.c"); 2 sourcemanager. createmainfileid ( pfile ); 3 preprocessor. EnterMainSourceFile (); Listado 3.2: Ejemplo de envío de ficheros de código a un Preprocessor de Clang El listado 3.3 muestra el código necesario para obtener los tokens preprocesados: se define una instancia de Token (línea 1), y mientras el token encontrado no sea la marca de final de fichero (línea 10), se ordena al preprocesador que obtenga el siguiente token (línea 3). Si todo va bien, el token es mostrado por pantalla (líneas 8 y 9), mientras que en caso de error (línea 4), se rompe la ejecución del bucle (línea 6).

72 50 3. LLVM y Clang 1 clang :: Token token ; 2 do { 3 preprocessor. Lex ( token ); 4 if( pdiagnosticsengine - > haserroroccurred ()) 5 { 6 break ; 7 } 8 preprocessor. DumpToken ( token ); 9 std :: cerr << std :: endl ; 10 } while ( token. isnot ( clang :: tok :: eof )); Listado 3.3: Ejemplo de separación en tokens de un fichero de código C con Clang Ficheros de cabecera Con lo que se ha implementado hasta el momento, si se pasa al preprocesador un código C que contenga alguna sentencia #include, lo más probable es que no funcione. El error obtenido sería similar al del listado testinclude. c :1:10: fatal error : stdio. h file not found 2 # include <stdio.h> 3 ^ Listado 3.4: Ejemplo de diagnóstico para un fichero de cabeceras no encontrado Este error se ha producido porque es necesario establecer en el preprocesador las rutas donde éste puede buscar los posibles ficheros de cabecera incluidos en el código. En particular, es muy importante especificar la localización de los ficheros de cabeceras estándar. Para ello basta incluir la línea de código que se muestra en el listado 3.5. Esta simple línea crea una instancia de HeaderSearchOptions con la información por defecto, incluyendo las rutas de acceso a los ficheros de cabecera estándar. 1 clang :: HeaderSearchOptions headersearchoptions ; Listado 3.5: Ejemplo de definición de opciones de búsqueda de cabeceras de Clang

73 3.3. Análisis y transformación de código con Clang 51 Internamente, esto sucede gracias a una llamada a ApplyHeaderSearchOptions, método que consulta el contenido de las los atributos LangOptions y TargetInfo del preprocesador. De todas formas, es posible añadir más opciones a HeaderSearchOptions modificando diversos flags y propiedades con los que se puede establecer cómo incializar las búsquedas de las cabeceras o directorios adicionales de consulta. Extrayendo la sintaxis del fichero En la situación actual, el resultado de la ejecución del preprocesador es un flujo de tokens. El siguiente paso consiste en escribir un analizador sintático que convierta dicho flujo en un AST. Como es de esperar, Clang también cuenta con clases para realizar este trabajo. El método que es necesario invocar para ello es clang::parseast(), que internamente se sirve de la instancia de Preprocessor ya configurada, de un ASTConsumer y un ASTContext. La clase ASTConsumer cuenta con varios métodos calificados como virtual con cuya redefinición se debe establecer el comportamiento de alto nivel del analizador, mientras que ASTContext mantiene elementos de larga duración del AST como tipos o declaraciones, necesarios para realizar tareas de análisis semántico. Las líneas 1 a 21 del listado 3.6 muestran el proceso de creación de los objetos antes descritos como necesarios para poder invocar a clang::parseast(). Una vez creados dichos objetos, basta llamar al método (línea 23) para lanzar el proceso de análisis del código. Tareas de análisis semántico Una vez definido el analizador, ya es posible realizar alguna pequeña tarea de análisis sobre el código de entrada. A continuación se plantea el ejemplo de implementación de un programa que muestra información sobre las variables globales declaradas en un fichero de código C. Una declaración, grosso modo, puede dividirse en dos partes: a la izquierda, el tipo (por ejemplo, const unsigned long int o struct { int a }); y a la derecha, una lista de modificadores y el propio nombre de la variable declarada (por ejemplo, b, i o *((**foo [][8])())[]). En el listado 3.7 se muestran algunos ejemplos de declaraciones.

74 52 3. LLVM y Clang 1 const clang :: TargetInfo & targetinfo = * ptargetinfo ; 2 3 clang :: IdentifierTable identifiertable ( languageoptions ); 4 clang :: SelectorTable selectortable ; 5 6 clang :: Builtin :: Context builtincontext ; 7 builtincontext. InitializeTarget ( targetinfo ); 8 clang :: ASTContext astcontext ( 9 languageoptions, 10 sourcemanager, 11 ptargetinfo, 12 identifiertable, 13 selectortable, 14 builtincontext, 15 0 /* size_reserve */); 16 clang :: ASTConsumer astconsumer ; clang :: Sema sema ( 19 preprocessor, 20 astcontext, 21 astconsumer ); clang :: ParseAST ( preprocessor, & astconsumer, astcontext ); Listado 3.6: Definiciones necesarias para llamar a clang::parseast() 1 const unsigned long int b; // b is just a constant 2 int * i; // declares ( and defines ) a pointer - to - int variable 3 extern int a; // declares an int variable 4 void printf ( const char * c,...) ; // a function prototype 5 typedef unsigned int u32 ; // a typedef 6 struct { int a } t; // a variable that has an anonymous struct as type 7 char *(*(** foo [][8]) ())[]; //????? Listado 3.7: Ejemplos de declaraciones de variables

75 3.3. Análisis y transformación de código con Clang 53 En Clang, la parte de la izquierda es representada mediante instancias de la clase DeclSpec, mientras que la potencialmente compleja parte derecha se corresponde con una lista de instancias de DeclaratorChunk. Un DeclaratorChunk tiene un tipo (Kind) que puede ser puntero (Pointer), referencia (Reference), array (Array) o función (Function). Para diferenciar entre todos estos tipos de declaraciones no es suficiente con realizar un proceso de análisis sintáctico, sino que el compilador también necesita analizar semánticamente el código. Para poder controlar dicho análisis y obtener sus resultados, es necesario definir una instancia propia de la clase ASTConsumer, que como ya se ha comentado, contiene varios métodos virtual que exigen su sobreescritura para definir el comportamiento concreto del análisis sintático. En este caso, el método a sobreescribir es HandleTopLevelDecl(), el cual es invocado cada vez que se recorre una declaración de alto nivel. Este método recibe como parámetro objetos de la jerarquía de clases Decl, utilizada por el AST para representar declaraciones. La implementación del método, que se puede ver en el listado 3.8, solamente detecta declaraciones de variables (instancias de la clase VarDecl) que sean globales y nativas de C (es decir, que no estén incluidas en una sección de código extern) Transformación de código A continuación se desarrolla otro pequeño tutorial que presenta las principales capacidades de transformación de código de Clang, haciendo especial hincapié en la transformación source-to-source (código fuente a código fuente, en este caso, de código C a código C). El ejemplo que servirá de base para el desarrollo de este tutorial será una operación de transformación con la que se desea refactorizar todas las funciones existentes en el código, renombrándolas con el sufijo -nueva. Para que el código resultante sea correcto, es necesario modificar tanto la declaración de cada una de las funciones como las correspondientes invocaciones de las mismas en el programa. Así mismo, para que el programa resultante sea correcto, será necesario especificar explícitamente que no se desea modificar ni la función principal main() (si se renombra, se perdería el

76 54 3. LLVM y Clang 1 virtual bool HandleTopLevelDecl ( clang :: DeclGroupRef d) 2 { 3 static int count = 0; 4 clang :: DeclGroupRef :: iterator it; 5 for ( it = d. begin (); it!= d. end (); it ++) 6 { 7 count ++; 8 clang :: VarDecl *vd = llvm :: dyn_cast < clang :: VarDecl >(* it); 9 if (! vd) 10 { 11 continue ; 12 } 13 std :: cout << vd << std :: endl ; 14 if( vd - > isfilevardecl () && vd - > hasexternalstorage () ) 15 { 16 std :: cerr << " Read top - level variable decl : "; 17 std :: cerr << vd -> getdeclname (). getasstring () ; 18 std :: cerr << std :: endl ; 19 } 20 } 21 return true ; 22 } Listado 3.8: Ejemplo de sobreescritura de la función HandleTopLevelDecl

77 3.3. Análisis y transformación de código con Clang 55 punto de entrada de la ejecución del programa) ni todas aquellas importadas mediante directivas #include (se modificaría el código original de las mismas). La explicación del presente tutorial se estructurará en torno a dos clases de C++ (el API de Clang está escrito en dicho lenguaje) llamadas MyRewriter y MyConsumer, las cuales sirven para preparar el entorno de trabajo e implementar el comportamiento del ejemplo de transformación respectivamente. Clase MyRewriter Además de preparar el entorno de trabajo de forma similar a como se explica en el tutorial anterior sobre cómo utilizar las funcionalidades de análisis de código de Clang, la clase MyRewriter proporciona también, mediante un método parse(), el punto de entrada a las funcionalidades de transformación de código utilizadas por MyConsumer, clase esta última cuya implementación será detallada más adelante. En el listado de código 3.9, correspondiente al constructor de la clase MyRewriter, pueden apreciarse en detalle las operaciones de preparación del entorno (líneas 2-14). Así mismo, dicho constructor también prepara el buffer de salida del código reescrito (líneas 15 y 16) e instancia la clase MyConsumer antes comentada (línea 17). La implementación del método parse() puede consultarse en el listado de código Las líneas 2 a 7 de dicho método toman como entrada un fichero de código y, de existir, lo asignan al SourceManager previamente definido (línea 7 del constructor de MyRewriter, listado 3.9) como fichero principal. Posteriormente, una vez preparadas ciertas clases auxiliares necesarias (líneas 8-10), se define un nuevo contexto creando una instancia de ASTContext y se inicializa la instancia de MyConsumer (línea 11). Finalmente, se llama al método estático ParseAST(), que lanza el comportamiento implementado en MyConsumer, cuya implementación se detallará a continuación.

78 56 3. LLVM y Clang 1 MyRewriter ( const std :: string & infilename, const std :: string & outfilename ) : _ infilename ( infilename ), _ outfilename ( outfilename ){ 2 _fsopts = new FileSystemOptions (); 3 _fm = new FileManager (* _fsopts ); 4 _ diagsid = new llvm :: IntrusiveRefCntPtr < DiagnosticIDs >() ; 5 _diags = new Diagnostic (* _ diagsid ); 6 _diags - > setsuppressalldiagnostics ( true ); 7 _sm = new SourceManager (* _diags,* _fm ); 8 _ headers = new HeaderSearch (* _fm ); 9 _ taropts. Triple = LLVM_ HOSTTRIPLE ; 10 _taropts. ABI = ""; 11 _taropts. CPU = ""; 12 _taropts. Features. clear (); 13 _ti = TargetInfo :: CreateTargetInfo (* _diags, _ taropts ); 14 _pp = new Preprocessor (* _diags, _langopts, *_ti, *_sm, * _ headers ); 15 string errors ; 16 _fos = new raw_fd_ostream ( _outfilename. c_str (), errors ); 17 _ consumer = new MyConsumer ( _infilename, _fos, * _diags, _ langopts ); 18 } Listado 3.9: Constructor de la clase MyRewriter 1 int parse (){ 2 const FileEntry * file = _fm - > getfile ( _ infilename ); 3 if (! file ){ 4 cerr << " Failed to open \ " << _ infilename << "\ " << endl ; 5 return EXIT_ FAILURE ; 6 } 7 _sm -> createmainfileid ( file ); 8 IdentifierTable idtable ( _ langopts ); 9 SelectorTable seltable ; 10 Builtin :: Context builtinctx (* _ti ); 11 _ context = new ASTContext ( _langopts, *_sm, *_ti, idtable, seltable, builtinctx, 0); 12 _consumer -> Initialize (* _context ); 13 ParseAST (* _pp, _consumer, * _context ); 14 return EXIT_ SUCCESS ; 15 } Listado 3.10: Método parse() del ejemplo MyRewriter

79 3.3. Análisis y transformación de código con Clang 57 Clase MyConsumer Como ya se ha comentado, esta clase es la encargada de especificar el comportamiento de la pequeña herramienta de transformación de código cuya implementación se pretende explicar con el presente tutorial. Dicho comportamiento puede estructurarse en torno a dos tareas principales: por una parte, el recorrido de los distintos nodos que conforman el AST que representa al fichero de entrada (comportamiento heredado de ASTConsumer), y por otra, la comprobación o visita de las estructuras de código contenidas en cada uno de los nodos (especialización para MyConsumer de las templates DeclVisitor, StmtVisitor y TypeLocVisitor). La extensión de la clase ASTConsumer por parte de MyConsumer obliga que esta última implemente los siguientes métodos: Initialize(): Este método, cuyo código puede verse en el listado 3.11, carga el contexto procedente de la instancia de ASTContext (líneas 2 a 4) y prepara el fichero de entrada para su lectura (líneas 6 a 9) y reescritura (línea 10). 1 virtual void Initialize ( ASTContext & context ){ 2 Context = & context ; 3 SM = & Context - > getsourcemanager (); 4 TUDecl = Context - > gettranslationunitdecl (); 5 // Get the ID and start / end of the main file. 6 MainFileID = SM - > getmainfileid (); 7 const llvm :: MemoryBuffer * MainBuf = SM - > getbuffer ( MainFileID ); 8 MainFileStart = MainBuf - > getbufferstart (); 9 MainFileEnd = MainBuf - > getbufferend (); 10 Rewrite. setsourcemgr ( Context - > getsourcemanager (), Context - > getlangoptions ()); 11 } Listado 3.11: Implementación del método Initialize() de MyConsumer HandleTopLevelDecl(): Este método, mostrado en el listado 3.12, se encarga de gestionar la aparición de cualquier declaración top-level (ver tutorial sobre análisis), limitándose a recorrer individualmente los diferentes componentes que

80 58 3. LLVM y Clang la pueden formar llamando a HandleTopLevelSingleDecl(). 1 virtual void HandleTopLevelDecl ( DeclGroupRef D) 2 { 3 for ( DeclGroupRef :: iterator I = D. begin (), E = D. end (); I!= E; ++ I) 4 if (*I) 5 HandleTopLevelSingleDecl (* I); 6 } 7 } Listado 3.12: Implementación del método HandleTopLevelDecl() de MyConsumer HandleTopLevelSingleDecl(): Este método es el primero en el que realmente se aprecia comportamiento específico de la operación de refactorización de funciones que se desea realizar. En primer lugar, se comprueba si la declaración encontrada corresponde a una función (línea 5 del listado 3.13). De ser así, se renombran (líneas 13 a 18) aquellas que no se correspondan con la función main() ni con funciones procedentes de directivas #include. HandleTranslationUnit(): Este recorre el código (línea 8 del listado 3.14) en busca de todas las llamadas a las funciones renombradas. Como se verá posteriormente, visitar un fragmento del código puede implicar su reescritura, por lo que una vez visitado, se vuelcan todos los posibles cambios a un buffer de salida (líneas 13 a 21) y de ahí, mediante un flush, al fichero de texto (línea 23). Si bien existe toda una serie de métodos visitadores cuya implementación es exigida por DeclVisitor, StmtVisitor y TypeLocVisitor, solamente se detallará el código de VisitStmt, que es el que efectivamente realiza la transformación de código tratada en el presente tutorial. La implementación del método VisitStmt(), que como quizás habrá deducido el lector a partir de su nombre, es invocado cada vez que se visita una sentencia de código dentro del AST, puede consultarse en el listado de código Este método, en caso de que la sentencia visitada es una llamada a una función distinta de main() o procedente de un #include (línea 3), reemplaza la cadena de texto correspondiente a cada llamada

81 3.3. Análisis y transformación de código con Clang 59 1 void HandleTopLevelSingleDecl ( Decl * D) 2 { 3 Stmt * body ; 4 5 if ( isa < FunctionDecl >( D)) { 6 FunctionDecl * funcdecl = ( FunctionDecl *) D; 7 if (SM -> isfrommainfile ( funcdecl -> getlocation ())) { 8 body =D-> getbody (); 9 } // ignore main function, and functions from headers 12 if (SM -> isfrommainfile ( funcdecl -> getlocation ()) &! funcdecl -> ismain ()) { 13 stringstream ss; 14 ss << funcdecl - > getnameasstring () << "- nueva "; 15 Rewrite. ReplaceText ( funcdecl - > getnameinfo (). getloc () /* begin location */, 16 funcdecl - > getnameasstring (). length () /* replaced text length */, 17 ss.str () /* new text */); 18 RenamedFunctions [ funcdecl ] = ss. str (); 19 } 20 else if (SM -> isfrommainfile ( funcdecl -> getlocation ())) { 21 // any function including main 22 FunctionDecl * funcdecl = ( FunctionDecl *) D; 23 } } // isa < FunctionDecl > 26 } Listado 3.13: Implementación del método MyConsumer::HandleTopLevelSingleDecl()

82 60 3. LLVM y Clang 1 void HandleTranslationUnit ( ASTContext & C) 2 { 3 // modify calls 4 TranslationUnitDecl * TU = Context - > gettranslationunitdecl (); 5 for ( DeclContext :: decl_iterator I = TU -> decls_begin (),E = TU -> decls_ end (); I!= E; ++ I) 6 { 7 Decl *D = *I; 8 Visit (D); 9 } // Get the buffer corresponding to MainFileID. 12 // If we haven t changed it, then we are done. 13 if ( const RewriteBuffer * RewriteBuf = Rewrite. getrewritebufferfor ( MainFileID )) 14 { 15 llvm :: outs () << " Src file changed.\ n"; 16 * OutFile << std :: string ( RewriteBuf -> begin (), RewriteBuf -> end ()); 17 } 18 else 19 { 20 llvm :: errs () << "No changes.\n"; 21 } OutFile -> flush (); 24 } Listado 3.14: Implementación del método MyConsumer::HandleTranslationUnit()

83 3.3. Análisis y transformación de código con Clang 61 por otra (línea 6) igual a la que se ha añadido el sufijo -nueva, creada en el momento del renombre de la función propiamiente dicha (líneas 13 a 18 del listado 3.13). 1 void VisitStmt ( Stmt * Node ) 2 { 3 if (isa < CallExpr >( Node ) && SM -> isfrommainfile ((( CallExpr *) Node ) -> getdirectcallee () -> getlocation ())) { 4 CallExpr * callexpr = ( CallExpr *) Node ; 5 FunctionDecl * funcdecl = callexpr - > getdirectcallee (); 6 Rewrite. ReplaceText ( callexpr -> getlocstart (),funcdecl -> getnameasstring (). length (), RenamedFunctions [ funcdecl ]); 7 } 8 9 for ( Stmt :: child_iterator I = Node -> child_begin (), E = Node -> child_end (); I!= E; ++I) 10 if (*I) 11 Visit (*I); 12 } Listado 3.15: Método VisitStmt() de MyConsumer Comentario sobre los tutoriales Como ya se ha comentado, Clang es un proyecto de investigación en pleno desarrollo, lo que hace que tanto los métodos de su API C++ como las clases en que éstos se encuentran estén expuestos a experimentar cambios continuamente. Por este motivo, es muy probable que en el momento de lectura del presente documento algunos de los métodos citados no existan, hayan sufrido cambios en su visibilidad o hayan sido trasladados a otra clase. De igual manera, es también muy probable que ciertas clases hayan visto modificados los nombres o visibilidades de sus atributos, así como la estructura de sus constructores. Con estos tutoriales simplemente se pretende dar unas pinceladas de las posibilidades que ofrece Clang para tareas como la realizada en en la herramienta implementada, que será descrita en profundidad en el próximo capítulo.

84

85 Capítulo 4 La herramienta OCLOptimizer En este capítulo se describirá en detalle la herramienta implementada y la optimización de código que incorpora esta primera versión, consistente en el desenrollamiento de bucles. En concreto, se comenzará en la sección 4.1 con una descripción de la herramienta y de su modo de funcionamiento. Una vez presentada la herramienta, en la sección 4.2 se comenta un ejemplo completo de ejecución. A continuación, las secciones 4.3 y 4.4 detallan, respectivamente, la estructura de clases seguida para la implementación de la herramienta y la optimización de desenrollamiento de bucles incorporada. Finalmente, las sección 4.5 se dedica a comentar diferentes decisiones tomadas sobre diferentes detalles de diseño e implementación Descripción y funcionamiento de la herramienta El lenguaje OpenCL permite programar múltiples tipos de dispositivos. Para ello es necesario implementar, por una parte, un código de host que se encarga de gestionar el contexto de ejecución para el dispositivo dado y, por otra, un código de kernel en el que se implementa el problema a resolver y que puede ejecutarse en cualquier dispositivo. Sin embargo, esta portabilidad funcional ofrecida por el estándar no se ve después reflejada en una portabilidad de rendimiento, ya que un código que haya sido optimizado para una determinada plataforma teniendo en cuenta las características de la misma 63

86 64 4. La herramienta OCLOptimizer muy probablemente no será óptimo en otra. En este proyecto se ha desarrollado una herramienta de ayuda a un programador experto para mejorar de forma automática el rendimiento de un código OpenCL para una determinada plataforma, utilizando para ello técnicas de optimización iterativa. A continuación se realizará una descripción detallada de las características de esta herramienta, así como de su funcionamiento. Para poder utilizar la herramienta, el usuario-programador deberá tener cierto conocimiento de las técnicas de optimización aplicables en códigos OpenCL, para después anotar el kernel con una serie de directivas que indican qué optimizaciones probar en qué partes del código. Estas directivas se encuentran asociadas a diferentes tipos de optimizaciones, y en ellas se pueden especificar los rangos de valores que se desea que tomen los diferentes parámetros asociados a cada técnica (por ejemplo, para un desenrollamiento de bucles, se indicaría el rango de factores de desenrollamiento a probar). Junto con el código anotado el usuario debe proporcionar a la herramienta un fichero de configuración en el que establezca diferentes parámetros relacionados con la ejecución de la misma, tales como el tamaño del problema, la definición del espacio de trabajo de OpenCL, los argumentos de la función del kernel, etc. Así mismo, también se proporciona a los usuarios un medio para la generación automática de códigos de host completamente funcionales. Inicialmente, dichos códigos de host implementan una inicialización aleatoria de los datos de entrada del problema, ya que ésta resulta necesaria para poder ejecutar y evaluar el rendimiento de las diferentes versiones. En caso de que el usuario tuviese intención de reutilizar el host autogenerado para otras tareas, bastaría con que cambiase ese fragmento del código por su propia inicialización de los datos. A continuación se describe, con el apoyo de la figura 4.1, el algoritmo seguido para la implementación en la herramienta de este proceso de optimización iterativa: 1. Creación de una estructura arbórea (un árbol n-ario no balanceado), en la que se inserta el código de kernel anotado, el cual funcionará como raíz del árbol de optimizaciones. 2. Análisis secuencial del código de kernel en busca de la primera directiva de opti-

87 4.1. Descripción y funcionamiento de la herramienta 65 mización disponible. 3. Una vez encontrada la directiva, expansión del nodo para generar el siguiente nivel del árbol, que contendrá las versiones generadas según la anotación encontrada en el fichero de kernel durante el paso anterior. 4. A partir de la información indicada por el usuario en el fichero de configuración, la herramienta genera automáticamente los códigos de host para cada versión, inicializando de forma aleatoria las estructuras de datos que sean necesarias para poder ejecutar el código. Adicionalmente, se indica al usuario mediante comentarios incluidos en el código de host qué secciones debe modificar para sustituir la inicialización generada automáticamente por la suya propia si desea utilizar ese host en otro contexto externo a la herramienta. 5. Compilación de los códigos de host asociados a cada versión del kernel perteneciente al nivel estudiado del árbol, ejecución de los mismos y evaluación del rendimiento, seleccionando los mejores de acuerdo a un porcentaje de tolerancia respecto de la versión con menor tiempo de ejecución y a un número máximo de versiones admitidas. Estos dos parámetros habrán sido previamente establecidos por el usuario en la directiva de compilación correspondiente a la optimización aplicada. Ambas cláusulas (tolerancia y número de versiones) son opcionales, de modo que si no se encuentran, todas las versiones pasarían a la siguiente iteración del algoritmo. 6. Para cada una de las versiones seleccionadas en el paso 5, se intentar ejecutar de nuevo el paso 2. Si no aparece ninguna optimización nueva, el proceso finaliza con el paso Puesto que el objetivo final de la herramienta es conseguir una única versión final óptima, en este último paso se seleccionará directamente la versión que ofrezca mejor rendimiento en promedio entre todas las procedentes del último nivel de optimización.

88 66 4. La herramienta OCLOptimizer Figura 4.1: Proceso general de optimización iterativa implementado en la herramienta Todo este algoritmo resulta transparente para el usuario, para quien la herramienta se comporta como una caja negra en la que puede introducir un código de kernel anotado y un fichero de configuración y obtiene como resultado un código de host y un kernel optimizado para la plataforma sobre la que se ejecuta la herramienta. En todo caso, las versiones intermedias tanto de los códigos de host como de los códigos de kernel generadas por la herramienta son almacenadas junto con el kernel óptimo y su host en la misma carpeta donde se encuentra el kernel de entrada, con lo que el usuario de la misma puede acceder a ellas si son de su interés. A continuación se realiza una explicación mucho más detallada del funcionamiento de la herramienta. Recuérdese que antes de ejecutar la herramienta, el usuario debe anotar su código original de kernel utilizando alguna de las directivas #pragma soportadas por la herramienta, indicando así qué optimización desea aplicar sobre qué fragmento de código y bajo qué condiciones. El formato de las directivas puede verse en el listado 4.1. Ca-

89 4.1. Descripción y funcionamiento de la herramienta 67 da optimización soportada cuenta con un nombre (<name>, por ejemplo, unroll para desenrollamiento de bucles) y unos parámetros asociados (<params>, siguiendo con el desenrollamiento de bucles, la directiva asociada recibiría como parámetros la sucesión de factores de desenrollamiento que se desea aplicar: todos los valores posibles desde <min> hasta <max> generados con paso <step>. Así mismo, es posible establecer, de forma opcional, dos parámetros para acotar el espacio de búsqueda de la versión óptima durante el proceso de selección: un porcentaje de tolerancia (tolerance) del tiempo medio de ejecución a partir del que se descartan las versiones evaluadas y/o un número máximo (number) de versiones seleccionadas para el siguiente nivel. En caso de que se introduzcan estos dos parámetros, la herramienta aplica primero el filtro tolerance, y en base a las versiones que hayan superado ese primer filtro, selecciona el número establecido con number. 1 # pragma oclopts <name > < params > [ tolerance <0-100 >] [ number n] Listado 4.1: Expresión regular de formato de las directivas #pragma oclopts Estas directivas se procesan de una en una, de modo que cada vez que una directiva es procesada se genera un conjunto de versiones resultado de la aplicación de la optimización indicada por el usuario con la directiva de acuerdo con las condiciones establecidas en sus parámetros. Como ya se ha comentado en la descripción del algoritmo de funcionamiento, estas versiones son compiladas y ejecutadas, obteniendo sus tiempos medios de ejecución y seleccionando como óptimas aquellas que cumplan las condiciones de tolerancia y/o número máximo de versiones establecidas en la propia directiva, repitiéndose sobre dichas versiones el proceso de búsqueda de una nueva directiva. En caso de que el usuario no establezca ninguna de esas dos condiciones, el proceso se repetiría para todas las versiones generadas. El código del listado 4.2 muestra un ejemplo de uso de estas directivas para sugerir a la herramienta que aplique un desenrollamiento sobre un determinado bucle. En este caso, se está ordenando a la herramienta que desenrolle el bucle considerando factores entre 2 y 16 (ambos inclusive) con paso 2 (unroll ). Una vez compiladas y ejecutadas las 8 versiones generadas, la herramienta seleccionaría las 2 mejores (number

90 68 4. La herramienta OCLOptimizer 2) para que avanzasen a la siguiente fase del proceso de optimización iterativa. El motivo por el que puede resultar interesante seleccionar para la siguiente fase las n mejores versiones y no únicamente la mejor es para no descartar prematuramente combinaciones de optimizaciones que, tras la aplicación posterior de otras técnicas, pueden convertirse en versiones óptimas del código. 1 # pragma oclopts unroll number 2 2 for ( int i =0;i <1000; i ++) { 3 a[ i] = a[ i] + s; 4 } Listado 4.2: Ejemplo de uso de las anotaciones de optimización Una vez que el usuario ha anotado el código con las optimizaciones que desea probar, puede dar comienzo del proceso. El diagrama de la figura 4.2 pretende ilustrar de forma general el proceso que sigue un código de kernel desde que es anotado mediante las directivas #pragma oclopts antes comentadas hasta obtener la versión óptima del mismo de acuerdo con las optimizaciones propuestas. Como se puede observar en dicho diagrama, el proceso se divide en tres fases que a continuación se detallan. Figura 4.2: Diagrama general de funcionamiento de OCLOptimizer

91 4.1. Descripción y funcionamiento de la herramienta Preprocesado En esta primera fase, el código de kernel es revisado secuencialmente, línea a línea, en busca de directivas de compilación de tipo #pragma oclopts similares a la descrita en el listado de código 4.2. Cada vez que el preprocesador de la herramienta localiza una de estas directivas, procesa la línea correspondiente y extrae los datos necesarios para su posterior aplicación. A partir de dichos datos, se construye una llamada a una función vacía definida en un fichero de cabeceras que el usuario habrá tenido que incluir en su código de kernel. Dicha función recibe como entradas los parámetros extraídos de la directiva procesada. Finalmente, se sustituye la directiva en formato #pragma por la llamada a la función vacía correspondiente. Esta transformación de las directivas en llamadas a funciones vacías se debe a ciertos problemas que tiene para su tratamiento la versión concreta de Clang utilizada en la implementación de la herramienta. El listado de código 4.3 muestra el resultado de este preprocesador para el ejemplo del listado # include " pragmas_ functions. h" 2 pragma_ oclopts_ unroll (2,16,2) 3 for ( int i =0;i<N;i ++) 4 { 5 a[ i] = a[ i] + s; 6 } Listado 4.3: Ejemplo de transformación a función de las anotaciones de optimización Optimización En la fase de optimización, para cada anotación encontrada en el kernel, la herramienta aplica sobre el fragmento de código al que afecta la optimización correspondiente mediante las transformaciones de código necesarias implementadas usando Clang. Puesto que cada optimización puede aplicarse sobre un mismo fragmento teniendo en cuenta diferentes parámetros, el resultado de esta fase es la generación de diversas versiones candidatas a ser seleccionadas como la óptima. Es en esta fase donde entra en juego todo el potencial ofrecido por Clang en lo que respecta a labores de análisis y trans-

92 70 4. La herramienta OCLOptimizer formación de código, ya que todo el proceso se realiza utilizando las funcionalidades de análisis léxico y sintático, construcción de ASTs, revisión y transformación de código previamente comentadas en el apartado 3.3 de la presente memoria. Los detalles de la problemática derivada de su uso serán comentados más adelante. Esta fase de la ejecución de la herramienta se compone de dos etapas difererenciadas, localización y aplicación de las optimizaciones, las cuales se detallan a continuación. Localización de las optimizaciones En esta primera etapa se recorre secuencialmente el código de kernel en busca de la primera llamada a función que se corresponda con alguna de las optimizaciones implementadas en la herramienta. Para ello se utilizan las funcionalidades que ofrece Clang para reconocimiento de la sintaxis del código e identificación de sus diferentes sentencias. Si se localiza alguna llamada a función asociada a una optimización, se recoge la información sobre la optimización procedente de la anotación original en forma de directiva #pragma. El código revisado en esta etapa puede ser tanto el código original anotado, si el proceso de optimización se encuentra en su primera iteración, como alguna de las versiones seleccionadas en una iteración previa. En ambos casos, la herramienta intentará localizar la siguiente optimización a aplicar, repitiéndose así el proceso hasta que no quede ninguna optimización pendiente. Aplicación de las optimizaciones En esta segunda etapa se realizan las transformaciones necesarias sobre el código para aplicar la optimización encontrada en la etapa anterior. Cada optimización indicada por la directiva correspondiente implica la generación del número de versiones determinado por los parámetros de la directiva (por ejemplo, para la optimización de partición en bloques sobre un bucle, se generarían tantas versiones como tamaños de bloque se deseen probar), las cuales serán compiladas y ejecutadas para evaluar su rendimiento.

93 4.1. Descripción y funcionamiento de la herramienta Evaluación La última fase del proceso, en la que se evalúa el rendimiento de las versiones generadas, también se divide en varias etapas. En la primera se genera un código de host para cada versión procedente de la fase anterior para, a continuación, en una segunda etapa, proceder a su compilación. En la última etapa se ejecuta cada versión varias veces sobre la plataforma establecida en el fichero de configuración, obteniendo sus tiempos medios de ejecución. Una vez ejecutadas se seleccionan aquellas que obtengan mejores tiempos de acuerdo a los parámetros establecidos por el usuario en la directiva correspondiente. Estas versiones seleccionadas se toman como base en una nueva iteración de la herramienta en la que se aplicará la siguiente anotación encontrada en el código. Una vez aplicadas todas las optimizaciones, se obvia el ciclo de realimentación y directamente se devuelve el código de kernel seleccionado como óptimo. A continuación se detallan estas etapas una a una. Generación del código de host Como ya se introdujo en el apartado sobre el modelo de ejecución de OpenCL, para poder ejecutar un código de kernel es necesario contar también con un código de host encargado de gestionar y preparar la plataforma de trabajo necesaria (definición del contexto, reserva, inicialización y transferencia de estructuras de datos, compilación del kernel, etc.). Todos estos parámetros del host son establecidos por el usuario en un fichero de configuración que, como puede verse en la figura 4.2, forma, junto con el código de kernel anotado, las entradas de la herramienta. En esta etapa, la herramienta simplemente escribe paso a paso en un fichero de texto las instrucciones en lenguaje C++ del código de host necesario para ejecutar el código de kernel asociado al mismo. Muchas de esas instrucciones reciben como parámetros la información contenida en el fichero de configuración antes comentado. Así mismo, estos códigos de host autogenerados incluyen una sección de profiling dedicada a la obtención del tiempo total de ejecución de los mismos, tiempo que será posteriormente

94 72 4. La herramienta OCLOptimizer utilizado como referencia en el proceso de evaluación de las diferentes versiones generadas. Este es un proceso, que, tal y como se puede ver en el ejemplo descrito en el apartado 2.3.1, resulta mecánico aunque a la vez, bastante tedioso si se ha de realizar a mano. Trabajando de esta forma, al mismo tiempo que se generan automáticamente los códigos de host necesarios para poder evaluar el rendimiento de las diferentes versiones de prueba, se proporciona al programador un medio para obtener sin coste alguno implementaciones funcionales de los mismos. Compilación de las versiones generadas Una vez que se ha generado el host correspondiente a cada uno de los códigos kernel a evaluar ya es posible llevar a cabo la siguiente etapa del proceso, consistente en la compilación de las versiones a evaluar. La herramienta, durante este proceso de compilación, efectúa, para cada host asociado a cada uno de los códigos de kernel a evaluar en el nivel del árbol de optimización correspondiente, la preceptiva llamada al sistema para invocar al compilador de C++ con los parámetros adecuados (nombre del fichero que contiene el código de host, rutas de acceso a librerías y ficheros de cabecera de OpenCL, etc.), los cuales también son extraídos del fichero de configuración antes comentado. Los programas resultantes de cada una de estas llamadas al sistema ya son generados utilizando nombres reconocibles por la herramienta, de modo que no sea necesario pasarlos como parámetros externos en el momento de su ejecución. Evaluación y selección Una vez generados todos los códigos correspondientes a las versiones a evaluar en un determinado nivel del árbol de optimización, la herramienta se encuentra en condiciones de ejecutarlos para obtener sus tiempos de ejecución. En función de los tiempos de ejecución y de los parámetros de tolerancia y límite de versiones establecidas por el usuario en la directiva añadida en el código, la herramienta seleccionará un número determinado de versiones que entrarán en el siguiente ciclo del proceso de optimización iterativa.

95 4.1. Descripción y funcionamiento de la herramienta 73 El primer paso del proceso de evaluación consiste en la ejecución y medición de los tiempos de ejecución de cada uno de los programas antes citados. Para ello, el generador de hosts incluye en cada código una sección de profiling de manera que, tras su ejecución, si ésta ha resultado exitosa, se muestre por salida estándar el tiempo de ejecución correspondiente. Para poder enviar directamente dichos tiempos a la herramienta, ésta crea un subproceso por cada ejecución a realizar. Dichos subprocesos ejecutan el código de host autogenerado correspondiente, el cual imprime el tiempo de ejecución del kernel (obtenido en la sección de profiling antes comentada) en la salida estándar del subproceso correspondiente. Dicha salida estándar está redirigida a un buffer de memoria reservado a tal efecto por la herramienta. El mecanismo de evaluación ejecuta cada kernel en cada experimento varias veces y calcula su tiempo medio de ejecución. Una vez calculados todos los tiempos medios se inicia el proceso de selección, que sigue el algoritmo que se detalla a continuación: 1. Para cada tamaño de experimento establecido en el fichero de configuración, ordenar los programas de forma ascendente de acuerdo con el tiempo medio de ejecución obtenido. 2. Descarte de todos los programas cuyo tiempo medio de ejecución sea superior al límite establecido por el porcentaje de tolerancia especificado en la directiva asociada a la técnica de optimización procesada. Las dos opciones extremas serían o bien dejar pasar todas, lo que acabaría provocando una explosión combinatoria inmanejable, o bien dejar pasar sólo una, que como ya se ha comentado podría suponer el descarte prematuro de combinaciones que resultasen ser óptimas. Por ejemplo, para un parámetro tolerance 90 se admitirían solamente aquellas versiones cuyo tiempo medio de ejecución sea, como máximo, un 90 % superior al mínimo obtenido para el tamaño de experimento correspondiente. Si no se incluye dicho parámetro, no se aplicaría este criterio de selección y todas las versiones ejecutadas avanzarían al paso Para cada tamaño de experimento, se otorga a cada versión del kernel una puntuación igual a la posición que ésta ocupa por orden ascendente de tiempos y se

96 74 4. La herramienta OCLOptimizer acumulan sus puntuaciones. Para un ejemplo con dos tamaños de experimento, si una versión es la más rápida en uno de ellos recibirá 1 punto, mientras que si es la tercera en el otro, recibirá 3 puntos, sumando un total de 4 puntos. 4. Una vez acumuladas las puntuaciones de todas las versiones, éstas son ordenadas de forma ascendente. Si la directiva de optimización aplicada no tenía especificado ningún límite number, todas las versiones pasarían a la siguiente iteración. En caso de que sí estuviese especificado dicho límite, se seleccionarían las number mejores, o bien todas las que hubiesen llegado hasta este último paso si su número resultase ser superior. El conjunto de versiones seleccionadas sería proporcionado como entrada en la siguiente iteración del proceso de optimización, repitiéndose éste hasta que no reste ninguna optimización pendiente de aplicar en los kernels de entrada, en cuyo caso, se selecciona directamente como versión óptima aquella que haya obtenido mejor puntuación (es decir, la más baja) y se pone fin al proceso de optimización iterativa Ejemplo completo de ejecución En esta sección se presentará un ejemplo completo de ejecución de la herramienta tomando como base un código de kernel que realiza dos multiplicaciones matriz-vector diferentes de tamaño , obteniendo dichos productos como resultados parciales y la suma de los vectores resultado de ambos como resultado final Código original y código anotado El código de kernel original del ejemplo planteado puede consultarse en el listado B.1 del apéndice B. Dicha versión es anotada por el usuario de acuerdo a lo mostrado en el listado de código 4.4:

97 4.2. Ejemplo completo de ejecución 75 1 # include " pragmas_ functions. h" 2 kernel void matvecmul_2 (...) { # pragma oclopts unroll number 1 5 for (j =0;j<N;j ++) { 6 R1 += b1[xid *N+j]* v1[j]; 7 } # pragma oclopts unroll for (j =0;j<N;j ++) { 11 R2 += b2[xid *N+j]* v2[j]; 12 } } Listado 4.4: Extracto del kernel de ejemplo anotado con directivas #pragma oclopts En el primer bucle el usuario decide probar a aplicar desenrollamiento con factores 4, 8, 12 y 16 y limitar la selección de las versiones resultantes a la mejor. Por tanto, la directiva que debe emplear es #pragma oclopts unroll number 1, tal y como puede verse en la línea 4 del listado 4.4. En el segundo bucle, el usuario decide probar a aplicar los mismos factores de desenrollamiento, incluyendo para ello la directiva #pragma oclopts unroll En este caso, al tratarse del último bucle del código, no procede indicar en la directiva ningún parámetro relacionado con el filtrado de las versiones, es decir, tolerance y/o number Fichero de configuración En lo que respecta al fichero de configuración, los listados B.3 a B.9 del apéndice B muestran el contenido de las diferentes secciones en las que éste se estructura. A continuación se comentan dichas secciones una a una: Parámetros comunes: Se definen parámetros generales como el tamaño del

98 76 4. La herramienta OCLOptimizer problema y el número de dimensiones del mismo (N=2048 y ndims=1; aunque se esté trabajando con matrices bidimensionales, el problema es unidimensional) y se establecen el tipo de dispositivo en que se va a ejecutar (device=gpu) y el número de argumentos del kernel de entrada (nargs=8). Parámetros del compilador: Se escoge el modo de compilación de la herramienta (en esta versión inicial, el único soportado es system) y se proporcionan las rutas de los ficheros de librerías (ocllibpath) y cabeceras (oclincludepath), necesarias para poder compilar los códigos de host. Espacio de trabajo: Se define el tamaño y dimensiones de los espacios de trabajo local y global de OpenCL. En este caso, se crearían tantos work-items como elementos tiene el vector resultado (globalsize=n) y no se definirían work-groups (localsize=1), por lo que cada work-item computaría un elemento del resultado. Argumentos: Para cada uno de los argumentos especificados en la cabecera de la función del fichero de kernel, se definen los buffers que los almacenarán en el host, y que servirán de punto de transferencia de los mismos entre la memoria de éste y las regiones de memoria del dispositivos en que se ejecute el kernel. Por ejemplo, para el resultado final de la operación, se establece el nombre (name=a), el número de elementos (size=n), el tipo (type=float*, es decir, un vector) y el tipo de buffer OpenCL con el que estará asociado (en este caso, mode=w, es decir, escritura en un buffer de salida de OpenCL). La explicación es análoga para el resto de argumentos Descripción del proceso de optimización iterativa Una vez anotado el código de kernel y preparado el fichero de configuración, ya es posible invocar a la herramienta para iniciar el proceso de optimización iterativa. Si bien el ejemplo de código planteado no es especialmente óptimo, puesto que es posible fusionar ambos bucles de modo que en cada iteración cada work-item calcule sus elementos de los vectores resultado de ambos productos, resulta muy adecuado para

99 4.2. Ejemplo completo de ejecución 77 ilustrar el funcionamiento del proceso de optimización iterativa, ya que en primer lugar se aplicaría la optimización correspondiente a la primera directiva y, para aquella versión o versiones seleccionadas, se aplicaría la segunda directiva. Una vez aplicada esta última directiva y evaluados los códigos generados por ésta, se seleccionaría directamente la versión con un tiempo medio de ejecución menor. A continuación se detallará paso a paso este proceso con el apoyo del diagrama de la figura 4.3. Figura 4.3: Proceso de optimización iterativa ejecutado en el ejemplo La herramienta recibe el código de kernel anotado por el usuario de acuerdo a lo comentado en la sección Dicho fichero pasa una fase de preprocesado para sustituir las directivas por llamadas a funciones vacías. El resultado es la versión inicial del proceso, identificada en el diagrama como versión 0. Sobre esa versión se aplica la primera directiva encontrada, consistente en la aplicación de desenrollamiento de bucles con factores 4, 8, 12 y 16 sobre el bucle que realiza la primera multiplicación

100 78 4. La herramienta OCLOptimizer matriz-vector. Como resultado de la aplicación de esta primera optimización se generan 4 versiones, identificadas en el diagrama como versiones 1, 2, 3 y 4. Para cada una de estas cuatro versiones, la herramienta genera el respectivo código de host. Dichos hosts son compilados y ejecutados varias veces para obtener los tiempos medios de ejecución de las cuatro versiones. Como el usuario ha indicado en la directiva aplicada que solamente desea seleccionar la mejor versión de todas las generadas con esa optimización (parámetro number 1), el mecanismo de evaluación solamente permite que sea la versión 3 la que continúe en el proceso de optimización. En la siguiente iteración, la herramienta detecta la segunda directiva (#pragma oclopts unroll ) en el fichero de kernel de la versión 3, por lo que aplica la optimización correspondiente. Como resultado de dicha optimización se generan otras cuatro versiones, identificadas en el diagrama como versiones 31, 32, 33 y 34. Junto a estas cuatro versiones se generan sus respectivos hosts, que son compilados y ejecutados varias veces para obtener sus tiempos medios de ejecución. En este caso, como no hay más optimizaciones pendientes, el mecanismo de evaluación devuelve directamente la versión 34 como la óptima para la plataforma establecida en el fichero de configuración. Los listados B.19 a B.23 del apéndice B muestran un extracto de la información que va mostrando la herramienta por pantalla a medida que avanza el proceso. Los listados de código 4.5 y 4.6 muestran de forma esquemática los desenrollamientos propuestos por la herramienta para la versión óptima del kernel del ejemplo, pudiendo consultarse en la figura B.10 del apéndice su forma completa. Así mismo, en el apéndice también pueden consultarse las principales partes del código de host autogenerado por la herramienta para la ejecución de dicha versión (listados B.11 a B.18 del apéndice). A modo ilustrativo, también se incluye, en el listado B.24, el desenrollamiento propuesto para la versión intermedia 3.

101 4.2. Ejemplo completo de ejecución 79 1 # include " pragmas_ functions. h" 2 kernel void matvecmul_2 (...) { for (j =0;j <((( N) -(12*1) ) +1) ;j=j +(12*1) ){ 5 R1 += b1[xid *N+j]* v1[j]; 6 R1 += b1[xid * N + (j +1) ] * v1 [(j+1) ]; 7 << desenrollamiento entre 2 y 10 >> 8 R1 += b1[xid * N + (j +11) ] * v1 [(j +11) ]; 9 } 10 for (j;j < N;j ++) 11 R1 += b1[xid * N + j] * v1[j]; << Segundo bucle desenrollado >> } Listado 4.5: Desenrollamiento óptimo propuesto para el primer bucle 1 # include " pragmas_ functions. h" 2 kernel void matvecmul_2 (...) { << Primer bucle desenrollado >> for (j =0;j <((( N) -(16*1) ) +1) ;j=j +(16*1) ){ 7 R2 += b2[xid *N+j]* v2[j]; 8 R2 += b2[xid * N + (j +1) ] * v2 [(j+1) ]; 9 << Desenrollamiento entre 2 y 14 >> 10 R2 += b2[xid * N + (j +15) ] * v2 [(j +15) ]; 11 } 12 for (j;j < N;j ++) 13 R1 += b1[xid * N + j] * v1[j]; } Listado 4.6: Desenrollamiento óptimo propuesto para el segundo bucle

102 80 4. La herramienta OCLOptimizer 4.3. Estructura En la figura 4.4 se muestra el diagrama de clases de la herramienta. A continuación se comentan la estructura general del diseño, las clases concretas que lo componen y el cometido de cada una de ellas. Nótese que las clases sombreadas en color grisáceo ya existían en Clang, si bien se incluyen en el diagrama para mostrar sus relaciones con las clases de la herramienta. A continuación se describirán de forma sucinta cada una de las clases que componen la herramienta. Para mayor nivel de detalle, se anima al lector a consultar la documentación en formato Doxygen incluida en el soporte de almacenamiento óptico que acompaña a la presente memoria (consultar el apéndice C para conocer la ubicación exacta de dicho recurso). Las clases que componen la herramienta se dividen en cuatro grandes paquetes: PragmaPreprocessor: Este paquete contiene las clases que implementan el preprocesador de ficheros de kernel, encargándose de recorrerlos, sustituir las directivas por llamadas a las funciones vacías comentadas anteriormente y extraer de las mismas información relevante para el funcionamiento general de la herramienta. OcloptsGenerator: Recoge todas las clases relacionadas con la aplicación de las optimizaciones y las transformaciones de código necesarias para ello. Así mismo, el subpaquete Annotations incluye la jerarquía de clases que modela la gestión de las diferentes anotaciones, que será comentada en detalle en la sección HostGenerator: Contiene las clases encargadas de la generación de los códigos de host adaptados a las necesidades de cada uno de los kernels, así como, en el subpaquete ExperimentsConfig, el mecanismo de procesado del fichero de configuración de experimentos de la herramienta. VersionSelector: Sus clases implementan el mecanismo de compilación, ejecución, evaluación y selección de las diferentes versiones de kernel generadas en cada nivel de optimización.

103 4.3. Estructura 81 Figura 4.4: Diagrama de clases de la herramienta

104 82 4. La herramienta OCLOptimizer A continuación se describirán brevemente cada una de las clases contenidas en estos paquetes. Se recuerda al lector que tiene a su disposición la documentación del código en formato Doxygen incluida en el soporte de almacenamiento óptico adjunto a la presente memoria (véase el apéndice C para conocer su ubicación exacta). Paquete PragmaPreprocessor Este paquete contiene dos clases: la propia PragmaPreprocessor, que implementa todo el proceso de recorrido secuencial del fichero para la búsqueda y sustitución de las directivas de tipo #pragma oclopts por las llamadas a funciones vacías, y PragmaInformation, que recoge cierta información de dichas anotaciones que resulta necesaria para la ejecución del algoritmo de optimización planteado. Paquete OcloptsGenerator Las clases CodeRewriter y CodeVisitor implementan todo el contexto necesario para poder utilizar las funcionalidades de análisis y transformación de código proporcionadas por Clang. Dicho contexto lo compone una serie de clases de Clang que CodeRewriter y CodeVisitor incluyen como atributos y que se ocupan de tareas como la implementación del mecanismo de análisis de código, la gestión de los buffers de ficheros, la selección del lenguaje de programación o extensión del mismo (en este caso, OpenCL) en que está escrito el código a analizar, las rutas donde Clang puede buscar los ficheros de cabecera incluidos en el código, etc. La clase OcloptsGenerator es el punto de entrada del algoritmo a las operaciones de transformación. Se encarga de ordenar a Clang que inicie el análisis de cada código de kernel a procesar. Por su parte, KernelInstance modela la información más representativa de un fichero de kernel: su nombre, el nombre de la función de OpenCL que implementa, etc. El subpaquete Annotations contiene todo lo necesario para la construcción y aplicación de las diferentes optimizaciones: la factoría AnnotationCreator, la clase abstracta Annotation y las clases concretas que modelan las diferentes optimizaciones implemen-

105 4.3. Estructura 83 tadas. Es en dichas clases concretas donde se realizaría el proceso de transformación de código asociado a cada una de las optimizaciones soportadas, utilizando para ello las herramientas proporcionadas por el API C++ de Clang. Posteriormente se comentará en detalle la implementación de la optimización de desenrollamiento de bucles, recogida en la clase UnrollingAnnotation. Paquete HostGenerator La clase principal de este paquete es la propia HostGenerator, encargada de realizar todo el proceso de escritura del fichero de host a partir de la información procedente tanto de la instancia correspondiente de KernelInstance (paquete OcloptsGenerator) como del fichero de configuración de experimentos. El subpaquete ExperimentsConfig contiene la implementación de un lector de ficheros de configuración, basado en las herramientas Lex y Yacc y que forma parte del proyecto de simulación de arquitecturas de computadores SESC [20], el cual se distribuye como software libre bajo licencia GPL. Para recoger y gestionar la información relevante procedente de dicho fichero se proporcionan las clases Argument, CommonParameters y Dimension, que modelan respectivamente cada uno de los argumentos de la función implementada en el código de kernel, parámetros comunes de ejecución (rutas a librerías, por ejemplo) y las dimensiones y tamaños de los work-groups de OpenCL. El acceso a toda esta información se realiza a través de la clase ConfigFileParser, encargada de invocar el proceso de parseado implementado en ExperimentsConfig e instanciar las clases antes comentadas a partir de la información recogida. La información de cada uno de los códigos de host generados (nombres del propio fichero de host y del kernel asociado, tamaño del experimento que realiza, etc.) es almacenada en una instancia de la clase HostInstance.

106 84 4. La herramienta OCLOptimizer Paquete VersionSelector La clase VersionSelector implementa el proceso completo de evaluación de las versiones generadas en un determinado nivel de optimización. Para ello, necesita de las clases incluidas en el paquete HostCompiler, las cuales se encargan de compilar los códigos de host, ejecutarlos y recoger los tiempos de ejecución obtenidos. Se contempla la posibilidad de proporcionar diversos métodos de compilación, por lo que se implementa una factoría (HostCompilerCreator) que crea instancias concretas que extienden a la clase abstracta HostCompiler. Por el momento, el único procedimiento implementado para realizar la compilación se basa en la realización de llamadas directas al sistema invocando a g++ con los parámetros adecuados, lo cual se implementa en la clase SystemHostCompiler. Una vez compilados, la clase HostExecutor se encarga de la ejecución de cada uno de los códigos de host. Finalmente, la clase ExperimentResult almacena los resultados de cada una de las pruebas realizadas, recogiendo cada uno de los tiempos de ejecución y calculando su media Optimización implementada: Loop unrolling Antes de proceder a comentar los detalles de implementación al desenrollamiento de bucles se realizará una pequeña introducción teórica a la optimización y a las ventajas que ésta puede aportar Introducción teórica Uno de los objetivos principales a la hora de escribir código óptimo para cualquier procesador moderno es conseguir aprovechar al máximo su arquitectura segmentada (ver ejemplo en figura 4.5), intentando que el pipeline se mantenga ocupado con instrucciones independientes entre sí durante toda la ejecución del programa [21]. Una de las formas de optimizar un fragmento de código siguiendo esta premisa es, precisamente, el desenrollamiento de bucles.

107 4.4. Optimización implementada: Loop unrolling 85 Figura 4.5: Esquema de ejecución de un pipeline de 5 etapas La técnica de optimización de desenrollamiento de bucles o loop unrolling consiste en replicar las instrucciones de un bucle un determinado número de veces, llamado factor de desenrollamiento a la vez que se reduce el número de iteraciones del dicho bucle multiplicando el incremento por dicho factor. En la figura 4.6 puede verse cómo se aplica la técnica considerando un factor de desenrollamiento 2 para un código sencillo que incrementa el valor de cada elemento de un vector a de 1000 posiciones una cantidad fija s. Con esta transformación se aumenta el número de instrucciones independientes (nótese que no hay dependencias de escritura entre ellas, sino que cada una sobreescribe su propia variable, lo cual no supone un problema) que pueden ser ejecutadas de forma segmentada por el procesador, lo que, como ya se ha comentado, implica un mejor aprovechamiento del pipeline. Además, el bucle desenrollado realiza un menor número de iteraciones, por lo que se reduce también la sobrecarga derivada de la comprobación de salto y el cálculo de la siguiente dirección a cargar en el contador de programa. Este segundo aspecto no es de gran relevancia si el código va a ejecutarse en CPUs modernas, las cuales suelen contar con circuitería específica que predice el comportamiento de los bucles [22], de modo que para la gran mayoría de las iteraciones no es necesario repetir estos cálculos. Sin embargo, la circuitería de otros tipos de procesadores, como por ejemplo, las GPUs, a menudo prescinden de ellos en aras de poder dedicar más recursos a la implementación de operaciones aritméticas. En el ejemplo anterior (figura 4.6) se da el caso de que el número de iteraciones del bucle original (1000) y el factor de desenrollamiento aplicado (2) son divisibles, de modo que la longitud del bucle desenrollado se reduce a 500 iteraciones (1000/2 = 500).

108 86 4. La herramienta OCLOptimizer (a) Código original (b) Código desenrollado Figura 4.6: Ejemplo de desenrollamiento de bucles con factor 2 Sin embargo, esto no tiene por qué suceder siempre. Véase ahora el ejemplo de la figura 4.7, en el que se aplica sobre el mismo código original la técnica de desenrollamiento de bucles con factor 3. En este caso, el número de iteraciones (1000) y el factor de desenrollamiento (3) no son divisibles (1000/3 = 333, 33...), por lo que si únicamente se aplican los cambios comentados para el ejemplo anterior se ejecutarían 333 iteraciones. De este modo, al final del bucle el último elemento del vector (a[999]) seguiría teniendo el valor antiguo. Esta situación es la que refleja el código de la figura 4.7(b). Para tener en cuenta estas situaciones existe una versión general de la transformación que a continuación se comenta y cuyo resultado puede verse, para factor 3, en el código de la figura 4.7(c): (a) Código original (b) Código mal desenrollado (c) Código bien desenrollado Figura 4.7: Ejemplo de desenrollamiento de bucles con factor 3

109 4.4. Optimización implementada: Loop unrolling 87 Se replican las instrucciones del bucle tantas veces como indique el factor de desenrollamiento, modificando las ocurrencias del contador del bucle para que hagan referencia al número de iteración correspondiente del bucle original. Es decir, si una instrucción es la réplica k de la original, la ocurrencia de la variable contador pasa de ser i a ser i+k. Se multiplica el incremento del bucle por el factor de desenrollamiento aplicado, resultando así el número de iteraciones del bucle desenrollado como la división entre el número de iteraciones del bucle original y el factor aplicado. Se modifica la condición de parada del bucle restándole el factor de desenrollamiento más 1. Si no se realiza esta modificación en el ejemplo, el bucle iteraría hasta un valor i=999, de modo que se calcularía el valor del elemento a[999], lo cual es correcto, pero también los de a[1000] y a[1001], sobrepasando los límites del vector. Con esta modificación, el bucle itera solamente hasta i=996, iteración en la que se calcularían los nuevos valores de a[996], a[997] y a[998]. Se añade a continuación del bucle desenrollado una copia modificada del bucle original que computa las iteraciones restantes. Con la incorporación de este bucle adicional se ejecutaría la iteración restante, calculando el nuevo valor de a[999] y finalizando así la computación de las 1000 posiciones del vector a Implementación en la herramienta Una vez explicada la formulación teórica de la técnica de desenrollamiento de bucles incorporada a la herramienta, se procede a comentar algunos detalles de implementación de la misma. El punto de entrada de la herramienta al proceso de transformación de código reside en el método abstracto ApplyAnnotation() de la superclase Annotation, que para esta optimización es extendida por la clase UnrollingAnnotation, dotando de una implementación concreta a dicho método para la técnica de desenrollamiento de bucles. Cada vez que el analizador de código detecta una nueva directiva de optimización, es invocado un método de la clase CodeVisitor que aplica la anotación llamando

110 88 4. La herramienta OCLOptimizer a ApplyAnnotation(). El listado 4.7 muestra el pseudocódigo asociado a la implementación dada por la clase UnrollingAnnotation al método ApplyAnnotation(). Este método realiza, para cada uno de los factores de desenrollamiento establecidos en la directiva correspondiente, los pasos necesarios para aplicar la transformación de código que implementa esta clase: en primer lugar, se instancia la clase Rewriter de Clang (línea 2), la cual permite realizar modificaciones del código fuente analizado; a continuación, borrado de la anotación de código para evitar que vuelva a ser procesada en iteraciones posteriores (línea 3) y finalmente, generación de la nueva versión del usando un determinado factor de desenrollamiento. Para generar esta nueva versión se realizan los siguientes pasos: se modifica tanto el incremento (línea 4) como la condición de parada del bucle (línea 5) en función del factor de desenrollamiento aplicado, para después realizar el desenrollamiento del cuerpo del bucle (línea 6) y añadir el bucle que computa las iteraciones restantes (línea 7). 1 void UnrollingAnnotation :: ApplyAnnotation (...) { 2 Rewriter * coderewriter = new Rewriter (...) ; 3 RemoveAnnotation (...) ; 4 UnrollIncrement (...) ; 5 AdaptLoopCondition (...) ; 6 UnrollBody (...) ; 7 << construcción y adición manual del bucle adicional >> 8 } Listado 4.7: Implementación del método UnrollAnnotation::ApplyAnnotation() A continuación se comenta la implementación de cada uno de los pasos antes comentados. Cancelación de la anotación procesada El primer paso consiste en eliminar la anotación procesada sustituyendo la llamada a la función vacía asociada a la optimización por una línea de comentario advirtiendo

111 4.4. Optimización implementada: Loop unrolling 89 de que el código ha sido modificada por la herramienta. De esta forma se informa al usuario de que su código ha sido transformado, a la vez que se evita que se vuelva a procesar la anotación en iteraciones posteriores del algoritmo. Adaptación de la cabecera del bucle original A continuación se adapta la cabecera del bucle aplicando dos cambios diferenciados, se aplica el desenrollamiento sobre el incremento del bucle y se adapta la condición de parada para contemplar la posibilidad de que el número de iteraciones no sea múltiplo del factor de desenrollamiento. De lo primero se encarga el método UnrollIncrement() de UnrollingAnnotation, cuya implementación para el caso concreto de un incremento en notación posfija (por ejemplo, i++) se muestra en el listado 4.8. Una vez comprobado el tipo de incremento (línea 2), se obtiene el nombre de la variable contador (línea 4) y, mediante un método auxiliar, se transforma para multiplicarlo por el factor de desenrollamiento y el paso original del bucle (línea 5). 1 void UnrollingAnnotation :: UnrollIncrement (...) { 2 if (isa < UnaryOperator >( Inc )){ 3 (* loopstep ) = 1; 4 (* counterasstring ) = currentrewriter - > ConvertToString ((( UnaryOperator *) Inc ) -> getsubexpr ()); 5 currentrewriter - > ReplaceText ( Inc - > getsourcerange (), PrepareLongIncrementString ((* counterasstring ), unrollingfactor, (* loopstep ))); 6 } 7 else return ; 8 } Listado 4.8: Esquema del método UnrollingAnnotation::UnrollIncrement() Lo segundo, en cambio, es tarea del método AdaptLoopCondition(), cuya implementación puede consultarse en el listado de código 4.9. El proceso consiste en obtener la cadena de la condición de parada del bucle (línea 3), preparar la nueva expresión modificada de acuerdo con lo comentado en la introducción teórica al desenrollamiento

112 90 4. La herramienta OCLOptimizer de bucles (líneas 6-14) y, finalmente, sustituirla en el código utilizando la instancia de Rewriter (línea 15). 1 void UnrollingAnnotation :: AdaptLoopCondition (...) { 2 if (isa < BinaryOperator >( Cond )){ 3 string conditionasstring = currentrewriter - > ConvertToString ((( BinaryOperator *) Cond ) -> getrhs ()); 4 string bracketedcondition = "(" + conditionasstring +")"; 5 6 std :: stringstream ss; 7 ss << "("; 8 ss << unrollingfactor ; 9 ss << "*"; 10 ss << loopstep ; 11 ss << ")"; 12 string substractiontocondition = ss. str (); string newcondition = "(" + bracketedcondition + "-" + substractiontocondition + ")"; 15 currentrewriter - > ReplaceText ((( BinaryOperator *) Cond ) -> getrhs () -> getsourcerange (), newcondition ); 16 } 17 } Listado 4.9: Esquema del método UnrollingAnnotation::AdaptLoopCondition() Desenrollamiento del cuerpo del bucle Una vez aplicadas las modificaciones sobre la cabecera del bucle original, el siguiente paso a realizar es el desenrollamiento del cuerpo de dicho bucle. El método responsable de esta tarea es UnrollingAnnotation::UnrollBody(), cuya implementación se recoge de forma esquemática en el listado de código El proceso implementado en dicho método se basa en recorrer secuencialmente (líneas 3-9) las sentencias que forman el cuerpo del bucle, intentando desenrollarlas mediante una llamada al método UnrollStatement(). En esta implementación de la optimización se aplica solamente a

113 4.4. Optimización implementada: Loop unrolling 91 bucles que no contienen anidamientos y cuyas instrucciones son de tipo sencillo, como operaciones aritméticas o llamadas a funciones. En caso de que se detecte algún otro bucle for dentro del cuerpo del bucle principal se aborta la ejecución y se advierte al usuario del problema. Si no surge este inconveniente, se desenrollan una a una las sentencias mediante la llamada UnrollStatement(), a partir de las que se genera la cadena de texto que sustituirá al cuerpo del bucle original (líneas 10-13). 1 void UnrollingAnnotation :: UnrollBody (...) { 2 multimap < long, string > unrolledblocks ; 3 for ( cuerpo del bucle ){ 4 if(isa < ForStmt >(* I)){ 5 // If there are any nested loop, stop the process 6 return ; 7 } 8 else UnrollStatement (...) ; 9 } 10 for ( sentencias desenrolladas ){ 11 << construir cadena de texto >> 12 } 13 << insertar cadena con sentencias desenrolladas >> 14 } Listado 4.10: Esquema de implementación de UnrollAnnotation::UnrollBody() Como ya se ha comentado, el método encargado de desenrollar una a una las sentencias del bucle original es UnrollStatement(). Este método, cuyo pseudocódigo puede consultarse en el listado 4.11, comienza buscando en la sentencia a desenrollar ocurrencias de la variable contador (líneas 3-5). En caso de encontrar alguna (línea 7), genera las sentencias desenrolladas creando copias de la original y sustituye la variable contador por su equivalente desenrollado (líneas 8-9). Si las sentencias encontradas no tienen ocurrencias de la variable contador, se copian directamente sin aplicarles ningún cambio (línea 12).

114 92 4. La herramienta OCLOptimizer 1 void UnrollingAnnotation :: UnrollStatement (...) { bool found = false ; 4 FindVariableOccurrences (...) ; 5 if( found ) CountVariableOccurrences (...) ; 6 for ( factores de desenrollamiento ){ 7 if( found ){ 8 RewriteStatement (...) ; 9 << insertar sentencia reescrita >> 10 } 11 else { 12 << insertar sentencia tal cual >> 13 } 14 } } Listado 4.11: Esquema del código de UnrollingAnnotation::UnrollStatement() 4.5. Detalles de diseño e implementación En esta sección se comentarán diferentes aspectos relevantes acerca del diseño y la implementación de la herramienta, como los diferentes problemas que ha planteado el enfoque que Clang ofrece para algunas operaciones de análisis y transformación de código (sección 4.5.1) o diferentes decisiones tomadas acerca del modelado de las optimizaciones soportadas por la herramienta (sección 4.5.2) Interacción con Clang Como ya se ha comentado en capítulos anteriores, Clang proporciona una serie de ventajas respecto otras alternativas similares: Facilidad de integración en otras aplicaciones gracias al API C++ que da acceso a sus librerías, a diferencia de, por ejemplo, el frontend de GCC.

115 4.5. Detalles de diseño e implementación 93 Mantenimiento de una legibilidad aceptable del código C obtenido tras las transformaciones aplicadas. Una alternativa al uso de Clang podría haber sido utilizar directamente LLVM generando un binario escrito en su lenguaje intermedio LLVM-IR y luego transformarlo a código C, pero el resultado de dicha transformación dista mucho de parecerse al código original, lo que dificultaría enormemente la comprensión e identificación de las optimizaciones propuestas por el usuario. Sin embargo, pese a la comodidad que estas ventajas suponen, también existen aspectos negativos que han dificultado en algunas ocasiones el completo aprovechamiento de las funcionalidades de análisis y transformación de código de Clang. A continuación se comentan algunos ejemplos de ello. Diseño adaptado a la interfaz Pese a la comodidad que supone el disponer de un API escrita en lenguaje C++ para poder integrar en la herramienta las funcionalidades de análisis y transformación de código que ofrece Clang, el diseño de las clases y métodos que la componen no está exclusivamente pensado para dar servicio en todo tipo de situaciones y usos en diferentes herramientas, ésta es simplemente una posibilidad que ofrece el frontend gracias a su arquitectura basada en librerías. En realidad, la orientación principal de su arquitectura es la construcción de la herramienta principal clang comentada en la descripción de la sección Esta limitación ha llevado a que no haya sido posible establecer para la herramienta un diseño lo más flexible posible, ya que en muchas ocasiones las diferentes decisiones de diseño se han visto condicionadas por completo por esta interfaz que ofrece Clang para sus clases y métodos. Reescritura de código Como ya se ha comentado, el desenrollamiento de bucles implica, entre otras cosas, la replicación de las instrucciones presentes en el cuerpo de los mismos tantas veces como

116 94 4. La herramienta OCLOptimizer indique el factor de desenrollamiento aplicado y modificar las ocurrencias de la variable contador del bucle en dichas instrucciones sumándole el factor de desenrollamiento correspondiente. Es decir, si el contador es una variable i, en la réplica k será sustituido por i+k. Es evidente que para poder realizar transformaciones de este tipo es necesario contar con algún mecanismo que permita hacer cosas como localizar las ocurrencias de i en las instrucciones y sustituirlas por las i+k correspondientes. Clang ofrece la posibilidad de sustituir cualquier elemento del código que haya sido identificado en el AST por cualquier otra cadena de texto mediante la clase Rewriter (véase el tutorial sobre transformación de código de la sección para más información). Sin embargo, una vez que se ha reescrito alguna parte del código utilizando este mecanismo, el fragmento reescrito no se incluye en el AST, por lo que no es posible acceder directamente a ese nuevo fragmento del código mediante un puntero al árbol y, por tanto, no es posible aplicar sobre dicho fragmento una nueva reescritura o cualquier otra transformación. Esto se debe a que los métodos de Rewriter localizan las sentencias a modificar a través de una instancia de la clase SourceRange, que representa el rango de líneas que ocupa dicha instrucción en el código y que es, a su vez, unos de los atributos del nodo correspondiente del árbol. Una opción interesante para superar esta carencia sería intentar replicar directamente sobre el AST las instrucciones del cuerpo del bucle, de modo que sí existan punteros a las mismas y así poder realizar las sustituciones necesarias utilizando los métodos de la clase Rewriter. Desgraciadamente, el API de Clang no ofrece en absoluto un mecanismo intuitivo para realizar modificaciones de este tipo sobre el AST, y repetidas consultas a la comunidad de desarrolladores de Clang han tenido siempre como respuesta que es posible hacerlo, pero que las tareas de creación de nuevos nodos y de comprobación de la corrección de la construcción del AST superan con creces la complejidad que puede tener plantear algún tipo de workaround adaptado a la transformación de código que se desee realizar. Por este motivo, se ha optado por implementar un workaround basado en la manipulación de las cadenas de texto asociadas a las instrucciones a replicar. Este proceso se compone de los siguientes pasos:

117 4.5. Detalles de diseño e implementación Para cada instrucción del cuerpo del bucle a desenrollar, extracción de su cadena de texto en el fichero de código mediante el método ConvertToString() de la clase Rewriter. 2. Replicación de la cadena correspondiente a cada instrucción tantas veces como indique el factor de desenrollamiento a aplicar sobre el bucle. 3. Búsqueda de ocurrencias en cada réplica de la variable contador correspondiente en dichas instrucciones, utilizando para ello las funciones de búsqueda de texto de la clase std::string. 4. Para cada ocurrencia de la variable contador, se realiza el desenrollamiento de la misma según el número de réplica correspondiente. Para realizar esta transformación se ha implementado el método auxiliar RewriteStatement(), que se sirve para ello de los métodos de manipulación de cadenas de std::string. 5. Una vez desenrollada, la nueva instrucción es concatenada en una cadena que contendrá el conjunto de sentencias desenrolladas correspondientes a la misma instrucción original. 6. Finalmente, se sustituye mediante Rewrite::ReplaceText() la instrucción original (la cual sí es accesible mediante un puntero al nodo del AST) por la cadena que contiene la concatenación de instrucciones desenrolladas para dicha instrucción original Modelado de las optimizaciones Independientemente del tipo de optimización que se desee aplicar, el procedimiento a seguir es, en principio, siempre el mismo: se localiza una anotación, se analiza para obtener la información necesaria, se elimina dicha anotación del código y se aplica la transformación correspondiente sobre el fragmento de código afectado.

118 96 4. La herramienta OCLOptimizer Jerarquía de clases La existencia de este conjunto de operaciones comunes a cualquier tipo de optimización, independientemente de las transformaciones concretas de código que ésta conlleve, recomienda el modelado del conjunto de anotaciones en forma de una jerarquía encabezada por una clase que factorice dichas operaciones y proporcione a la vez una interfaz para la aplicación de las transformaciones de código necesarias. De esta forma, cada nueva optimización implementada será modelada como una clase de dicha jerarquía, heredando los métodos comunes procedentes de la superclase e implementando todo lo necesario para realizar la transformación de código asociada a la optimización. Con esta forma de trabajar se pretende desacoplar las operaciones básicas de gestión de las diferentes optimizaciones respecto de las transformaciones concretas de código que se han de realizar, de modo que la parte del código encargada de ordenar la aplicación de una determinada optimización no necesite saber qué tipo de optimización se está aplicando. Instanciación mediante método factoría Sin embargo, esto introduce la necesidad de delegar en algún componente de la herramienta la tarea de decidir, en base a la información extraída de la anotación correspondiente, cuál es el tipo concreto de optimización que se desea aplicar. Para implementar esta funcionalidad se ha decidido utilizar una versión simplificada del patrón de diseño conocido como método factoría [23], cuya estructura puede verse en la figura 4.8, de manera que es la clase AnnotationCreator quien decide qué clase concreta de optimización según la información contenida en la anotación correspondiente. Por ejemplo, para una directiva de tipo #pragma oclopts unroll, el método factoría CreateAnnotation() crearía una instancia de la clase UnrollAnnotation, la cual, como tal, incluiría los métodos necesarios para realizar las transformaciones necesarias en un desenrollamiento de bucles, pero como instancia también de Annotation incluiría

119 4.5. Detalles de diseño e implementación 97 Figura 4.8: Diagrama de clases del patrón Método Factoría original la interfaz necesaria para que desde una instancia de CodeVisitor se lance el proceso de aplicación de la optimización sin conocer el tipo concreto de la misma. La simplificación reside en la forma de decidir qué clase concreta se desea instanciar, ya que mientras que el patrón original recomienda un diseño basado en una clase abstracta que proporcione solamente la interfaz de creación para invocar a otra clase concreta que realmente realice la instanciación, la versión simplificada prescinde de dicha jerarquía de clases, dejando una única clase concreta encargada de decidir qué optimización instanciar. El listado 4.12 muestra la implementación de dicha clase. 1 class AnnotationCreator { 2 public : 3 virtual Annotation * CreateAnnotation ( Stmt * Node ){ 4 string pragmafunction = << nombre func. vacía >> 5 if( pragmafunction. compare (" pragma_oclopts_unroll ") ==0) 6 return new UnrollingAnnotation ( Node ); 7 return NULL ; 8 } 9 }; Listado 4.12: Implementación de la clase AnnotationCreator Estos detalles de diseño pueden verse en la figura 4.9 (extracto del diagrama de clases de la herramienta, ver figura 4.4 del apartado 4.3), que contiene las partes antes descritas, así como una muestra de cómo se insertarían en el diseño optimizaciones

120 98 4. La herramienta OCLOptimizer adicionales a la implementada por la clase UnrollingAnnotation, la cual será descrita en el próximo apartado. Figura 4.9: Detalle del diagrama de clases sobre el modelado de las optimizaciones

121 Capítulo 5 Resultados experimentales Una vez vista la implementación de la herramienta OCLOptimizer se procede a estudiar el impacto que tiene la optimización de desenrollamiento de bucles incluida en la misma en el rendimiento de diversos códigos tanto sobre CPU como GPU. Se han planteado diversas pruebas tomando como referencia dos problemas diferentes: por una parte, un problema sintético como es la multiplicación de una matriz cuadrada por un vector (sección 5.1) y, por otra, un problema frecuente y con mayor carga computacional como es la convolución de imágenes (sección 5.2). Al final del capítulo, en la sección 5.3, se incluye un pequeño comentario a modo de resumen de los resultados obtenidos en estas pruebas. Los recursos de computación que han utilizados para la realización de estas pruebas son los siguientes: Plataforma CPU-XEON: Esta plataforma ha sido utilizada para las pruebas de ejecución sobre CPU. Está compuesta por el nodo compute-5-0 del cluster pluton de Grupo de Arquitectura de Computadores. Este nodo cuenta con un procesador hexa-core (6 núcleos físicos, 12 hilos lógicos) Intel Xeon X5650 a 2,67 Ghz, 12 MB de caché L4 y 12 GB de memoria RAM. La versión disponible de OpenCL en esta plataforma es la 1.1, a través de la versión 2.4 del SDK de AMD APP. 99

122 Resultados experimentales Plataforma GPU-TESLA: Esta plataforma ha sido utilizada para las pruebas de ejecución sobre GPU. Está formada por el sistema NVIDIA Tesla S2050, compuesto por 4 GPUs Fermi de 448 núcleos e instalado en el mismo cluster. La implementación disponible en esta plataforma es proporcionada por NVIDIA para la versión 1.0 del estándar. Puesto que esta primera implementación de OCLOptimizer no contempla la optimización de códigos en entornos multi-gpu, solamente se ha utilizado en estas pruebas una de las 4 GPUs disponibles Producto matriz-vector Para la realización de estas pruebas se ha preparado un código de kernel que realiza una multiplicación matriz cuadrada-vector. Las versiones de dicho kernel generadas por la herramienta han sido ejecutadas en la plataforma GPU-TESLA para matrices de tamaños , y Una vez que la herramienta ha obtenido una versión óptima sobre una plataforma dada para el kernel de entrada anotado por el usuario, es posible ordenarle al compilador de OpenCL que intente aplicar optimizaciones adicionales. Con la realización de estas pruebas se pretende analizar la influencia que estas optimizaciones adicionales pueden tener en el rendimiento de los kernels óptimos generados por OCLOptimizer, de modo que sea posible cuantificar qué parte de la mejora es atribuible a la herramienta y cuál al compilador de OpenCL. La figura 5.1 muestra los resultados de estas pruebas. Figura 5.1: Aceleraciones obtenidas en GPU para multiplicaciones matriz-vector

123 5.1. Producto matriz-vector 101 Como puede verse, la aceleración se mantiene estable en un 75 % en aquellas versiones que solamente han sido optimizadas con OCLOptimizer. En cambio, aquellas que además han sido optimizadas por el compilador de OpenCL obtienen unos picos de aceleración muy superiores, que rondan el 200 % respecto de la versión original. La tabla 5.1 muestra los tiempos de ejecución de la versión original del kernel para los diferentes tamaños probados. Nótese que en cada caso se ha utilizado como referencia la versión original del kernel sin optimizar u optimizada con el compilador de OpenCL, respectivamente. N Original Original + Opt. compilador ,4433 ms 3,85926 ms ,2959 ms 15,1982 ms ,568 ms 34,4499 ms Tabla 5.1: Tiempos de referencia de la multiplicación matriz-vector en GPU Así mismo, también resulta interesante estudiar cómo influye el factor de desenrollamiento en la aceleración obtenida para las diferentes versiones generadas. La figura 5.2 muestra la evolución de la aceleración conseguida en la plataforma GPU-TESLA en función del factor de desenrollamiento aplicado en las versiones generadas para un producto matriz-vector de tamaño En este caso, en color rojo se representan las aceleraciones de las versiones optimizadas solamente con OCLOptimizer, mientras que en azul se representan las aceleraciones de las versiones que además han sido optimizadas por el compilador de OpenCL. Puede observarse cómo en el primer caso el máximo aumento de rendimiento (sobre un 75 %) se alcanza aproximadamente en un factor 32 y se mantiene estable a partir de ahí. En el caso de haber compilado con el optimizador de código activado, puede verse cómo el incremento que experimenta el rendimiento en función de dicho factor es muy superior, alcanzándose un máximo próximo al 200 % para un factor de 22 y estabilizándose a partir de 36 entre el 150 % y el 175 %. Esto indica que la elección del factor óptimo de desenrollamiento no es trivial. Los tiempos de referencia del kernel original utilizados para el cálculo de estas aceleraciones son los citados en la tabla 5.1 para una matriz de tamaño

124 Resultados experimentales Figura 5.2: Aceleraciones en GPU para una multiplicación matriz-vector Este importante incremento del rendimiento en función del uso o no del optimizador automático del compilador de OpenCL que se puede apreciar en la figura 5.1 probablemente sea debido a que el desenrollamiento descubre al optimizador del compilador muchas más opciones para intentar aplicar de forma agresiva técnicas como la vectorización o el reordenamiento de instrucciones. Otro factor importante en la ejecución de la herramienta es el tiempo total que ésta tarda en realizar el proceso completo de optimización iterativa. La tabla 5.2 muestra un desglose del tiempo de ejecución de la herramienta para el caso de prueba de la multiplicación matriz-vector con tamaños de experimento 2000, 4000 y Como puede verse, de los aproximadamente 15 minutos que dura la ejecución completa de la herramienta, el tiempo dedicado a la ejecución de las versiones generadas supone aproximadamente el 95 % del total. Los resultados mostrados en la tabla demuestran que el tiempo de ejecución de la herramienta está dominado por el tiempo de ejecución de las versiones a evaluar, lo que supone una importante limitación para su escalabilidad a la hora de tratar problemas grandes u optimizaciones en las que se genere un elevado número de versiones diferentes. Como solución a este problema se propone como línea de trabajo futuro la búsqueda de mecanismos alternativos de evaluación utilizando heurísticas o modelos de rendimiento para diferentes plataformas.

125 5.2. Convolución de imágenes 103 Fase Preprocesado Generación de kernels Generación de hosts Compilación Ejecución Evaluación TOTAL TOTAL (minutos) Tiempo (ms) 2 ms 32 ms 128 ms ms ms 1 ms ms 15 minutos Tabla 5.2: Desglose de tiempos de OCLOptimizer para multiplicaciones matriz-vector 5.2. Convolución de imágenes Una vez realizada una primera fase de pruebas con un código sintético como es la multiplicación matriz-vector, se procede a probar el funcionamiento de OCLOptimizer con un problema de mayor carga y complejidad computacional, como es el caso de la convolución de imágenes. Además, éste es un problema bastante popular y que resulta muy adecuado para su implementación con OpenCL. Para la realización de estas pruebas se ha tomado como referencia un ejemplo de implementación de una convolución en OpenCL [24] disponible en AMD Developer Central, portal de soporte para desarrolladores de AMD. La convolución es una operación fundamental en teoría de señales y consiste en la combinación de dos señales para producir una tercera de modo que se toma una señal de entrada, la cual es convolucionada con una máscara o filtro para obtener el resultado final. En el listado 5.1 se muestra el pseudocódigo de la operación para una imagen (matriz bidimensional), que consiste en ir desplazando la máscara por toda la imagen de entrada para ir calculando, para cada punto de la imagen en que se centre la máscara, la suma de los productos de cada píxel de la máscara por el píxel correspondiente de la entrada. La figura 5.3 muestra un ejemplo de convolución para una imagen de entrada de tamaño 8 8 y una máscara de tamaño 3 3.

126 Resultados experimentales 1 for y = 0 to ImageHeight do 2 for x = 0 to ImageWidth do 3 sum = 0 4 for i = -h to h do 5 for j = -w to w do 6 sum = sum + input (j,i) * filter (x-j,y-i) 7 end for 8 end for 9 output (x, y) = sum 10 end for 11 end for Listado 5.1: Pseudocódigo del algoritmo de convolución Figura 5.3: Ejemplo de convolución para una imagen de entrada 8 8 y una máscara 3 3 Para implementar la convolución en OpenCL se define un espacio de trabajo de igual tamaño y dimensiones que la imagen, de modo que cada work-item estará asociado a un píxel concreto y se encargará de calcular la suma de los productos de los valores de los píxeles de la máscara por los valores de los píxeles de la imagen que se encuentran en su zona de influencia de acuerdo con el tamaño de la máscara. A la vista del comportamiento del optimizador automático de código del compilador de OpenCL deducido de los resultados obtenidos para la multiplicación matriz-vector en GPU-TESLA, se han realizado pruebas de convolución de imágenes tanto en la propia

127 5.2. Convolución de imágenes 105 plataforma GPU-TESLA como en la plataforma CPU-XEON para comprobar si dicho comportamiento se mantiene tanto para problemas con mayor carga computacional (como es el caso de la convolución) como para plataformas diferentes. En concreto se han realizado pruebas de convolución de imágenes para tamaños de salida , , y , tanto en GPU-TESLA como en CPU-XEON. En todas ellas se ha fijado un tamaño de filtro de y se ha aplicado un rango de factores de desenrollamiento entre 2 y 16 con paso 2 (es decir, se ha utilizado la directiva #pragma oclopts unroll ). En primer lugar se han probado sobre la plataforma GPU-TESLA las versiones generadas por OCLOptimizer para la convolución de imágenes de diferentes tamaños. La figura 5.4 compara las aceleraciones obtenidas por las versiones que solamente han sido optimizadas utilizando la herramienta OCLOptimizer con aquellas obtenidas aplicando adicionalmente optimizaciones por parte del compilador de OpenCL. Como puede verse, la aceleración se mantiene estable en torno al 25 % para las versiones optimizadas sólo con OCLOptimizer, mientras que las que también fueron optimizadas por el compilador alcanzan aceleraciones de entre el 60 % y el 90 %. La tabla 5.3 muestra los tiempos con los que han sido calculadas estas aceleraciones. Figura 5.4: Aceleraciones obtenidas para la convolución de imágenes en GPU Como ya se ha comentado en la sección dedicada al caso de prueba de la multiplicación matriz-vector, otro aspecto cuyo estudio puede resultar interesante es la influencia

128 Resultados experimentales N Original Original + Opt. compilador ,988 ms 27,1216 ms ,67 ms 95,5092 ms ,59 ms 381,441 ms ,2 ms 1529,12 ms Tabla 5.3: Tiempos de referencia de la convolución de imágenes en GPU del factor de desenrollamiento en la aceleración obtenida para las diferentes versiones generadas. La figura 5.5 muestra la evolución de la aceleración alcanzada en GPU-TESLA en función del factor de desenrollamiento aplicado en las versiones generadas para la convolución de imágenes de tamaño Siguiendo el mismo criterio que en la figura 5.2, en rojo se representan las aceleraciones de las versiones optimizadas solamente con OCLOptimizer, mientras que en azul se representan las aceleraciones de las versiones que además han sido optimizadas por el compilador de OpenCL. En este caso las aceleraciones obtenidas por las versiones optimizadas solamente con la herramienta apenas alcanzan el 25 % antes comentado, mientras que en las que también han sido optimizadas por el compilador se roza el 100 %. Así mismo, también puede observarse cómo la aceleración mantiene una evolución ascendente para los factores 2, 4, 8 y 16 (para estos factores el bucle adicional del desenrollamiento solamente computa 1 iteración), mientras que para 6, 10, 12 y 14 se producen descensos en el rendimiento debido a que, para estos factores, el número de iteraciones no afectadas por el desenrollamiento es superior. Los tiempos de referencia del kernel original utilizados para el cálculo de estas aceleraciones son los citados en la tabla 5.3 para un tamaño Los experimentos realizados para la convolución de imágenes en GPU-TESLA parecen corroborar el impacto positivo que tiene el optimizador automático del compilador de OpenCL sobre el rendimiento de las versiones generadas por OCLOptimizer para GPUs. A continuación se repiten los mismos experimentos de convolución de imágenes sobre CPU-XEON con el objeto de comprobar si este comportamiento también se ve reflejado en las versiones generadas por la herramienta para una plataforma completamente diferente como es una CPU.

129 5.2. Convolución de imágenes 107 Figura 5.5: Aceleraciones obtenidas por la herramienta para una convolución de una imagen de tamaño en GPU con factores de desenrollamiento entre 2 y 16 La figura 5.6 muestra las aceleraciones obtenidas en CPU-XEON por las versiones de la convolución de imágenes generadas por OCLOptimizer. En este caso, las versiones que solamente han sido optimizadas por la herramienta obtienen una mejora de rendimiento ligeramente inferior a la de sus homólogas para GPU, con solamente un 10 % de aceleración respecto del kernel original. Sin embargo, aquellas que además fueron optimizadas por el compilador no suponen mejora alguna. La tabla 5.4 muestra los tiempos de referencia del kernel original utilizados para el cálculo de estas aceleraciones. Figura 5.6: Aceleraciones obtenidas para la convolución de imágenes en CPU

Clasificación de las Herramientas CASE

Clasificación de las Herramientas CASE Qué es una herramienta CASE? Las herramientas CASE (Computer Aided Software Engineering, Ingeniería de Software Asistida por Computadora) son diversas aplicaciones informáticas destinadas a aumentar la

Más detalles

Computación de Propósito General en Unidades de Procesamiento Gráfico GPGPU. Clase 0 Lanzamiento del Curso. Motivación

Computación de Propósito General en Unidades de Procesamiento Gráfico GPGPU. Clase 0 Lanzamiento del Curso. Motivación Computación de Propósito General en Unidades de Procesamiento Gráfico () Pablo Ezzatti, Martín Pedemonte Clase 0 Lanzamiento del Curso Contenido Evolución histórica en Fing Infraestructura disponible en

Más detalles

Sistemas Operativos. Introducción. Tema 6

Sistemas Operativos. Introducción. Tema 6 Sistemas Operativos Introducción Qué es un sistema operativo? Ubicación de un sistema operativo en un computador Descripción de un sistema operativo: Funcional Estructural Realización Funciones de los

Más detalles

CÓMPUTO DE ALTO RENDIMIENTO EN MEMORIA COMPARTIDA Y PROCESADORES GRÁFICOS

CÓMPUTO DE ALTO RENDIMIENTO EN MEMORIA COMPARTIDA Y PROCESADORES GRÁFICOS CÓMPUTO DE ALTO RENDIMIENTO EN MEMORIA COMPARTIDA Y PROCESADORES GRÁFICOS Leopoldo N. Gaxiola, Juan J. Tapia Centro de Investigación y Desarrollo de Tecnología Digital Instituto Politécnico Nacional Avenida

Más detalles

TAREA 1. INTRODUCCIÓN A LOS SISTEMAS OPERATIVOS.

TAREA 1. INTRODUCCIÓN A LOS SISTEMAS OPERATIVOS. 1 TAREA 1. INTRODUCCIÓN A LOS SISTEMAS OPERATIVOS. 1- Cuáles son las principales funciones de un sistema operativo? Los Sistemas Operativos tienen como objetivos o funciones principales lo siguiente; Comodidad;

Más detalles

Intel lanza su procesador Caballero Medieval habilitado para Inteligencia Artificial

Intel lanza su procesador Caballero Medieval habilitado para Inteligencia Artificial Intel lanza su procesador Caballero Medieval habilitado para Inteligencia Artificial Intel ha lanzado su procesador Xeon Phi en la Conferencia Internacional de Supercomputación de Alemania. El procesador

Más detalles

Técnicas de Programación

Técnicas de Programación Técnicas de Programación 2.1.- Introducción: unos conceptos previos y primeros conceptos de la API Introducción La resolución de un problema con medios informáticos implica generalmente la siguiente secuencia

Más detalles

Arquitecturas GPU v. 2013

Arquitecturas GPU v. 2013 v. 2013 Stream Processing Similar al concepto de SIMD. Data stream procesado por kernel functions (pipelined) (no control) (local memory, no cache OJO). Data-centric model: adecuado para DSP o GPU (image,

Más detalles

Arquitecturas de Altas Prestaciones y Supercomputación

Arquitecturas de Altas Prestaciones y Supercomputación Arquitecturas de Altas Prestaciones y Supercomputación Presentación del itinerario Julio de 2014 Arquitecturas de Altas Prestaciones y Supercomputación Julio de 2014 1 / 15 Agenda Introducción 1 Introducción

Más detalles

Es un conjunto de palabras y símbolos que permiten al usuario generar comandos e instrucciones para que la computadora los ejecute.

Es un conjunto de palabras y símbolos que permiten al usuario generar comandos e instrucciones para que la computadora los ejecute. Los problemas que se plantean en la vida diaria suelen ser resueltos mediante el uso de la capacidad intelectual y la habilidad manual del ser humano. La utilización de la computadora en la resolución

Más detalles

Tema V Generación de Código

Tema V Generación de Código Tema V Generación de Código Una vez que se ha realizado la partición HW/SW y conocemos las operaciones que se van a implementar por hardware y software, debemos abordar el proceso de estas implementaciones.

Más detalles

ROGRAMA DE CURSO Código Nombre EL4102. Arquitectura de Computadores Nombre en Inglés Computer Organization SCT

ROGRAMA DE CURSO Código Nombre EL4102. Arquitectura de Computadores Nombre en Inglés Computer Organization SCT ROGRAMA DE CURSO Código Nombre EL4102 Arquitectura de Computadores Nombre en Inglés Computer Organization SCT Unidades Horas de Horas Docencia Horas de Trabajo Docentes Cátedra Auxiliar Personal 6 10 3

Más detalles

Computadora y Sistema Operativo

Computadora y Sistema Operativo Computadora y Sistema Operativo Según la RAE (Real Academia de la lengua española), una computadora es una máquina electrónica, analógica o digital, dotada de una memoria de gran capacidad y de métodos

Más detalles

Sistema electrónico digital (binario) que procesa datos siguiendo unas instrucciones almacenadas en su memoria

Sistema electrónico digital (binario) que procesa datos siguiendo unas instrucciones almacenadas en su memoria 1.2. Jerarquía de niveles de un computador Qué es un computador? Sistema electrónico digital (binario) que procesa datos siguiendo unas instrucciones almacenadas en su memoria Es un sistema tan complejo

Más detalles

Tema 2 Introducción a la Programación en C.

Tema 2 Introducción a la Programación en C. Tema 2 Introducción a la Programación en C. Contenidos 1. Conceptos Básicos 1.1 Definiciones. 1.2 El Proceso de Desarrollo de Software. 2. Lenguajes de Programación. 2.1 Definición y Tipos de Lenguajes

Más detalles

Un sistema de bases de datos sirve para integrar los datos. Lo componen los siguientes elementos:

Un sistema de bases de datos sirve para integrar los datos. Lo componen los siguientes elementos: Qué es una base de datos? El problema de los datos Todas las empresas requieren almacenar información. Desde siempre lo han hecho. La información puede ser de todo tipo. Cada elemento informativo (nombre,

Más detalles

Introducción a la programación: Contenido. Introducción

Introducción a la programación: Contenido. Introducción Introducción a la programación: Contenido Introducción a la programación:... 1 Introducción... 1 1. Procesamiento automatizado de información... 1 2. Concepto de algoritmo.... 2 3. Lenguajes de programación....

Más detalles

Qué es un programa informático?

Qué es un programa informático? Qué es un programa informático? Un programa informático es una serie de comandos ejecutados por el equipo. Sin embargo, el equipo sólo es capaz de procesar elementos binarios, es decir, una serie de 0s

Más detalles

Fundamentos de Programación. Sabino Miranda-Jiménez

Fundamentos de Programación. Sabino Miranda-Jiménez Fundamentos de Programación Sabino Miranda-Jiménez MÓDULO 1. Introducción a la computación Temas: La computación en el profesional de ingeniería Desarrollo computacional en la sociedad Aplicaciones Software

Más detalles

Diseño y simulación de un algoritmo basado en lógica difusa para el despacho de ascensores en un edificio

Diseño y simulación de un algoritmo basado en lógica difusa para el despacho de ascensores en un edificio Diseño y simulación de un algoritmo basado en lógica difusa para el despacho de ascensores en un edificio PROYECTO FIN DE CARRERA Ingeniería de Telecomunicación Autor: Guillermo Iglesias García Director:

Más detalles

1.2.-Analisis de los componentes

1.2.-Analisis de los componentes 1.2.-Analisis de los componentes 1.2.1.-CPU La Unidad Central de Proceso (conocida por sus siglas en inglés, CPU). Es el lugar donde se realizan las operaciones de cálculo y control de los componentes

Más detalles

Tema 2 Conceptos básicos de programación. Fundamentos de Informática

Tema 2 Conceptos básicos de programación. Fundamentos de Informática Tema 2 Conceptos básicos de programación Fundamentos de Informática Índice Metodología de la programación Programación estructurada 2 Pasos a seguir para el desarrollo de un programa (fases): Análisis

Más detalles

Sistema de Gestión de Aplicaciones Implementadas en FPGAs

Sistema de Gestión de Aplicaciones Implementadas en FPGAs Sistema de Gestión de Aplicaciones Implementadas en FPGAs Ledo Bañobre, R. 1, Losada Sampayo, A. 1, Álvarez Ruiz de Ojeda, J. 1 1 Departamento de Tecnología Electrónica, Escuela Técnica Superior de Ingenieros

Más detalles

INFORMÁTICA 4º ESO. Qué es un Sistema Operativo (O.S.)?

INFORMÁTICA 4º ESO. Qué es un Sistema Operativo (O.S.)? UD.1 1 Qué es un Sistema Operativo (O.S.)? Definición Instalación Ejecución Funcionamiento de un S.I. sin Sistema Operativo UD.1 2 Estructura de un Sistema Operativo Núcleo (kernel) CPU Administrador de

Más detalles

Optimización de Rutinas Multinivel de Álgebra Lineal en Sistemas Multicore

Optimización de Rutinas Multinivel de Álgebra Lineal en Sistemas Multicore Máster en Nuevas Tecnologías en Informática Facultad de Informática Universidad de Murcia Optimización de Rutinas Multinivel de Álgebra Lineal en Sistemas Multicore Autor: Jesús Cámara Moreno Directores:

Más detalles

ARQUITECTURA BÁSICA DEL ORDENADOR: Hardware y Software. IES Miguel de Cervantes de Sevilla

ARQUITECTURA BÁSICA DEL ORDENADOR: Hardware y Software. IES Miguel de Cervantes de Sevilla ARQUITECTURA BÁSICA DEL ORDENADOR: Hardware y Software. IES Miguel de Cervantes de Sevilla Índice de contenido 1.- Qué es un ordenador?...3 2.-Hardware básico de un ordenador:...3 3.-Software...4 3.1.-Software

Más detalles

Programación Orientada a Objetos

Programación Orientada a Objetos Programación Orientada a Objetos PROGRAMACIÓN ORIENTADA A OBJETOS 1 Sesión No. 8 Nombre: El Modelo de diseño con UML Contextualización Los modelos que podemos crear con UML son varios, por lo que debemos

Más detalles

Métodos para escribir algoritmos: Diagramas de Flujo y pseudocódigo

Métodos para escribir algoritmos: Diagramas de Flujo y pseudocódigo TEMA 2: CONCEPTOS BÁSICOS DE ALGORÍTMICA 1. Definición de Algoritmo 1.1. Propiedades de los Algoritmos 2. Qué es un Programa? 2.1. Cómo se construye un Programa 3. Definición y uso de herramientas para

Más detalles

Motherboard. Daniel Rúa Madrid

Motherboard. Daniel Rúa Madrid Motherboard Daniel Rúa Madrid Qué es? La Motherboard es la placa principal de circuitos impresos y contiene los buses, que permiten que los datos sean transportados entre los diferentes componentes de

Más detalles

Sistemas Electrónicos Especialidad del Grado de Ingeniería de Tecnologías de Telecomunicación

Sistemas Electrónicos Especialidad del Grado de Ingeniería de Tecnologías de Telecomunicación Especialidad del Grado de Ingeniería de Tecnologías de Telecomunicación Charlas Informativas sobre las Especialidades de los Grados E.T.S.I.I.T. Jesús Banqueri Ozáez Departamento de Electrónica y Tecnología

Más detalles

Contenido. Prefacio Orígenes de la programación orientada a objetos... 1

Contenido. Prefacio Orígenes de la programación orientada a objetos... 1 Prefacio... xv 1. Orígenes de la programación orientada a objetos... 1 1.1 La crisis del software... 1 1.2 Evolución del software... 3 1.3 Introducción a la programación orientada a procedimientos... 4

Más detalles

Un importante problema para sistemas de la nueva generación

Un importante problema para sistemas de la nueva generación Un importante problema para sistemas de la nueva generación J. A. Stankovic, Misconceptions about Real-Time Computing: A Serious Problem for Next Generation Systems, IEEE Computer, October 1988. Manifestar

Más detalles

Introducción a la Computación. Herramientas Informáticas. Omar Ernesto Cabrera Rosero Universidad de Nariño

Introducción a la Computación. Herramientas Informáticas. Omar Ernesto Cabrera Rosero Universidad de Nariño Introducción a la Computación Omar Ernesto Cabrera Rosero Universidad de Nariño 6 de Julio 2010 Esquema Terminología Informática 1 Terminología Informática Computación e Informática Dato e Información

Más detalles

UNIDAD NO. 01 CONCEPTOS INFORMÁTICOS BÁSICOS

UNIDAD NO. 01 CONCEPTOS INFORMÁTICOS BÁSICOS UNIDAD NO. 01 CONCEPTOS INFORMÁTICOS BÁSICOS Objetivo general de la unidad: Explicar conceptos básicos computacionales partiendo del concepto general de sistema. 1.1 CONCEPTO DE DATO E INFORMACIÓN Dato:

Más detalles

Unidad VII Optimización. M.C. Juan Carlos Olivares Rojas

Unidad VII Optimización. M.C. Juan Carlos Olivares Rojas Unidad VII Optimización M.C. Juan Carlos Olivares Rojas Agenda 7.1 Tipos de optimización. 7.1.1 Locales. 7.1.2 Bucles. 7.1.3 Globales. 7.1.4 De mirilla. 7.2 Costos. 7.2.1 Costo de ejecución. 7.2.2 Criterios

Más detalles

CSC 2. SÍNTESIS Y REDACCIÓN FINAL DE LOS CRITERIOS DE EVALUACIÓN (CRITERIOS ORIGINALES Nº)(ESTÁNDARES Nº) 3. CCLAVE

CSC 2. SÍNTESIS Y REDACCIÓN FINAL DE LOS CRITERIOS DE EVALUACIÓN (CRITERIOS ORIGINALES Nº)(ESTÁNDARES Nº) 3. CCLAVE Sociedad de la información. Introducción histórica de la informática. Impacto de las Tecnologías de la Información y Comunicación (TIC) en los diversos ámbitos de la sociedad actual. Avances y riesgos.

Más detalles

Nociones básicas de computación paralela

Nociones básicas de computación paralela Nociones básicas de computación paralela Javier Cuenca 1, Domingo Giménez 2 1 Departamento de Ingeniería y Tecnología de Computadores Universidad de Murcia 2 Departamento de Informática y Sistemas Universidad

Más detalles

Al final, qué sabré hacer?... Itinerario del proceso de aprendizaje... Capítulo 1. Conceptos generales a modo de introducción (CG)

Al final, qué sabré hacer?... Itinerario del proceso de aprendizaje... Capítulo 1. Conceptos generales a modo de introducción (CG) Contenido presentación... Al final, qué sabré hacer?... Itinerario del proceso de aprendizaje... xvii xxiii xxv Capítulo 1. Conceptos generales a modo de introducción (CG) OBJETIVO DIDÁCTICO... 1 1.1.

Más detalles

HARDWARE: DISPOSITIVOS DE ENTRADA, PROCESAMIENTO Y SALIDA/ SOFTWARE: SOFTWARE DE SISTEMAS DE APLICACIONES. Ralph Stair y George Reynolds

HARDWARE: DISPOSITIVOS DE ENTRADA, PROCESAMIENTO Y SALIDA/ SOFTWARE: SOFTWARE DE SISTEMAS DE APLICACIONES. Ralph Stair y George Reynolds HARDWARE: DISPOSITIVOS DE ENTRADA, PROCESAMIENTO Y SALIDA/ SOFTWARE: SOFTWARE DE SISTEMAS DE APLICACIONES Ralph Stair y George Reynolds Hardware: dispositivos de entrada, procesamiento y salida En este

Más detalles

Modelos de Programación Paralela Prof. Gilberto Díaz

Modelos de Programación Paralela Prof. Gilberto Díaz Universisdad de Los Andes Facultad de Ingeniería Escuela de Sistemas Modelos de Programación Paralela Prof. Gilberto Díaz gilberto@ula.ve Departamento de Computación, Escuela de Sistemas, Facultad de Ingeniería

Más detalles

FUNCIONAMIENTO DEL ORDENADOR

FUNCIONAMIENTO DEL ORDENADOR FUNCIONAMIENTO DEL ORDENADOR COMPUTACIÓN E INFORMÁTICA Datos de entrada Dispositivos de Entrada ORDENADOR PROGRAMA Datos de salida Dispositivos de Salida LOS ORDENADORES FUNCIONAN CON PROGRAMAS Los ordenadores

Más detalles

Dispositivos Digitales. EL-611 Complemento de Diseño Lógico y. Dispositivos Digitales

Dispositivos Digitales. EL-611 Complemento de Diseño Lógico y. Dispositivos Digitales EL-611 Complemento de Diseño Lógico y Objetivos y Evaluación Segundo Curso de Sistemas Digitales Complementar Materia Enfoque Diseños de Mayor Envergadura 1 Control + Examen y 6 Ejercicios (aprox.) Tareas

Más detalles

METRICA VERSION MÉTRICA versión 3. Metodología de Planificación, Desarrollo y Mantenimiento de Sistemas de Información

METRICA VERSION MÉTRICA versión 3. Metodología de Planificación, Desarrollo y Mantenimiento de Sistemas de Información 9.000 MÉTRICA versión 3 Metodología de Planificación, Desarrollo y Mantenimiento de Sistemas de Información 9.010 Enero 2000 borrador de metodología MÉTRICA v. 3 Ofrece a las organizaciones un instrumento

Más detalles

TEMARIO DE PROFESORES TÉCNICOS DE F.P. : SISTEMAS Y APLICACIONES INFORMÁTICAS. Octubre 1997 (Publicado en el B.O.E. de 13 de Febrero de 1.

TEMARIO DE PROFESORES TÉCNICOS DE F.P. : SISTEMAS Y APLICACIONES INFORMÁTICAS. Octubre 1997 (Publicado en el B.O.E. de 13 de Febrero de 1. TEMARIO DE PROFESORES TÉCNICOS DE F.P. : SISTEMAS Y APLICACIONES INFORMÁTICAS. Octubre 1997 (Publicado en el B.O.E. de 13 de Febrero de 1.996) SISTEMAS Y APLICACIONES INFORMÁTICAS 1. Representación y comunicación

Más detalles

Unidad 2: Taller de Cómputo. Estructura y Componentes de la Computadora UNIDAD DOS: INTRODUCCIÓN

Unidad 2: Taller de Cómputo. Estructura y Componentes de la Computadora UNIDAD DOS: INTRODUCCIÓN UNIDAD DOS: INTRODUCCIÓN Una computadora es una máquina electrónica diseñada para manipular y procesar información de acuerdo a un conjunto de ordenes o programas. para que esto sea posible se requiere

Más detalles

PROGRAMA: COMPUTACION I

PROGRAMA: COMPUTACION I UNIVERSIDAD NACIONAL EXPERIMENTAL DEL TACHIRA VICERECTORADO ACADÉMICO DECANATO DE DOCENCIA DEPARTAMENTO DE INGENIERÍA INFORMÁTICA 1 PROGRAMA: COMPUTACION I Código 0415102T Carrera: Ingeniería Informática

Más detalles

ESTRUCTURA BÁSICA DE UN ORDENADOR

ESTRUCTURA BÁSICA DE UN ORDENADOR ESTRUCTURA BÁSICA DE UN ORDENADOR QUÉ ES UN ORDENADOR? Un ordenador es una máquina... QUÉ ES UN ORDENADOR? Un ordenador es una máquina... QUÉ ES UN ORDENADOR? Un ordenador es una máquina... Qué son los

Más detalles

INFORMATICA III. Capítulo I: Plataformas

INFORMATICA III. Capítulo I: Plataformas INFORMATICA III Capítulo I: Plataformas Plataformas Hardware Modelos de sistemas Sistemas operativos Herramientas de desarrollo Informática III Pág. 2 Plataformas Hardware Modelos de sistemas Sistemas

Más detalles

4.1. Circuitos Digitales Configurables

4.1. Circuitos Digitales Configurables 4.1. Circuitos Digitales Configurables Los circuitos digitales configurable son sistemas electrónicos digitales cuya función se puede modificar utilizando solamente una parte de los elementos que los componen

Más detalles

CAPITULO III CONTROLADORES

CAPITULO III CONTROLADORES CAPITULO III CONTROLADORES 3.1 Controladores El controlador es el segundo elemento en un sistema de control automático, éste toma una señal de entrada y la compara con un valor establecido para obtener

Más detalles

Programación (PRG) PRÁCTICA 10. Algoritmos de búsqueda

Programación (PRG) PRÁCTICA 10. Algoritmos de búsqueda Programación (PRG) Facultad de Informática Departamento de Sistemas Informáticos y Computación Universidad Politécnica de Valencia 1. Introducción El objetivo de esta práctica es estudiar el comportamiento

Más detalles

Evolución del software y su situación actual

Evolución del software y su situación actual Evolución del software y su situación actual El software es el conjunto de programas que permite emplear la PC, es decir, es el medio de comunicación con la computadora, el control de sus funciones y su

Más detalles

M. C. Felipe Santiago Espinosa

M. C. Felipe Santiago Espinosa M. C. Felipe Santiago Espinosa Junio de 2008 Un sistema empotrado es un procesador, con sus elementos externos que desarrolla una función especifica de manera autónoma. Un sistema empotrado es un sistema

Más detalles

Recopilación presentada por 1

Recopilación presentada por 1 Aula Aula de de Informática Informática del del Centro Centro de de Participación Participación Activa Activa para para Personas Personas Mayores Mayores de de El El Ejido Ejido (Almería). (Almería). Consejería

Más detalles

Fundamentos de Informática 3. Construcción de Software

Fundamentos de Informática 3. Construcción de Software 2 Contenidos Fundamentos de Informática 3. Construcción de Software - Introducción - - - Diseño -Algoritmos -Diagramas de Flujo -Pseudocódigos - Codificación - Pruebas - Mantenimiento Fundamentos de Informática

Más detalles

Programación Concurrente y Paralela. Unidad 1 Introducción

Programación Concurrente y Paralela. Unidad 1 Introducción Programación Concurrente y Paralela Unidad 1 Introducción Contenido 1.1 Concepto de Concurrencia 1.2 Exclusión Mutua y Sincronización 1.3 Corrección en Sistemas Concurrentes 1.4 Consideraciones sobre el

Más detalles

5.3.3 FICHA DE LA MATERIA SISTEMAS OPERATIVOS, SISTEMAS DISTRIBUIDOS Y REDES

5.3.3 FICHA DE LA MATERIA SISTEMAS OPERATIVOS, SISTEMAS DISTRIBUIDOS Y REDES 5.3.3 FICHA DE LA MATERIA SISTEMAS OPERATIVOS, SISTEMAS DISTRIBUIDOS Y REDES DENOMINACIÓN DE LA MATERIA SISTEMAS OPERATIVOS, SISTEMAS DISTRIBUIDOS Y REDES MÓDULO AL QUE PERTENECE CRÉDITOS ECTS 30 CARÁCTER

Más detalles

DISEÑO DE SISTEMAS ELECTRÓNICOS DIGITALES BASADOS EN EL PROCESADOR TMS320C3X DE TEXAS INSTRUMENTS. UNA VISIÓN PRÁCTICA.

DISEÑO DE SISTEMAS ELECTRÓNICOS DIGITALES BASADOS EN EL PROCESADOR TMS320C3X DE TEXAS INSTRUMENTS. UNA VISIÓN PRÁCTICA. DISEÑO DE SISTEMAS ELECTRÓNICOS DIGITALES BASADOS EN EL PROCESADOR TMS320C3X DE TEXAS INSTRUMENTS. UNA VISIÓN PRÁCTICA. Sergio Gallardo, Javier Lillo, Sergio Toral, Federico Barrero Universidad de Sevilla.

Más detalles

2.1 METODOLOGÍA PARA LA SOLUCIÓN DE PROBLEMAS

2.1 METODOLOGÍA PARA LA SOLUCIÓN DE PROBLEMAS 2.1 METODOLOGÍA PARA LA SOLUCIÓN DE PROBLEMAS El proceso de resolución de un problema con una computadora conduce a la escritura de un programa y su ejecución en la misma. Aunque el proceso de diseñar

Más detalles

Conceptos y definiciones básicos en computación

Conceptos y definiciones básicos en computación UNIVERSIDAD MICHOACANA DE SAN NICOLÁS DE HIDALGO FACULTAD DE INGENIERIA ELECTRICA Laboratorio de Herramientas Computacionales Conceptos y definiciones básicos en computación M.I. Rosalía Mora Lab. Juárez

Más detalles

Selección del Hardware y Software Administración del proceso de desarrollo de Sistemas de Información.

Selección del Hardware y Software Administración del proceso de desarrollo de Sistemas de Información. Administración del proceso de desarrollo de Sistemas de Información. Determinación de las necesidades de hardware y software. Existencia de equipo en la organización. Proceso de estimación de las cargas

Más detalles

2. Codificar de forma sistemática la secuencia de instrucciones en un lenguaje.

2. Codificar de forma sistemática la secuencia de instrucciones en un lenguaje. Modulo 1. Introducción a los lenguajes de programación La solución de problemas mediante en uso de un computador nos lleva a desarrollar programas o aplicaciones, la construcción de estos programas debe

Más detalles

Agradecimientos. Nota de los autores. 1 Problemas, algoritmos y programas 1

Agradecimientos. Nota de los autores. 1 Problemas, algoritmos y programas 1 Prologo Agradecimientos Nota de los autores Índice general I III V VII 1 Problemas, algoritmos y programas 1 1.1 Programas y la actividad de la programación.................... 4 1.2 Lenguajes y modelos

Más detalles

La última versión disponible cuando se redactó este manual era la 5 Beta (versión ), y sobre ella versa este manual.

La última versión disponible cuando se redactó este manual era la 5 Beta (versión ), y sobre ella versa este manual. Manual de Dev-C++ 4.9.9.2 Página 1 de 11 Introducción Dev-C++ es un IDE (entorno de desarrollo integrado) que facilita herramientas para la creación y depuración de programas en C y en C++. Además, la

Más detalles

MOMENTO I. BLOQUE 1. Opera las funciones básicas del sistema operativo y garantiza la seguridad de la información

MOMENTO I. BLOQUE 1. Opera las funciones básicas del sistema operativo y garantiza la seguridad de la información MOMENTO I. BLOQUE 1. Opera las funciones básicas del sistema operativo y garantiza la seguridad de la información Objetos de aprendizaje: Computadora LECTURA 1: La computadora La computadora Es una máquina

Más detalles

OpenDomo en Raspberry Pi

OpenDomo en Raspberry Pi David Sánchez Herrero Administración de Redes y Sistemas Operativos en Entornos de Software Libre OpenDomo es un sistema embebido libre desarrollado por la empresa OpenDomo Services S.L., basado en GNU/Linux,

Más detalles

Tile64 Many-Core. vs. Intel Xeon Multi-Core

Tile64 Many-Core. vs. Intel Xeon Multi-Core Tile64 Many-Core vs. Intel Xeon Multi-Core Comparación del Rendimiento en Bioinformática Myriam Kurtz Francisco J. Esteban Pilar Hernández Juan Antonio Caballero Antonio Guevara Gabriel Dorado Sergio Gálvez

Más detalles

Planificaciones Algoritmos y Programación I. Docente responsable: CARDOZO MARTIN MIGUEL. 1 de 8

Planificaciones Algoritmos y Programación I. Docente responsable: CARDOZO MARTIN MIGUEL. 1 de 8 Planificaciones 9511 - Algoritmos y Programación I Docente responsable: CARDOZO MARTIN MIGUEL 1 de 8 OBJETIVOS Capacitar al alumno en el diseño y programación documentados de algoritmos y en la elección

Más detalles

Monitorización continua las 24 Horas del día Capacidad de operar en redes de área extensa, a través de diferentes vías de comunicación

Monitorización continua las 24 Horas del día Capacidad de operar en redes de área extensa, a través de diferentes vías de comunicación 1.0 Introducción Hoy en día es difícil imaginar una actividad productiva sin el apoyo de un computador o de una máquina, en la actualidad estas herramientas no sólo están al servicio de intereses económicos,

Más detalles

COMPONENTES DEL PC LEONARDO OLIVARES VILLA MATEO CARDONA ARENAS

COMPONENTES DEL PC LEONARDO OLIVARES VILLA MATEO CARDONA ARENAS COMPONENTES DEL PC LEONARDO OLIVARES VILLA MATEO CARDONA ARENAS Tipos de procesadores. Dedicados: Para desarrollar una tarea muy especifica. Ejecutando un único algoritmo de forma óptima. de propósito

Más detalles

INDICE Prologo Capitulo 1. Algoritmos y programas Capitulo 2. La resolución de los problemas con computadoras y las herramientas de programación

INDICE Prologo Capitulo 1. Algoritmos y programas Capitulo 2. La resolución de los problemas con computadoras y las herramientas de programación INDICE Prologo XI Capitulo 1. Algoritmos y programas 1.1. Configuraciones de una computadora 1 1.2. Lenguajes de programación 2 1.3. Resolución de problemas 1.3.1. Fase de resolución del problema 3 1.3.1.1.

Más detalles

Bitbloq 2: Entorno de programación

Bitbloq 2: Entorno de programación 1.1.5. Bitbloq 2: Entorno de programación Bitbloq 1 es una herramienta online que permite crear programas para un microcontrolador y cargarlos en el mismo de forma sencilla y sin tener necesariamente conocimientos

Más detalles

Tema 2: Conceptos básicos. Escuela Politécnica Superior Ingeniería Informática Universidad Autónoma de Madrid

Tema 2: Conceptos básicos. Escuela Politécnica Superior Ingeniería Informática Universidad Autónoma de Madrid Tema 2: Conceptos básicos Ingeniería Informática Universidad Autónoma de Madrid 1 O B J E T I V O S Introducción a la Informática Adquirir una visión global sobre la Informática y sus aplicaciones. Conocer

Más detalles

Nombre de la asignatura: Programación Básica. Créditos: Objetivo de aprendizaje

Nombre de la asignatura: Programación Básica. Créditos: Objetivo de aprendizaje Nombre de la asignatura: Programación Básica Créditos: 2 4-6 Objetivo de aprendizaje Plantear metodológicamente la solución de problemas susceptibles de ser computarizados a través del manejo de técnicas

Más detalles

Potente rendimiento de doble núcleo para los negocios de hoy y de mañana

Potente rendimiento de doble núcleo para los negocios de hoy y de mañana Potente rendimiento de doble núcleo Potente rendimiento de doble núcleo para los negocios de hoy y de mañana Con la inigualable nueva gama de portátiles Toshiba para la empresa que incluyen el procesador

Más detalles

Tecnologías, Organización y Microarquitectura

Tecnologías, Organización y Microarquitectura Septiembre 2012 Tecnología de Integración Nanotecnología Tecnología de Integración Imágenes obtenidas con TEM (Transmission Electron Microscope) de una cepa del virus de la influenza, y de un transistor

Más detalles

El espectro de almacenamiento (Jerarquías de Memorias)

El espectro de almacenamiento (Jerarquías de Memorias) El espectro de almacenamiento (Jerarquías de Memorias) Las computadoras de hoy utilizan una variedad de tecnologías de almacenamiento. Cada tecnología está orientada hacia una función específica, con velocidades

Más detalles

Las optimizaciones pueden realizarse de diferentes formas. Las optimizaciones se realizan en base al alcance ofrecido por el compilador.

Las optimizaciones pueden realizarse de diferentes formas. Las optimizaciones se realizan en base al alcance ofrecido por el compilador. Unidad III: Optimización Las optimizaciones pueden realizarse de diferentes formas. Las optimizaciones se realizan en base al alcance ofrecido por el compilador. La optimización va a depender del lenguaje

Más detalles

1.1. Modelos de arquitecturas de cómputo: clásicas, segmentadas, de multiprocesamiento.

1.1. Modelos de arquitecturas de cómputo: clásicas, segmentadas, de multiprocesamiento. 1.1. Modelos de arquitecturas de cómputo: clásicas, segmentadas, de multiprocesamiento. Arquitecturas Clásicas. Estas arquitecturas se desarrollaron en las primeras computadoras electromecánicas y de tubos

Más detalles

FICHA PÚBLICA DEL PROYECTO

FICHA PÚBLICA DEL PROYECTO NUMERO DE PROYECTO: 218824 EMPRESA BENEFICIADA: MICROCALLI DEL GOLFO S.A DE C.V TÍTULO DEL PROYECTO: LÍNEA DE PRODUCTOS DE SOFTWARE PARA DOMÓTICA OBJETIVO DEL PROYECTO: Incorporar el paradigma de LPS como

Más detalles

Introducción a la programación

Introducción a la programación Introducción a la programación Conceptos Básicos El objetivo fundamental de éste curso es enseñar a resolver problemas mediante una computadora. El programador de computadoras es antes que nada una persona

Más detalles

Diseño de algoritmos paralelos

Diseño de algoritmos paralelos Diseño de algoritmos paralelos Curso 2011-2012 Esquema del capítulo Visión general de algunos algoritmos serie. Algoritmo paralelo vs. Formulación paralela Elementos de un Algoritmo paralelo Métodos de

Más detalles

Universidad Centroccidental Lisandro Alvarado. Decanato de Ciencias y Tecnología Departamento de Sistemas

Universidad Centroccidental Lisandro Alvarado. Decanato de Ciencias y Tecnología Departamento de Sistemas Universidad Centroccidental Lisandro Alvarado Decanato de Ciencias y Tecnología Departamento de Sistemas PROGRAMA INSTRUCCIONAL PROGRAMA: ANALISIS DE SISTEMAS DEPARTAMENTO: SISTEMAS ASIGNATURA: INTRODUCCIÓN

Más detalles

ORGANIZACIÓN DE COMPUTADORAS

ORGANIZACIÓN DE COMPUTADORAS Instituto Politécnico Superior Departamento Electrotecnia T ÉCNICO UNIVERSITARIO EN SISTEMAS ELECTRÓNICOS Introducción a la Computación ORGANIZACIÓN DE COMPUTADORAS ABEL LOBATO 2012 Introducción a la Computación

Más detalles

Tema 1: Arquitectura de ordenadores, hardware y software

Tema 1: Arquitectura de ordenadores, hardware y software Fundamentos de Informática Tema 1: Arquitectura de ordenadores, hardware y software 2010-11 Índice 1. Informática 2. Modelo de von Neumann 3. Sistemas operativos 2 1. Informática INFORMación automática

Más detalles

Especialidades en GII-TI

Especialidades en GII-TI Especialidades en GII-TI José Luis Ruiz Reina (coordinador) Escuela Técnica Superior de Ingeniería Informática Mayo 2014 Qué especialidades tiene la Ingeniería Informática? Según las asociaciones científicas

Más detalles

TPSC Cloud. Software de Gestión Participativa, Gestión de Riesgos y Cumplimiento

TPSC Cloud. Software de Gestión Participativa, Gestión de Riesgos y Cumplimiento TPSC Cloud Software de Gestión Participativa, Gestión de Riesgos y Cumplimiento Creemos que podemos hacer un importante aporte en el área de seguridad y calidad sanitaria. The Patient Safety Company The

Más detalles

Administración Informática. Unidad I. Tipos de sistemas y su clasificación A) Sistemas de información.

Administración Informática. Unidad I. Tipos de sistemas y su clasificación A) Sistemas de información. UNIVERSIDAD NACIONALDE INGENIERÁ UNI NORTE SEDE REGIONAL EN ETELI Ing. Mario Pastrana Moreno. Unidad I. Tipos de sistemas y su clasificación 10-09-2010 Administración Informática A) Sistemas de información.

Más detalles

Fase de Pruebas Introducción.

Fase de Pruebas Introducción. Fase de Pruebas Introducción. El desarrollo de sistemas de software implica una serie de actividades de producción en las que las posibilidades de que aparezca el fallo humano son enormes. Los errores

Más detalles

Introducción. Universidad Nacional Tecnológica del Cono Sur de Lima JORGE AUGUSTO MARTEL TORRES 1

Introducción. Universidad Nacional Tecnológica del Cono Sur de Lima JORGE AUGUSTO MARTEL TORRES 1 Universidad Nacional Tecnológica del Cono Sur de Lima Especialidad Ingeniería Mecánica Ingeniería Electrónica Introducción PROGRAMACIÓN DE INGENIERÍA Semana 01-A: Introducción Arquitectura Ing. Jorge A.

Más detalles

MICROPROCESADOR. Ing. Raúl Rojas Reátegui

MICROPROCESADOR. Ing. Raúl Rojas Reátegui MICROPROCESADOR Ing. Raúl Rojas Reátegui OBJETIVOS Al termino de la sesión el estudiante será capaz de: Describir las principales características de un Microprocesador. Describir las principales características

Más detalles

Fundamentos de programación JAVA

Fundamentos de programación JAVA Pág. N. 1 Fundamentos de programación JAVA Familia: Editorial: Autor: Computación e informática Macro Ricardo Walter Marcelo Villalobos ISBN: 978-612-304-238-7 N. de páginas: 296 Edición: 2. a 2014 Medida:

Más detalles

IFCD0111 Programación en Lenguajes Estructurados de Aplicaciones de Gestión

IFCD0111 Programación en Lenguajes Estructurados de Aplicaciones de Gestión IFCD0111 Programación en Lenguajes Estructurados de Aplicaciones de Gestión 1. MÓDULO 1. MF0223_3 SISTEMAS OPERATIVOS Y APLICACIONES INFORMÁTICAS UNIDAD FORMATIVA 1. UF1465 COMPUTADORES PARA BASES DE DATOS

Más detalles

TEORÍA DE AUTÓMATAS Y LENGUAJES FORMALES TRABAJO DE PRÁCTICAS. Convocatoria de junio de 2013

TEORÍA DE AUTÓMATAS Y LENGUAJES FORMALES TRABAJO DE PRÁCTICAS. Convocatoria de junio de 2013 TEORÍA DE AUTÓMATAS Y LENGUAJES FORMALES Ingeniería Técnica en Informática de Sistemas Segundo curso Departamento de Informática y Análisis Numérico Escuela Politécnica Superior Universidad de Córdoba

Más detalles

TEMA 4. PROCESO UNIFICADO

TEMA 4. PROCESO UNIFICADO TEMA 4. PROCESO UNIFICADO Definición El Proceso Unificado de Desarrollo Software es un marco de desarrollo de software que se caracteriza por estar dirigido por casos de uso, centrado en la arquitectura

Más detalles

Introducción a los sistemas operativos. Ing Esp Pedro Alberto Arias Quintero

Introducción a los sistemas operativos. Ing Esp Pedro Alberto Arias Quintero Introducción a los sistemas operativos Ing Esp Pedro Alberto Arias Quintero Unidad 1: Conceptos generales de Sistemas Operativos. Tema 1: Introducción: 1.1 Introducción: Qué es un sistema operativo?. 1.2

Más detalles

TEMA 1: Concepto de ordenador

TEMA 1: Concepto de ordenador TEMA 1: Concepto de ordenador 1.1 Introducción Los ordenadores necesitan para su funcionamiento programas. Sin un programa un ordenador es completamente inútil. Para escribir estos programas necesitamos

Más detalles

MAGMA. Matrix Algebra on GPU and Multicore Architecture. Ginés David Guerrero Hernández

MAGMA. Matrix Algebra on GPU and Multicore Architecture. Ginés David Guerrero Hernández PLASMA GPU MAGMA Rendimiento Trabajo Futuro MAGMA Matrix Algebra on GPU and Multicore Architecture Ginés David Guerrero Hernández gines.guerrero@ditec.um.es Grupo de Arquitecturas y Computación Paralela

Más detalles

Proyecto de Innovación y Mejora de la Calidad Docente. Convocatoria Nº de proyecto: 126

Proyecto de Innovación y Mejora de la Calidad Docente. Convocatoria Nº de proyecto: 126 Proyecto de Innovación y Mejora de la Calidad Docente Convocatoria 2015 Nº de proyecto: 126 Título del proyecto: Desarrollo de una aplicación (App) para plataformas móviles para mejorar la enseñanza/aprendizaje

Más detalles

Software definida radio: investigación y verificación de pruebas en una plataforma libre

Software definida radio: investigación y verificación de pruebas en una plataforma libre SOARES, Jaqueline Kennedy A. [1] SOARES, Jaqueline Kennedy A. Software defined radio: investigación y verificación de pruebas en una plataforma libre. Revista científica multidisciplinaria base de conocimiento.

Más detalles