Diseño, implementación y prueba de una librería para el tratamiento paralelo de un contenedor de datos

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

Download "Diseño, implementación y prueba de una librería para el tratamiento paralelo de un contenedor de datos"

Transcripción

1 UNIVERSIDADE DA CORUÑA FACULTADE DE INFORMÁTICA Departamento de Electrónica e Sistemas PROXECTO DE FIN DE CARREIRA DE ENXEÑERÍA INFORMÁTICA Diseño, implementación y prueba de una librería para el tratamiento paralelo de un contenedor de datos Autor: Director: Director: Álvaro de Vega Díaz Basilio B. Fraguela Rodríguez Diego Andrade Canosa A Coruña, Septiembre de 2010

2

3 Información general Título del proyecto: Diseño, implementación y prueba de una librería para el tratamiento paralelo de un contenedor de datos Clase de proyecto: Proyecto de investigación y desarrollo Nombre del autor: Álvaro de Vega Díaz Directores del proyecto: Basilio B. Fraguela Rodríguez Diego Andrade Canosa Miembros del tribunal: Miembros suplentes: Fecha de lectura: Calificación:

4

5 Autorización de entrega D. Basilio Bernardo Fraguela Rodríguez y D. Diego Andrade Canosa CERTIFICAN Que el proyecto titulado Diseño, implementación y prueba de una librería para el tratamiento paralelo de un contenedor de datos ha sido realizado por Álvaro de Vega Díaz, con D.N.I N, bajo la dirección de D. Basilio Bernardo Fraguela Rodríguez y D. Diego Andrade Canosa. Esta memoria constituye la documentación que, con nuestra autorización, entrega dicho alumno para optar a la titulación de Ingeniero en Informática. D. Diego Andrade Canosa D. Basilio Bernardo Fraguela Rodríguez A Coruña, a 17 de Septiembre de 2010

6

7 A mis padres.

8

9 Agradecimientos En primer lugar me gustaría agradecer a los profesores Basilio Bernardo Fraguela Rodríguez y Diego Andrade Canosa el haberme dado la oportunidad de realizar con ellos este proyecto, así como toda la ayuda prestada a lo largo de estos meses. Ambos me han ofrecido permanente disponibilidad y atención, incluso estando lejos o sacrificando vacaciones y fines de semana. Asimismo han sabido ser tolerantes con el poco respeto del autor por los plazos y las planificaciones. También me gustaría agradecer la buena acogida y colaboración que he vivido durante todo este tiempo en el Grupo de Arquitectura de Computadores y su apretado laboratorio. Dyer ha conseguido que yo consiguiera proyecto, Carlos Hugo me ha resuelto mil y una pequeñas dudas, Jacobo me ha ayudado en la caza y captura de algún escurridizo bug, Raquel me ha despejado algunas dudas en la redacción de esta memoria, Jose me ha dado sabios consejos sobre diferentes problemas y Toño, Pedro, Pablo, Andrés, Moisés, Iván, Sabela, Roberto, Diego y Jorge han sido unos magníficos compañeros en algún que otro café. Gracias a mis muy numerosos amigos por su apoyo moral y compañía. Sobre todo a Lucía, mi novia además de amiga, porque su paciencia de grande es santa y como tal todavía increíble para mí. Gracias por haber sabido esperar por mi tanto tiempo y disculparme en mis numerosos aplazamientos y pérdidas de atención. Por último, muchas gracias a mis abnegados padres que han esperado sin rechistar que su hijo acabara sus parsimoniosos estudios sin ningún reproche o exigencia a pesar del esfuerzo que estos largos años les han supuesto. Esto es gracias a ellos, a su esfuerzo personal y económico, por lo que a ellos les dedico esta memoria.

10

11 Resumen Con la llegada de los procesadores multinúcleo la paralelización se ha extendido a todos los ámbitos de la programación. En este contexto, resulta de especial importancia el uso de herramientas que permitan al programador centrarse en el desarrollo de las aplicaciones, haciendo transparentes los detalles de programación paralela. Este proyecto aborda parte de este problema con la construcción de una librería en C++ para el tratamiento paralelo de un contenedor de datos de tipo conjunto. En contraposición a otras técnicas, las librerías presentan una serie de ventajas: portabilidad, rapidez de desarrollo, no exigen el aprendizaje de nuevos lenguajes a sus usuarios y facilitan la reutilización de código. Con el fin de evaluar las mejoras obtenidas, se utilizan varios programas de prueba que hacen un uso intensivo de contenedores de tipo conjunto. Se compara el rendimiento obtenido mediante el contenedor paralelo desarrollado con el obtenido usando un contenedor no paralelo. Se concluye que la librería elaborada aporta una solución sencilla, portable, eficiente y adaptable al programador interesado en explotar las capacidades de paralelismo de los procesadores de última generación, sin preocuparse por los detalles de la programación paralela. Palabras clave Conjunto, colección, contenedor de datos, Intel Threading Building Blocks, C++, librería, procesadores multinúcleo, paralelización.

12

13 Índice general 1. Introducción Motivación Alcance y objetivos Descripción de la solución Proceso de desarrollo Planificación Materiales y costes Estructura de esta memoria Contextualización Antecedentes Paradigmas de organización de sistemas paralelos Programación de sistemas paralelos Herramientas de expresión de paralelismo POSIX Threads HPF OpenMP MPI HTA IntelTBB Intel TBB Características Ventajas Contenidos de la librería Inicialización de la librería XIII

14 Rangos Algoritmos Contenedores Exclusión mutua Gestor de tareas Un ejemplo sencillo de uso Estudio del rendimiento El tipo Concurrent_Set Requisitos Operaciones a implementar Estructura de datos Soporte de elementos variables La clase concurrent_set como plantilla Estrategias de paralelización Operaciones paralelas que no modifican el indexado Operaciones paralelas que modifican el indexado Operaciones igual a igual Operaciones de todos con todos Operaciones que generan conjuntos de nuevos elementos Operaciones de reducción Implementación y paralelización de operaciones Operaciones de iteración Operaciones de mapa Operaciones de lista Operaciones relacionales Operaciones básicas de conjuntos Operaciones avanzadas sobre conjuntos Evaluación Programas de evaluación Controlador aéreo Camino más corto Algoritmo de Barnes-Hut Análisis de expresividad XIV

15 5.3. Análisis de rendimiento y escalabilidad Paralelización de la librería mediante OpenMP vs IntelTBB Rendimiento y escalabilidad de los programas de evaluación Conclusiones Resumen del trabajo realizado Objetivos alcanzados Trabajo futuro Búsqueda de una mayor eficiencia Implementaciones alternativas Extensión de la librería A. Manual de usuario 131 A.1. Uso y compilación A.2. Parámetros de plantilla A.3. Tipos A.4. Inicialización de la librería A.5. Constructores A.6. Utilidades básicas A.7. Iteradores A.8. Manipulación de datos A.8.1. Operaciones de lista A.8.2. Operaciones de mapa A.8.3. Otras operaciones A.9. Operaciones sobre el conjunto A.9.1. Operciones relacionales A.9.2. Operaciones básicas de conjuntos A.9.3. Operaciones avanzadas sobre conjuntos B. OpenMP 145 B.1. Directivas principales B.1.1. Formato B.1.2. Directiva parallel B.1.3. Directivas de reparto de trabajo B.1.4. Directivas combinadas XV

16 B.1.5. Directivas de sincronización B.1.6. Cláusulas principales B.2. Funciones B.2.1. Funciones de gestión de hilos B.2.2. Funciones de bloqueo B.3. Variables de entorno B.3.1. Variable OMP_NUM_THREADS B.3.2. Variable OMP_DYNAMIC B.3.3. Variable OMP_SCHEDULE XVI

17 Índice de figuras 1.1. Diagrama de Gantt Organización de un multiprocesador de memoria compartida Organización de un multiprocesador de memoria distribuida Etapas de procesamiento en un pipeline Árbol de tareas Repositorio de tareas Aceleración - Plataforma Aceleración - Plataforma Aceleración - Plataforma Tiempo de ejecución frente a tamaño de grano - Plataforma Tiempo de ejecución frente a tamaño de grano - Plataforma Tiempo de ejecución frente a tamaño de grano - Plataforma Aceleración con particionador afín/no afín - Plataforma Aceleración con particionador afín/no afín - Plataforma Aceleración con particionador afín/no afín - Plataforma Estructura de datos de un concurrent_set Estructura en forma de rejilla para evitar bloqueos Secuencia ejecución con cruce en orden Secuencia ejecución con cruce en ciclo Cruce de un conjunto consigo mismo Estructura de un concurrent_set_iterator Predicción de colisiones por el controlador aéreo Proceso de búsqueda del camino más corto Cálculo de fuerzas mediante un quadtree XVII

18 5.4. Tiempos de ejecución para air_control Escalabilidad para air_control Variación en el n o de subconjuntos para air_control Tiempos de ejecución para shortest_path_ Escalabilidad para shortest_path_ Variación en el n o de subconjuntos para shortest_path_ Tiempos de ejecución para shortest_path_ Escalabilidad para shortest_path_ Variación en el n o de subconjuntos para shortest_path_ Tiempos de ejecución para barnes-hut Escalabilidad para barnes-hut Variación en el n o de subconjuntos para barnes-hut XVIII

19 Índice de cuadros 1.1. Resumen de actividades Resumen de los costes del proyecto Tiempos de ejecución - Plataforma Tiempos de ejecución - Plataforma Tiempos de ejecución - Plataforma Análisis de la expresividad en SLOC OpenMP vs IntelTBB Tiempos de ejecución de air_control Escalabilidad de air_control en c Escalabilidad de air_control en c Tiempo - n o de subconjuntos en air_control Tiempos de ejecución de shortest_path_ Escalabilidad de shortest_path_1 en c Escalabilidad de shortest_path_1 en c Tiempo - n o de subconjuntos en shortest_path_ Tiempos de ejecución de shortest_path_ Escalabilidad de shortest_path_2 en c Escalabilidad de shortest_path_2 en c Tiempo - n o de subconjuntos en shortest_path_ Tiempos de ejecución de barnes-hut Escalabilidad de barnes-hut en c Escalabilidad de barnes-hut en c Tiempo - n o de subconjuntos en barnes-hut XIX

20 XX

21 Índice de listados 3.1. Función suma de matrices Función suma de matrices paralelizada Functor para la paralelización Aplicación de una operación Paralelización de la aplicación de una operación Operación de reducción Paralelización de una operación de reducción Pseudocódigo para el controlador aéreo Algoritmo de búsqueda del camino más corto Primera solución a la expansión en anchura Segunda solución a la expansión en anchura Algoritmo de Barnes-Hut A.1. Parámetros de plantilla de concurrent_set A.2. Constructores de concurrent_set XXI

22 XXII

23 Capítulo 1 Introducción En este proyecto se desarrolla una librería que permite al programador hacer uso de un contenedor de datos de tipo conjunto, paralelizado de modo transparente al usuario. El presente capítulo describe la motivación de este trabajo, los objetivos planteados y una breve exposición de la solución adoptada. Asimismo, presenta la metodología y fases del desarrollo del proyecto, y concluye con el detalle de la estructura y capítulos de esta memoria Motivación La mejora constante en la tecnología de integración supone un aumento en el número de transistores que es posible integrar en un solo chip [4] [5]. El incremento del paralelismo a nivel de instrucción, uso principal que durante años se le ha dado a la existencia de este número creciente de transistores, no conlleva grandes mejoras a nivel de rendimiento ya que las dependencias entre instrucciones ponen un límite a su explotación. Así, se ha hecho necesario emplear los nuevos transistores disponibles en explotar paralelismo con un nivel de grano más grueso. La tendencia actual consiste en integrar un creciente número de núcleos en el mismo chip, dando lugar a lo que se ha dado en llamar prodesadores multinúcleo. En este tipo de arquitecturas es posible explotar el paralelismo a nivel de hilo. Este tipo de paralelismo ha sido común en campos especializados de la computación como son la computación científica y de alto rendimiento. Sin embargo, la disponibilidad de varios núcleos de procesamiento en un solo microprocesador es una novedad que ha extendido el ámbito y necesidad de la programación paralela a todas las áreas de la informática 1

24 en las que se presentan aplicaciones con demandas computacionales elevadas, o incluso medias si precisan tiempos de respuesta cortos. Como la programación paralela ha estado limitada durante muchos años a programadores especializados que trabajaban en áreas de aplicación restringidas, las herramientas disponibles estaban frecuentemente enfocadas a un dominio concreto y eran difíciles de usar. Hoy en día, sin embargo, todas las aplicaciones con un mínimo de exigencias computacionales deben ser paralelizadas. Ésto conlleva que muchos programadores no especializados en computación científica, se tienen que enfrentar a la paralelización de un gran número de aplicaciones tanto nuevas como heredadas. Es necesario por tanto disponer de herramientas que faciliten a estos programadores la explotación del paralelismo en sistemas multinúcleo. Existen varias aproximaciones al problema de la paralelización de algoritmos: lenguajes diseñados específicamente para este fin, extensiones de lenguajes existentes o librerías que complementan estos mismos lenguajes. Mientras que los lenguajes específicos no permiten la reutilización de código ya existente y las extensiones a lenguajes requieren el aprendizaje de una nueva sintaxis, el uso de librerías resulta especialmente interesante por su portabilidad y la facilidad que proporciona tanto en la reutilización de código como en el aprendizaje de su uso. Asimismo, las diferentes herramientas de paralelización permiten abordar la programación de sistemas paralelos mediante dos enfoques: el paralelismo de datos, en el que se aplica una misma operación de forma simultánea sobre los diferentes datos de un conjunto; y el paralelismo de tareas, donde cada tarea se divide en varias subtareas que son ejecutadas simultáneamente. El paralelismo de datos es característico de las librerías basadas en contenedores que paralelizan las operaciones sobre sus contenidos. Sin embargo, es posible adoptar un enfoque de paralelismo de datos mediante una herramienta orientada al paralelismo de tareas sin más que aplicar subtareas idénticas a diferentes fragmentos de datos. Una librería orientada al paralelismo de tareas es Intel Threading Building Blocks (TBB) [8] [9]. Esta librería está implementada en un lenguaje ampliamente extendido como C++ [24] y permite la paralelización de aplicaciones de propósito general de forma simple, eficiente y portable. Libera al programador de la responsabilidad en la gestión de los hilos computacionales, siendo responsabilidad del mismo únicamente especificar la división del trabajo a realizar en tareas que serán asignadas, automáticamente por parte de la librería, a hilos computacionales. 2

25 De esta manera, el programador debe tratar con conceptos como especificar métodos que permitan la división recursiva del trabajo y la definición de tareas, empleando un buen número de funciones y clases para la paralelización. Resulta por tanto deseable proporcionar un nivel de abstracción superior al programador que le permita construir aplicaciones eficientes abstrayéndose, todavía más si cabe, de las labores de paralelización subyacentes. Una de las construcciones más comúnmente utilizadas y en las que se han explorado más frecuentemente las oportunidades de programación paralela son las estructuras de datos regulares como las matrices. Sin embargo, no ha ocurrido lo mismo con otras estructuras irregulares como pueden ser las listas, colas, árboles, o conjuntos. Este tipo de estructuras son comúnmente utilizadas por programadores sin experiencia en la paralelización. La construcción de un contenedor paralelo, como ya se ha comentado, es un buen camino para ayudar a los programadores a incorporar paralelismo en sus programas y la librería Intel TBB constituye una plataforma adecuada sobre la que desarrollar dicho contenedor Alcance y objetivos El principal objetivo de este proyecto es construir una librería que permita el tratamiento paralelo de un contenedor de datos de tipo conjunto de forma transparente al usuario, sobre un lenguaje de amplia difusión como C++. Se estudia también el comportamiento de Intel TBB como herramienta de paralelización. De esta manera los objetivos concretos de este proyecto son: Realizar un estudio del uso de la librería Intel TBB tanto teórico como experimental. Este estudio incluye sus características, funcionalidades y conceptos de paralelización subyacentes. Desarrollar un contenedor de datos paralelo de tipo conjunto, que haga transparente al usuario las tareas subyacentes de paralelización. Éste ha de permitir las funcionalidades más comúnmente demandadas en las aplicaciones que hacen uso intensivo de este tipo de contenedores y debe respetar la interfaz de los conjuntos de la librería estándar. Ilustrar el uso de la librería a través de una serie de programas de prueba que sirvan de base para evaluar su expresividad y rendimiento experimentalmente. 3

26 1.3. Descripción de la solución El contenedor construido proporciona todas las operaciones implementadas por los conjuntos de la Standard Template Library 1 (STL), de forma que la implementación secuencial existente y la paralela propuesta en este proyecto sean intercambiables en el código sin repercutir en el correcto funcionamiento del mismo. Adicionalmente se han añadido varias operaciones que comúnmente se realizan sobre este contenedor. Entre ellas, operaciones que dan soporte al álgebra de conjuntos, relaciones, aplicaciones y predicados [2]. Para alcanzar los objetivos planteados se ha diseñado un contenedor de datos de tipo conjunto construido a partir de una serie de subconjuntos que permanecen ocultos al usuario de la librería y que unidos contienen todos los elementos del conjunto global. De esta forma, las operaciones aplicadas sobre el conjunto pueden subdividirse internamente a la librería en operaciones separadas que serán ejecutadas concurrentemente sobre cada uno de los subconjuntos que lo forman. Así surge un patrón de división de cada tarea en varias subtareas de forma natural que simplifica la paralelización de las operaciones usando Intel TBB. Cada uno de los subconjuntos que forman un conjunto es implementado mediante un conjunto de la librería STL. La STL está diseñada poniendo especial atención en la eficiencia y se ha probado y mejorado a lo largo de años. Utilizándola se garantiza que el procesamiento de cada uno de los subconjuntos sea altamente eficiente. Para probar la librería se han utilizado tanto pequeños programas de prueba como algoritmos de uso extendido en la industria y en diversas áreas científicas, tales como el algoritmo de los n-cuerpos de Barnes-Hut. También se ha estudiado la expresividad de la librería, ya que uno de los objetivos de este proyecto es simplificar la programación de algoritmos paralelos Proceso de desarrollo El proceso de desarrollo se ha basado en el modelo incremental [1] el cual se basa en construir una implementación parcial del sistema global y posteriormente ir aumentando, en cada iteración del proceso, la funcionalidad del sistema. Cada iteración supone el refinamiento de funcionalidades anteriores o el desarrollo de una nueva fun- 1 Librería Estándar de Plantillas. 4

27 cionalidad. Cada una de estas fases, a su vez, se construye realizando un ciclo de desarrollo completo, que incluye las etapas de análisis, diseño, implementación y prueba. Así, en una primera fase se ha creado el núcleo de la librería implementando la estructura de datos a utilizar y sus operaciones básicas. Después, se han incorporado en distintos bloques las operaciones definidas en la STL para el tipo conjunto. Más tarde, se han añadido nuevas operaciones comúnmente utilizadas sobre este tipo de contenedor. Por último, se han paralelizado las operaciones con gran carga computacional. Esta estructuración en el proceso de desarrollo de software facilita el establecimiento de una serie de hitos que permiten gestionar decisiones y realizar tareas de verificación y validación sobre el trabajo realizado. De este forma, se intenta manejar convenientemente el riesgo que aparece de forma natural en los proyectos de investigación y desarrollo Planificación En la realización de este proyecto se siguieron una serie de pasos que se pueden agrupar en las siguientes fases: Contextualización. Esta fase estudia el contexto y las herramientas implicadas en el proyecto. Partes importantes de la misma son la familiarización con el lenguaje de programación C++, su librería de plantillas estándar (STL), y la librería Intel TBB. Desarrollo. En la segunda fase se procede al desarrollo de la librería siguiendo, como ya se ha dicho, un modelo incremental. Partiendo del núcleo del sistema, se ha pasado por diversas fases de análisis, diseño, implementación y pruebas hasta alcanzar el resultado deseado. Evaluación y conclusiones. La tercera fase consiste en la implementación de varios programas de prueba que hacen un uso intensivo de la librería desarrollada, y la extracción de resultados respecto a la mejora de la programabilidad y el rendimiento proporcionados por la librería. Algunos de estos programas son utilizados en la industria y en ámbitos científicos, acercando así la evaluación a un entorno de desarrollo real. 5

28 Id Tarea Duración Inicio Fin T1 Documentación previa 30 días T2 Familiarización con el entorno de trabajo 5 días T3 Desarrollo 90 días T4 Experimentación 10 días T5 Conclusiones 4 días T6 Redacción de la documentación 30 días Cuadro 1.1: Resumen de actividades Figura 1.1: Diagrama de Gantt Documentación. En la última fase se documenta el trabajo realizado. La documentación incluye tanto esta memoria como la documentación técnica que acompaña a la distribución de la librería. Las tareas en las que se ha dividido el proyecto, así como su duración, se muestran en el Cuadro 1.1. La fase de Contextualización se divide en dos tareas, una de Documentación previa y otra de Familiarización con el entorno de trabajo. La tarea de Desarrollo incluye tanto el desarrollo de la librería como de los programas de prueba. Se define una tarea de Experimentación en la que se lleva a cabo la toma de medidas de evaluación y una de Conclusiones en la que se analizan los resultados obtenidos. Finalmente, en la tarea de Redacción de la documentación se incluyen las labores descritas para la fase de Documentación. La planificación se muestra en la Figura 1.1 a través de un diagrama de Gantt. La jornada laboral empleada ha sido de 8 horas diarias de lunes a viernes. Con el fin de ajustarse a las fechas de inicio y fin de proyecto, fue necesario un cambio en el calendario laboral incluyendo algunos sábados como días de trabajo y estableciendo extensiones en el horario laboral en determinadas fechas. La planificación final del proyecto determina una duración de 139 días con horas de trabajo. 6

29 Tipo de recurso Nombre Coste S.O GNU/Linux Ubuntu e GNU Compiler Collection e Intel TBB e Software Doxygen e Kile e Inkscape e Gnuplot e OpenProj e SLOCCount e Hardware PC multinúcleo (7 meses) 600 e Cluster nm (10 horas) 10 e Recursos humanos Ingeniero Informático (7 meses) e Total e Cuadro 1.2: Resumen de los costes del proyecto 1.6. Materiales y costes En la realización de este proyecto han sido necesarios un compilador ANSI C++ y la librería Intel TBB, disponible bajo licencia GPLv2 2. Como compilador de C++ se ha usado gcc [26] por ser ésta una plataforma de código abierto, distribuida bajo la licencia GPL, y con amplio soporte del estándar ANSI C++. También se han utilizado una serie programas de apoyo, que aunque no han sido parte indispensable del desarrollo del proyecto, sí han tenido un papel importante. Así, En la planificación se ha utilizado OpenProj, distribuido bajo licencia CPAL 3. En la fase de pruebas se ha utilizado SLOCCount, disponible bajo licencia GPLv2. En la generación de la documentación técnica se ha usado Doxygen [28], distribuido bajo licencia GPL. En la redacción y composición de la documentación del proyecto se ha utilizado Kile, disponible bajo licencia GPLv2. En la elaboración de diagramas se ha utilizado Inkscape, distribuido bajo licencia GPLv2. En la construcción de gráficas se ha utilizado Gnuplot, disponible bajo una licencia propia de código abierto. Como plataforma hardware para el desarrollo y las pruebas se ha usado un compu- 2 General Public License. 3 Common Public Attribution License. 7

30 tador personal multinúcleo de última generación. Así mismo, en la toma de medidas de rendimiento se han utilizado diferentes nodos del cluster nm [32] perteneciente al Grupo de Arquitectura de Computadores (GAC) de la Universidade da Coruña. El Cuadro 1.2 refleja un resumen de los recursos empleados en el desarrollo de este proyecto con una aproximación a los costes de los mismos. Para estimar el salario del ingeniero informático se utiliza como referencia el convenio colectivo vigente [33] Estructura de esta memoria El resto de este documento está organizado en 5 capítulos: En el capítulo 2 se analiza el estado del arte en el ámbito de la computación paralela. En el capítulo 3 se describe la librería de paralelización de propósito general Intel TBB, se presenta un pequeño ejemplo de uso y un análisis de su rendimiento bajo diferentes configuraciones. En el capítulo 4 se describe la interfaz y operaciones implementadas de la librería propuesta, se abordan los detalles de implementación del contenedor y la forma en la que se ha conseguido extraer paralelismo. En el capítulo 5 se recoge la evaluación de la librería. Finalmente, el capítulo 6 presenta las conclusiones más importantes derivadas del estudio y el trabajo llevado a cabo en este proyecto. Se plantean también posibles líneas de trabajo futuro, que completará el desarrollado hasta ahora. 8

31 Capítulo 2 Contextualización El aumento constante de las demandas computacionales ha provocado la evolución continua de los sistemas de computación. Esta evolución ha llevado inevitablemente a la computación paralela a abarcar todos los ámbitos de la informática. Las arquitecturas paralelas de sistemas son hoy mayoritarias, a la par que se desarrollan nuevas herramientas que facilitan al programador abordar la paralelización de algoritmos. Este capítulo analiza los antecedentes que han desembocado en la situación actual y describe los diferentes paradigmas y herramientas de paralelización disponibles hoy en día Antecedentes En 1965 Gordon E. Moore publica un artículo en el que describe las leyes que marcarían la evolución de los circuitos integrados hasta el presente [4]. En él argumenta que el coste por componente en un circuito integrado disminuye con el aumento del número de componentes, hasta alcanzar un mínimo en el que, si se añaden más componentes, el coste vuelve a crecer debido a que un excesivo tamaño del circuito favorece los fallos que arruinan el circuito en su conjunto. Moore establece que el número de componentes integrados en un solo chip que minimiza los costes de fabricación por componente se duplica cada año al aumentar progresivamente la escala de integración. Ésta regla se conoce como Ley de Moore y se viene cumpliendo año tras año, con pequeños ajustes en el factor de multiplicación [5], desde que fue formulada. El aumento de la escala de integración no sólo aumenta el número de componentes integrables en un chip de bajo coste, sino que también permite aumentar la frecuen- 9

32 cia de reloj del procesador. El aumento de la frecuencia de reloj hace posible procesar un mayor número de instrucciones por unidad de tiempo, lo que ha permitido atender parte de las crecientes demandas computacionales durante años. Sin embargo, los límites en la disipación del calor generado causan que el constante aumento de la frecuencia no constituya una opción factible en la búsqueda de una mayor capacidad de cómputo [6]. Por lo tanto, si no se puede disminuir en la medida de lo necesario el tiempo de ejecución de una instrucción, la solución para incrementar la productividad, pasa por procesar más instrucciones simultáneamente utilizando diferentes formas de paralelismo. Es precisamente el creciente número de componentes integrados en un solo chip el que permite adoptar diferentes soluciones de paralelismo en las sucesivas arquitecturas de computadores. En un principio se explota el paralelismo a nivel de bit, pasando de arquitecturas de bit serie a bit paralelo, pero pronto se adoptan estrategias de paralelismo a nivel de instrucción. Así, por ejemplo, se explota el paralelismo funcional a nivel de instrucción introduciendo unidades de punto flotante independientes que permiten tratar este tipo de instrucciones sin hacer esperar a las siguientes en el flujo de programa. También se popularizan los procesadores segmentados, que explotan el paralelismo temporal, dividiendo el cauce de ejecución en sucesivas fases, donde cada una puede ejecutar, concurrentemente a las otras, una porción de una instrucción diferente. El paralelismo espacial a nivel de instrucción es implementado por las arquitecturas superescalares, que replican la circuitería de cada una de las fases de ejecución, de forma que varias instrucciones puedan ejecutarse en una misma fase en un determinado momento. Sin embargo, el paralelismo a nivel de instrucción está limitado por las dependencias entre las instrucciones del flujo de ejecución. Más allá de un cierto número de instrucciones es costoso y poco probable encontrar instrucciones independientes que se puedan ejecutar simultáneamente. En el ámbito de la supercomputación, en el que es común el procesado de grandes estructuras regulares de datos, se explota el paralelismo a nivel de lazo mediante los procesadores vectoriales, populares sobre todo durante la década de los ochenta. Los procesadores vectoriales siguen dos estrategias diferentes de paralelización. Por una parte, los procesadores supersegmentados explotan el paralelismo temporal dividiendo el procesamiento de cada elemento de un vector en un gran número de fases muy cortas de forma que muchos elementos se procesan simultáneamente. Por otra parte, 10

33 los procesadores matriciales 1 explotan el paralelismo espacial replicando las unidades aritmético-lógicas de forma que se pueden procesar un gran número de elementos de forma simultánea. Sin embargo, estas arquitecturas están fuertemente especializadas en un tipo concreto de computación y también tienen limitaciones derivadas de las dependencias entre las instrucciones vectoriales. Hoy en día son herederos de estos planteamientos arquitecturales los procesadores gráficos y el conjunto de instrucciones multimedia de los procesadores convencionales. Una vez agotadas o limitadas las posibilidades de explotación de paralelismo en los niveles de bit, instrucción o lazo, se ha venido explotando el paralelismo a tamaños de grano mayores, como puede ser el paralelismo a nivel de hilo o a nivel de proceso. Los procesadores multihilo 2 constituyen una evolución de los procesadores superescalares en los que se explota el paralelismo a nivel de hilo. En éstos se dispone de un cauce de ejecución con fases replicadas de forma que se pueden ejecutar varias instrucciones diferentes en la misma fase, como en los procesadores superescalares, pero además proporciona la posibilidad de que estas instrucciones provengan de dos hilos de ejecución diferentes, de forma que es más fácil encontrar instrucciones independientes entre sí. Por su parte, los sistemas multiprocesador, en los que se combina el trabajo de varios procesadores para abordar un trabajo global, explotan tanto el paralelismo a nivel de hilo como a nivel de proceso. Sólo recientemente, con la aparición de los procesadores multinúcleo, ha sido posible explotar el paralelismo a nivel de hilo o proceso dentro de la arquitectura de un único circuito integrado Paradigmas de organización de sistemas paralelos Dentro de los sistemas de procesamiento paralelo multiprocesador existen dos grandes paradigmas de organización: memoria compartida y memoria distribuida [7]. En el caso de los sistemas de memoria compartida existe un espacio de memoria global compartido por todos los procesadores (ver Figura 2.1). El tiempo de acceso a una determinada posición de memoria es aproximadamente el mismo para cualquier procesador. Este tipo de computadores se denominan multiprocesadores simétricos o SMP 3. 1 También denominados Array Processors en inglés. 2 También denominados Simultaneous MultiThreading (SMT). 3 Por las siglas del inglés Symetric Multi-Processor. 11

34 Figura 2.1: Organización de un multiprocesador de memoria compartida Los principales inconvenientes en este tipo de organizaciones son los riesgos de acceso simultáneo a memoria, que pueden provocar problemas de concurrencia si dos procesadores acceden a la misma región; y la baja escalabilidad, ya que al compartir todos los procesadores una misma red de acceso a memoria, el número de procesadores está limitado por el caudal que soporte esta red. De la misma forma, mantener la coherencia caché en estos sistemas no es un asunto trivial y puede complicar la arquitectura. La principal ventaja que presenta esta organización es la sencillez y velocidad de la comunicación entre procesadores, que se limita a accesos de lectura y escritura en memoria, lo cual simplifica enormemente la programación. En los sistemas de memoria distribuida cada procesador tiene acceso exclusivo a su memoria local (ver Figura 2.2). La única forma en la que dos procesadores pueden compartir información es mediante comunicaciones explícitas por paso de mensajes. Estos mensajes son en general lentos y su gestión complica la programación. A cambio, esta organización es muy escalable ya que la mayoría de los accesos son a las memorias locales y no cargan la red de interconexión. Esta categoría incluye, entre otras, las arquitecturas de procesamiento paralelo masivo o MPP 4 y los clusters. La diferencia fundamental entre ambos es que en un cluster cada una de las partes componentes constituye un computador completo que puede trabajar autónomamente. Un planteamiento intermedio entre estas dos organizaciones son los sistemas de memoria compartida-distribuida. En este caso cada procesador cuenta con una memoria local a la que puede acceder rápidamente, pero ésta es referenciada en un espacio de direccionamiento global compartido por todos los procesadores. Los accesos 4 Por las siglas del inglés Massive Parallel Processing. 12

35 Figura 2.2: Organización de un multiprocesador de memoria distribuida a secciones de memoria fuera de la memoria local tienen lugar mediante una red de interconexión y son, por lo tanto, mucho más lentos. Por esta razón, esta organización se denomina de acceso a memoria no uniforme o NUMA 5. Los sistemas NUMA presentan las ventajas de las dos organizaciones anteriores. Así, facilita la programación pues las comunicaciones entre procesadores se limitan a lecturas y escrituras en memoria, pero mantiene la escalabilidad de una organización de memoria distribuida. El principal problema de estos sistemas es la complejidad del controlador de memoria y el mantenimiento de la coherencia caché. El aumento continuo de la escala de integración ha facilitado que a principios de la pasada década se comenzaran a fabricar computadores SMP integrados en un único chip. Éstos son los llamados procesadores multinúcleo. Actualmente esta tecnología abarca todo tipo de mercados, estando ya presente en los procesadores más comunes del ámbito doméstico. Éste es el tipo de plataforma sobre la se empleará la librería propuesta en este proyecto Programación de sistemas paralelos La programación de sistemas que sólo explotan paralelismo a nivel de instrucción no difiere de la programación de un sistema secuencial. Todo el trabajo de paralelización lo realiza el procesador, junto con el compilador, de forma transparente al programador. Sin embargo, la programación en los sistemas paralelos a nivel de hilo o proceso requiere la participación activa del programador. Éste debe determinar cómo se divide el trabajo global en diferentes hilos de ejecución y cómo se comunican 5 Por las siglas del inglés Not Uniform Memory Access. 13

36 y sincronizan éstos entre sí. Esta sección describe los diversos paradigmas existentes dentro de la programación paralela. Paso de mensajes vs. memoria compartida En los sistemas de memoria compartida los hilos se comunican mediante operaciones de lectura escritura en áreas comunes de la memoria. Ésto puede requerir utilizar mecanismos de sincronización explícitos en el acceso a las variables compartidas para evitar las condiciones de carrera. En los sistemas de memoria distribuida las comunicaciones tienen lugar mediante operaciones explícitas de envío y recepción de datos. La transferencia de un mensaje provoca una copia de memoria local a memoria local de los procesadores que participan en la comunicación. En términos generales, ésto complica la programación, ya que en muchas ocasiones no es trivial hacer un reparto de datos que optimice el balanceo de carga y minimice el uso de la red de interconexión. Sin embargo, usando un modelo de paso de mensajes no es necesario utilizar mecanismos de sincronización puesto que ésta ya está implícita en las operaciones de envío y recepción. Asimismo, es posible simular un modelo de programación de paso de mensajes en una arquitectura de memoria compartida y un modelo de memoria compartida en un entorno de paso de mensajes, mediante una capa software que lo soporte. Paralelización de tareas vs. paralelización de datos Existen dos aproximaciones a la hora de abordar la programación de un sistema paralelo: el paralelismo de datos, en el que se aplica una misma operación de forma simultánea sobre los diferentes datos de un conjunto; y el paralelismo de tareas, donde cada tarea se divide en varias subtareas que son ejecutadas simultáneamente en diferentes procesadores. La clave de la paralelización de tareas reside en la división del trabajo en diferentes tareas que son asignadas directamente a hilos de ejecución físicos, sobre cuya planificación solamente decide el sistema operativo. Por otra parte, la paralelización a nivel de datos se basa en la creación de tareas que realizan una misma operación sobre diferentes fragmentos de un conjunto de datos. Realmente este enfoque es una particularización del anterior, ya que nada impide a los sistemas basados en paralelización de tareas el aplicar una misma operación a diferentes conjuntos de datos. El paralelismo 14

37 a nivel de datos aporta sencillez a la programación y distribución de trabajo, que ahora se limita a definir la distribución de los datos entre los diferentes procesadores Herramientas de expresión de paralelismo A medida que el paralelismo a nivel de hilo o proceso se extiende en los sistemas de computación, surgen diversas herramientas que ayudan al programador a implementar aplicaciones paralelas. Estas herramientas se pueden clasificar en tres tipos: lenguajes específicos, extensiones a lenguajes y librerías de paralelización. En el caso de los lenguajes específicamente diseñados para la paralelización, éstos obligan al programador a aprender un nuevo lenguaje y no permiten aprovechar el legado de programas ya construidos en otros lenguajes de uso genérico. Un caso diferente son los lenguajes generalistas que soportan de forma nativa algún tipo de construcción para expresar paralelismo. Sin embargo, en este caso las herramientas de paralelización suelen ser muy primitivas y poco específicas, de forma que no suponen una gran ayuda al programador. Por su parte, las extensiones a lenguajes sí que permiten la reutilización de código existente, pero obligan al programador a estudiar una nueva sintaxis y requieren un sistema de compilación adicional. Las librerías de paralelización, sin embargo, no requieren aprendizaje de nuevas sintaxis, permiten la reutilización de código y son más portables y rápidas de desarrollar que los compiladores. Son, por lo tanto, la mejor opción en un gran número de casos. A continuación, se estudian algunas de las herramientas de paralelización disponibles hoy en día POSIX Threads La mayoría de lenguajes no contemplan la existencia de varios hilos de ejecución y éstos deben ser creados mediante llamadas a librerías específicas. En los sistemas basados en UNIX, la interfaz POSIX 6 [10] proporciona funcionalidades multihilo que permiten crear, ejecutar, comunicar, sincronizar y eliminar hilos de ejecución. Sin embargo, utilizar los hilos nativos del sistema presenta problemas de portabilidad, ya que su implementación es fuertemente dependiente del sistema operativo subyacente. Por otra parte, los hilos son genéricos en su planteamiento. Sus utilidades abarcan desde el manejo de interfaces de usuario, hasta realizar tareas de paralelización, sin que sopor- 6 POSIX: Portable Operating System Interface for UNIX. 15

38 ten instrumentos específicos de paralelización 7. Ésto provoca que en general sea complicado llevar a cabo tareas de paralelización gestionando hilos directamente, y más complicado aún conseguir una escalabilidad óptima. Finalmente, es el programador el que gestiona directamente el número de hilos en ejecución, lo que puede provocar problemas de ineficiencia por excesivos cambios de contexto, en caso de utilizar más hilos de los estrictamente requeridos, o desaprovechamiento de recursos, en caso de utilizar menos hilos de los necesarios HPF High Performance Fortran 8 (HPF) [11] [12] es un lenguaje de programación diseñado para dar soporte a la programación orientada al paralelismo de datos [13]. Este lenguaje surge a principios de los años noventa en un contexto en el que existen diversos dialectos de lenguajes orientados al paralelismo de datos, muchos de ellos específicos para alguna arquitectura determinada. El objetivo principal de HPF es permitir extraer el máximo rendimiento de las arquitecturas paralelas sin sacrificar la portabilidad y con un alto nivel de estandarización. Para ello se basa en un lenguaje ampliamente aceptado en el ámbito de la computación científica como es Fortran. Ésto permite que HPF sea compatible con el código existente y que no requiera el aprendizaje de una sintaxis completamente nueva, aunque sí enriquecida con nuevas construcciones. Así, se incluyen operaciones elementales con vectores, operaciones de reducción, manejo de mascaras, directivas de distribución de datos y otras diversas herramientas que dotan al programador de instrumentos para el manejo del paralelismo de datos. Sin embargo, los principales problemas de HPF son que soporta casi exclusivamente un modelo de programación basado en la explotación de paralelismo de datos y que Fortran, aunque es un lenguaje de propósito general, está extendido tan sólo en ámbitos de computación científica. Históricamente el principal problema de HPF en el ámbito de la comunidad de computación de altas prestaciones, a la que estaba orientado principalmente, es que esperaba demasiado de la tecnología de compiladores existente. De esta forma, el rendimiento de las aplicaciones desarrolladas en HPF podía oscilar mucho de unas plataformas a otras, en función de la calidad del compilador disponible, y con demasiada frecuencia el rendimiento era muy inferior al conseguido con desarrollos manuales. 7 Por esta razón, la programación con hilos se considera el lenguaje ensamblador del paralelismo. 8 Fortran de alto rendimiento. 16

39 Por este motivo el lenguaje HPF ha caído en desuso OpenMP OpenMP 9 [14] [16] es una herramienta de paralelización enfocada a sistemas de memoria compartida que goza de gran popularidad. OpenMP es una extensión disponible para varios lenguajes de programación como Fortran y C++ que expresa el paralelismo mediante directivas de compilación, funciones y variables de entorno que guían al compilador en una paralelización semiautomática del código. De esta forma, el programador se libera en gran medida de la responsabilidad de la distribución del trabajo, ya que es el compilador el que se encarga de repartirlo de forma escalable entre los procesadores disponibles. Para ello utiliza un modelo de paralelización basado en una estrategia fork-join, de forma que cada vez que se encuentra una sección del código que debe ejecutarse en paralelo, se crean una serie de hilos que realizarán el trabajo, los cuales se destruyen al final de dicha sección. La creación y destrucción de hilos puede ser virtual si el runtime de OpenMP es eficiente, evitando la sobrecarga constante en la generación y destrucción de hilos que puede perjudicar al rendimiento. Por último, OpenMP está limitado en cierta forma por las directivas que ofrece. Éstas están orientadas fundamentalmente a la paralelización de grandes bucles regulares, si bien en los últimos años su ámbito de aplicación ha crecido gracias a la inclusión de nuevas directivas de creación y encolado de tareas en su última especificación [15] MPI Al igual que OpenMP, MPI 10 [17] [18] goza de gran popularidad como herramienta de paralelización. MPI es un estándar que define una especificación de una API que permite la comunicación por paso de mensajes entre varios computadores. Generalmente se utiliza en multicomputadores de memoria distribuida, pero también es posible utilizarlo en entornos de memoria compartida. MPI, al contrario que el uso de hilos nativos, es portable ya que constituye una capa de abstracción que oculta el uso de los mismos. Sin embargo, es necesario asignar las tareas a los procesadores explícitamente y conserva gran parte de la complejidad de la programación de los hilos. Ésto provoca más esfuerzo de programación, mayor tiempo de depuración y mayor esfuerzo 9 Open Multi-Processing. 10 Message Passing Interface (Interfaz de Paso de Mensajes). 17

40 de mantenimiento. Un antecesor cercano a MPI es PVM 11. Este sistema comparte en gran medida el planteamiento de MPI y ha llegado ha tener cierta repercusión sobre todo en el ámbito académico. Sin embargo, no cuenta con un soporte comparable a MPI y su presencia en entornos de producción es meramente testimonial HTA Hierarchically Tiled Arrays 12 (HTA) [20] [19] es una librería disponible para lenguajes como Matlab y C++ basada en el paralelismo de datos que cuenta con implementaciones para entornos de memoria tanto compartida como distribuida. Proporciona un tipo de dato que facilita la creación de aplicaciones basadas en bloques de vectores y matrices utilizando lenguajes orientados a objetos. Un HTA es un vector dividido en bloques o secciones separadas por líneas de partición. Cada sección puede ser un vector al uso u otro HTA, dotando al tipo de dato de un marcado carácter recursivo. Las ideas principales de esta librería son mejorar la localidad de los accesos a memoria y facilitar al programador la explotación del paralelismo de datos de forma transparente. Ésta es una herramienta muy específica centrada en el paralelismo de datos en vectores y matrices que no soporta otras formas de paralelismo IntelTBB En el año 2006 Intel publica la librería Intel Threading Building Blocks o Intel TBB [8] [9] para el lenguaje de programación C++. Esta librería implementa el paralelismo mediante la definición de tareas que pueden ejecutarse concurrentemente, las cuales son asignadas de forma automática por un planificador a los hilos físicos disponibles. Las tareas se sitúan en un nivel de abstracción superior a los hilos, separando las labores de paralelización de los detalles de implementación. Intel TBB fomenta la definición recursiva de tareas, de forma que en caso de que el número de tareas sea inferior al número de hilos de ejecución disponibles, es posible aprovechar los recursos ociosos subdividiendo alguna de dichas tareas, de forma que se obtiene un balanceo de carga dinámico. Aunque esta librería está basada en el paradigma de paralelismo de tareas, contiene construcciones que soportan directamente las formas más comunes de 11 Parallel Virtual Machine (Máquina Virtual Parallela). 12 Literalmente vectores particionados jerárquicamente. 18

41 paralelismo, incluidas diferentes formas de paralelismo de datos. Éstas y otras características hacen de la librería Intel TBB un buen candidato para la paralelización de la librería propuesta en este proyecto. 19

42 20

43 Capítulo 3 Intel Threading Building Blocks La paralelización del contenedor implementado en este proyecto se realiza utilizando la librería Intel Threading Building Blocks (Intel TBB). Se trata de una librería de paralelización de propósito general para C++ en la que el programador define una serie de tareas paralelas que la propia librería se encarga de asignar a hilos de ejecución concurrentes de forma transparente. Esto permite al programador centrarse en la extracción de paralelismo de los algoritmos sin la distracción de los mecanismos de implementación subyacentes. En este capítulo se estudian las principales características de esta herramienta de paralelización. Así, en la Sección 3.1 se exponen sus fundamentos y las ventajas que de ellos se derivan, en la Sección 3.2 se estudia qué funciones y utilidades ofrece, y por último, en la Sección 3.3 se plantea un pequeño ejemplo de uso de la librería con el que se realizan pruebas de rendimiento sobre la misma Características Como hemos visto en el capítulo anterior, las librerías de paralelización presentan una serie de ventajas respecto a otros métodos para la extracción de paralelismo: son más portables, no exigen a los programadores el aprendizaje de nuevos lenguajes y facilitan la reutilización de código legado. La librería Intel TBB hereda además muchos de los avances desarrollados en tecnologías paralelas de los últimos años. A continuación se detallan algunos de ellos. Intel TBB se encuadra dentro de un paradigma de paralelización orientada a tareas. La labor del desarrollador se centra en la descomposición del trabajo a realizar en 21

44 un gran número de pequeñas tareas que se puedan ejecutar concurrentemente. La librería se encarga de repartir estas tareas automática y dinámicamente entre los diferentes hilos de ejecución de forma transparente al usuario. Para ello fomenta una definición recursiva de la división de las tareas. Si en un momento dado es necesario un número mayor de tareas, es posible dividir las tareas actuales en otras más pequeñas de forma recursiva, hasta alcanzar el número deseado de las mismas. Este mecanismo favorece la escalabilidad y facilita el correcto balanceo de carga. En el reparto de trabajo entre los hilos de ejecución se utiliza la táctica de task stealing 1. Cuando un hilo se encuentra ocioso, intenta traer a su cola de tareas trabajo de la cola de otro hilo. Esto provoca que el balanceo de carga se gestione de forma dinámica y eficiente, liberando, por lo tanto, al usuario de la gestión de los hilos de ejecución. También se minimiza la labor de sincronización ya que ésta es implícita en la mayoría de los casos. De esta forma, no sólo conseguimos una mayor sencillez y legibilidad en el código, sino que se evitan posibles ineficiencias o errores encubiertos derivados de un mal uso de los bloqueos. Finalmente, la librería Intel TBB utiliza programación genérica mediante el uso de plantillas, de forma similar a la STL. De este modo, se dota de genericidad y eficiencia a todas las herramientas que proporciona Ventajas De los principios en los que se fundamenta la librería se derivan toda una serie de beneficios, algunos de las cuales ya se han mencionado en la sección anterior. A continuación se detallan las ventajas más importantes: Correspondencia entre paralelismo y recursos disponibles Cuando trabajamos con una tecnología basada en hilos debemos mapear los hilos lógicos en hilos físicos. Si el número de hilos lógicos es inferior al de hilos físicos estaremos desaprovechando recursos. Si es superior estaremos provocando continuos cambios de contexto que lastrarán el rendimiento. El rendimiento es óptimo cuando existe un hilo lógico por cada hilo físico. Intel TBB puede guardar esta correspondencia 1 Robo de tareas. 22

45 de forma escalable ya que es la propia librería la que maneja el número de hilos y reparte las tareas entre ellos manteniéndolos ocupados todo el tiempo. Inicio y terminación de tareas poco costoso La creación y destrucción de hilos es pesada en comparación con la creación y destrucción de tareas. En sistemas Linux crear o destruir una tarea es en torno a 18 veces más rápido, y en sistemas Windows llega a ser hasta 100 veces más rápido [8]. Por tanto, es más eficiente dividir el trabajo en múltiples tareas que en múltiples hilos, pues su gestión es mucho menos costosa. Para ello Intel TBB no crea y destruye hilos constantemente. Éstos se crean al principio y luego se reparten las tareas a ejecutar entre los hilos creados. Orden de ejecución de tareas eficiente La política que rige el orden de ejecución de los hilos suele ser del tipo roundrobin [3]. Ésta es una política que reparte el tiempo de ejecución de forma equitativa ya que no dispone de información sobre qué clase de tarea están ejecutando los hilos. El gestor de tareas de la librería Intel TBB cuenta con información de más alto nivel que el planificador del sistema y aprovecha esta información para ejecutar las tareas en un orden más eficiente en tiempo y espacio. Para ello ejecuta aquellas tareas que aprovechan mejor los datos contenidos en la memoria caché, y ahorra espacio ejecutando tareas que generan otras tareas nuevas sólo cuando sea necesario. Balanceo de carga mejorado Al fomentar el uso de tareas que son divisibles recursivamente, es posible generar tantas tareas como sea necesario para balancear bien la carga de trabajo entre los hilos de ejecución. El reparto de tareas se lleva a cabo utilizando el ya mencionado task stealing, de forma que se consigue un balanceo dinámico que minimiza el número de hilos ociosos. Alto nivel de abstracción La mayor ventaja de la orientación a tareas de la librería Intel TBB proviene del nivel de abstracción que proporciona. Trabajar con tareas permite razonar acerca del 23

46 paralelismo intrínseco en un algoritmo dado con mayor independencia de los detalles de implementación Contenidos de la librería La librería Intel TBB proporciona una serie de herramientas para la paralelización de diferente complejidad y nivel de abstracción. Entre ellas encontramos plantillas de funciones de paralelización, plantillas de contenedores paralelos, mecanismos de sincronización de bajo nivel y el gestor de tareas. Las plantillas proporcionan herramientas de paralelización a alto nivel que cubren las formas más comunes de paralelismo, mientras que los mecanismos de sincronización y el gestor de tareas permiten gestionar el paralelismo a un nivel más bajo en casos más concretos Inicialización de la librería Antes de usar la librería debemos inicializar el gestor de tareas. Para ello es necesario declarar un objeto de tipo tbb::task_scheduler_init 2 cuyo constructor lleva a cabo la inicialización. El destructor por su parte terminará el gestor de tareas. No obstante, puede llevarse a cabo una terminación anticipada invocando el método terminate() sobre el objeto de inicialización declarado. Una vez inicializada la librería no constituye un error declarar más objetos de este tipo. Si hay varios objetos task_scheduler_init, la terminación de la librería se llevará a cabo cuando sea invocado el destructor del último objeto. El constructor de task_scheduler_init admite como parámetro el número de hilos de ejecución con el que se inicializará la librería. Si no se le pasa ningún parámetro se dejará que la propia librería escoja el número adecuado de hilos. Ésto es equivalente a pasar el valor task_scheduler_init::automatic. También es posible pasar el valor task_scheduler_init::deferred que permite especificar más adelante el número de hilos deseados, invocando al método initialize() sobre el objeto de inicialización. 2 Los componentes de la librería se definen bajo el espacio de nombres tbb. En adelante se omite por brevedad. 24

47 Rangos Un rango define un espacio de iteraciones, o más genéricamente, un problema computacional que puede ser dividido recursivamente. Se proporciona un rango por defecto denominado blocked_range<t> que representa un espacio de iteración unidimensional sobre el tipo T, pero podemos definir nuestros propios rangos. Un rango es una clase que proporciona un constructor de particionado y los siguientes métodos: empty(): Determina si el rango está o no vacío. is_divisible(): Determina si el rango es divisible en dos subrangos no vacíos. Para dividir un rango en dos subrangos se utiliza el constructor de particionado. Éste recibe como primer parámetro el rango a dividir. El constructor devuelve un nuevo rango que se corresponderá con la segunda mitad del rango recibido como primer parámetro, el cual será actualizado para representar tan sólo la primera mitad de su valor original. Como segundo parámetro el constructor recibe el valor split, que simplemente sirve para diferenciarlo de un constructor copia ordinario. El constructor para blocked_range<t> recibe tres parámetros. Los valores de inicio y fin del rango, y un tercer parámetro que determina el tamaño de grano. Si no se especifica ninguno, el tamaño de grano tomará el valor uno. El tamaño de grano especifica un número de iteraciones razonable para construir un bloque de procesamiento (tarea) que será asignado a un hilo de ejecución. El rango inicial se dividirá recursivamente hasta alcanzar un tamaño menor o igual que el tamaño de grano. Si el tamaño de grano es excesivamente pequeño, el rango inicial se subdividirá en un número demasiado elevado de subrangos que habrá que procesar por separado. Ésto conlleva una gran sobrecarga en la gestión de un excesivo número de subtareas. Si el tamaño de grano es excesivamente grande, se dispondrá de un número insuficiente de subrangos para asignar a los diferentes hilos de ejecución, con lo que perderemos capacidad de paralelización o escalabilidad en la misma. La determinación de un tamaño de grano apropiado es una tarea crítica y para ello Intel TBB proporciona diferentes políticas de particionado de rangos: simple_partitioner: El particionado se realizará teniendo en cuenta el 25

48 tamaño de grano especificado. Éste constituye un umbral máximo para el tamaño de los subrangos generados. auto_partitioner: El tamaño de grano se determinará automáticamente en base a heurísticas internas de la biblioteca. affinity_partitioner: Como en el caso anterior, determina el tamaño de grano automáticamente utilizando una serie de heurísticas, pero además intenta aprovechar mejor los recursos caché asignando siempre los mismos subrangos a los mismos procesadores con el fin de utilizar los datos ya cargados en las cachés. Además, un rango blocked_range<t> debe poder ser recorrido. Para ello se define un iterador sobre el mismo con inicio determinado por begin(), final determinado por end(), y paso definido mediante el operador incremento, operator++(). Los rangos permiten especificar los espacios de iteraciones asignados a las tareas. En determinadas ocasiones, dividir una tarea en dos se puede traducir en dividir su rango de iteración y aplicar el trabajo definido por la tarea a cada uno de los subrangos Algoritmos Intel TBB ofrece una serie de plantillas de funciones para los patrones de paralelismo más comúnmente utilizados. Aunque también sea posible utilizar directamente el gestor de tareas, de forma que podamos crear nuestras propias plantillas o diseñar nuestra estructura de tareas directamente, estas funciones simplifican el proceso de paralelización enormemente y es recomendable usarlas siempre que se pueda. parallel_for La plantilla de función parallel_for nos permite romper un problema computacional expresado por un rango en subproblemas más pequeños que pueden ser procesados por separado. Su utilidad principal es la paralelización de bucles en los que las iteraciones son independientes entre sí. La función parallel_for requiere tres parámetros. El primero es un rango que representa el espacio de iteración o problema inicial a resolver. El segundo un functor 3 3 Objeto que se puede emplear en sustitución de una función. 26

49 que representa las computaciones llevadas a cabo en cada subespacio. El tercero es la política de particionado que se empleará en la división del rango de entrada. Si no se especifica esta última se tomará por defecto el valor simple_partitioner. Para paralelizar un determinado problema debemos definir una clase functor que redefina la función operator(). Ésta debe implementar las computaciones realizadas dentro de un determinado subrango del problema que se recibirá como único parámetro. El functor debe almacenar todos los operandos necesarios para el procesado. Éstos se pasarán generalmente como parámetros del constructor y se almacenarán como atributos del objeto functor. La función parallel_for se encarga de dividir el rango inicial y los que se vayan generando según convenga y a cada uno aplicarle las operaciones definidas por el functor. Puede ser necesario hacer copias del functor para aplicarlas sobre los subrangos del problema original. Por lo tanto, la clase del functor debe proporcionar un constructor copia que inicialice correctamente los atributos que representan los operandos. En general, el constructor copia definido por defecto será válido. La operación operator() debe ser constante, ya que un cambio en un objeto functor dejaría desactualizadas el resto de sus copias. parallel_reduce Las operaciones de reducción paralelizables se pueden llevar a cabo utilizando la plantilla de función parallel_reduce. El funcionamiento es parecido al caso de parallel_for. Ambas reciben los mismos tres parámetros, salvo que, en esta ocasión, los functores que recorren cada rango, que expresa un subproblema procesable en paralelo, deben recopilar los resultados parciales de sus subespacios y después reunirlos en un resultado global. Para ello la clase del functor debe almacenar, además de los operandos necesarios, los resultados parciales obtenidos. Éstos deben ser accesibles para que puedan ser leídos al final del procesamiento en el functor pasado como parámetro. Igual que en el caso de parallel_for, un functor puede ser dividido en dos con el fin de ser aplicado a cada uno de los subrangos obtenidos después de una división de rango. Cada uno de ellos debe tener los resultados parciales inicializados convenientemente. Por ello la clase del functor debe proporcionar un constructor de particionado que inicialice convenientemente estos atributos. Este constructor recibe como primer parámetro el functor original y como segundo el valor split para distinguirlo de un constructor 27

50 copia convencional. Cuando cada uno de los functores ha recorrido su subrango obteniendo sus resultados parciales acumulados, éstos deben ser integrados en un resultado global. Para ello la clase functor proporciona un método join() que reduce los resultados parciales de dos functores. Éste método recibe como parámetro el functor que se ha de integrar con el functor sobre el que se invoca el mismo. De esta forma, se pueden acumular los resultados parciales obtenidos en el functor sobre el que se invoca el método join(). Por lo tanto, la ejecución de un parallel_reduce provoca la división de las tareas en subtareas 4, la ejecución de las subtareas y la integración de los resultados parciales en un único resultado global. parallel_do La plantilla de función parallel_do hace el papel de un parallel_for en el que la extensión del problema a paralelizar no es conocida a priori. Un ejemplo sería el procesamiento de una lista enlazada en la que cada nodo puede procesarse en paralelo pero en la cual no conocemos la longitud de la misma. Los elementos de la lista se extraen uno a uno y se asignan a un hilo de ejecución para su procesamiento hasta alcanzar el final de la misma. La función parallel_do toma tres parámetros. El primero es un iterador que marca el principio del espacio de iteraciones. El segundo es el iterador que marca el final del mismo espacio. No trabajamos sobre un rango definido ya que el espacio de iteraciones es desconocido a priori. Sólo conocemos su principio, final y cómo avanzar al elemento siguiente. El tercer parámetro es el functor que implementa la computación requerida para cada elemento del espacio de iteración. En este caso, la clase functor definirá el operador operator() de forma que procese un único elemento de los recorridos por el iterador y que recibirá como su único parámetro. Sin embargo, ésto tiene el problema inherente de que el acceso a los elementos es secuencial, lo que constituye un cuello de botella importante si luego el procesamiento de cada uno de los elementos no contiene la cantidad suficiente de computación. Incluso si es suficiente, siempre tendremos un problema de falta de escalabilidad. Por grande que sea la cantidad de computación de cada elemento del espacio de iteración, siempre es teóricamente posible añadir más procesadores de forma que la extracción serializada de trabajo del iterador no consiga alimentarlos a todos. 4 Dividiendo los rangos y los functores implicados. 28

51 Figura 3.1: Etapas de procesamiento en un pipeline Se pueden evitar los problemas causados por el cuello de botella del acceso secuencial a los elementos del espacio de iteración utilizando dos posibles soluciones. La primera y más sencilla consiste en utilizar iteradores de acceso aleatorio. La segunda solución usa un segundo argumento feeder, de tipo parallel_do<item>& en la función operator() del functor, que permite añadir nuevo trabajo de forma dinámica. Así, por ejemplo, si se tratara de procesar una estructura de tipo árbol, en el procesamiento de cada uno de los nodos se podría añadir como nuevo trabajo el procesamiento de cada uno de sus hijos usando feeder.add(nodo_hijo). Así se conseguiría generar trabajo de forma escalable. Una instancia de parallel_do no termina su ejecución hasta procesar todos los ítems añadidos y alcanzar el final de la secuencia inicial de ítems a procesar. pipeline Mediante las clases pipeline y filter se implementa el patrón de paralelismo pipeline 5. Este patrón imita a las cadenas de montaje. El procesamiento de cada elemento se divide en una serie de etapas de procesamiento por las que el elemento va pasando (ver Figura 3.1). Las etapas están ordenadas consecutivamente y en cada una de ellas se lleva a cabo una parte del proceso global. Algunas etapas requieren que los elementos sean procesados uno a uno y en orden. Otras pueden ser paralelizadas, procesándose varios elementos al mismo tiempo. Las distintas etapas procesando distintos elementos deben ser independientes entre sí y, por lo tanto, son paralelizables. Los objetos de la clase pipeline actúan como contenedores de etapas de procesamiento o filtros. Para añadir etapas a un objeto pipeline se utiliza el método add_filter() del mismo, pasando como parámetro la etapa que se desea añadir. 5 Literalmente, cauce o tubería. 29

52 Una vez añadidas todas las etapas, en el orden el que se deben ejecutar, se invoca el método run() para ejecutarlas. Este método recibe como parámetro el número máximo de elementos que pueden estar en fase de procesamiento al mismo tiempo. Si este número es excesivamente bajo limitará nuestra capacidad de paralelismo, mientras que si es muy alto puede llevar a un excesivo consumo de recursos. Antes de destruir las etapas, éstas deben borrarse del pipeline. Para ésto se dispone del método clear(), que borra todos los filtros contenidos en el pipeline. Las clases que representan las etapas de procesamiento o filtros deben heredar de la clase filter. El constructor de la etapa debe inicializar los operandos necesarios e invocar al constructor de la superclase con uno de los valores serial o parallel. El primero indica que la etapa es serie, por lo que los elementos se procesan en orden y de uno en uno. El segundo indica que la etapa es paralela, por lo que es posible simultanear el procesamiento de varios elementos al mismo tiempo. El número de éstos no podrá rebasar nunca el número determinado con el parámetro del método run() visto anteriormente. También se debe sobreescribir el método virtual operator() de la superclase. Éste es el método invocado para procesar cada uno de los elementos. El elemento a ser procesado se recibe como parámetro mediante un puntero de tipo void*. Por su parte, el método debe devolver un puntero al objeto que se procesará en la etapa siguiente. La primera etapa de procesamiento recibe un puntero vacío, ya que es la etapa que debe generar el flujo de elementos. El valor devuelto por la última etapa se ignora ya que no hay más etapas que lo puedan procesar. El caudal 6 obtenido está limitado por dos factores. Uno es el número máximo de elementos procesados simultáneamente que establecimos anteriormente. El otro es el caudal de la etapa más lenta en la línea de procesamiento. Esta etapa constituirá el cuello de botella que determine el caudal global. Es necesario, por lo tanto, mantener las etapas secuenciales ligeras, desplazando el trabajo a las paralelas en la medida de lo posible. El tamaño de los elementos también puede afectar al caudal. Si los elementos son excesivamente pequeños la sobrecarga por la gestión de los mismos puede ser muy alta. Si los elementos son muy grandes, puede que se produzcan una gran cantidad de fallos caché ralentizando el procesado. Es recomendable escoger el tamaño de elemento más grande posible que permita aprovechar la memoria caché. 6 Número de elementos procesados por unidad de tiempo. 30

53 Contenedores En términos generales, los contenedores proporcionados por la STL de C++ no permiten actualizaciones concurrentes ya que pueden corromper el estado del contenedor. No obstante, se pueden utilizar usando mecanismos de exclusión mutua en el acceso, de forma que sólo un hilo de ejecución pueda acceder a los elementos del contenedor en un momento dado. Ésto supone secuencializar los accesos al contenedor, desaprovechando en gran medida el paralelismo disponible, con la consiguiente pérdida de rendimiento. Intel TBB proporciona contenedores que permiten accesos y actualizaciones concurrentes de forma eficiente. Para ello se utilizan bloqueos de grano fino o algoritmos no bloqueantes. Los bloqueos de grano fino utilizan bloqueos sobre pequeñas porciones del contenedor dejando disponible el resto para otros posibles accesos. En el caso de los algoritmos no bloqueantes, se usan hilos que corrigen los efectos de otros hilos que interfieran en su operación. Estas técnicas hacen que los contenedores proporcionados en la librería Intel TBB sean menos eficientes en general que los contenedores de la STL. Su uso está justificado cuando esta pérdida de rendimiento se compensa con el aumento del paralelismo. concurrent_hash_map La clase concurrent_hash_map<key, T, HashComp> representa una tabla de dispersión que permite accesos concurrentes a sus elementos. La tabla mapea valores de tipo Key en valores de tipo T, comportándose en realidad como un contenedor de elementos de tipo std::pair<const Key, T>. La clase HashComp define una función de dispersión para los valores de tipo Key y establece un criterio de comparación entre ellos. Para hacer ésto HashComp debe definir los métodos hash() y equal(). El método hash() toma un valor constante de tipo Key y devuelve un valor de tipo size_t resultado de calcular la función de dispersión para el valor recibido como parámetro. El método equal() recibe dos parámetros de tipo Key y devuelve un valor booleano que tomará el valor cierto en el caso de que ambos valores se consideren iguales según el criterio de igualdad empleado, y falso en caso contrario. Ambos métodos están relacionados, ya que si dos valores se consideran iguales, su función de dispersión debe devolver el mismo resultado. Lo contrario no tiene por qué ser cierto. En general los métodos de HashComp serán métodos de 31

54 clase (static) pero no tiene por qué ser así. Si el comportamiento de los métodos debe cambiar en distintas instancias de HashComp debe proporcionarse la instancia concreta a utilizar en la tabla de dispersión como parámetro de su constructor. En los accesos a los elementos se utilizan bloqueos de grano fino a nivel de elemento. De esta forma se puede acceder simultáneamente a cualquiera de los elementos del contenedor sin problemas de concurrencia. Para ello el contenedor proporciona las clases const_accesor y accesor que actúan como punteros inteligentes y permiten acceder a los elementos del mismo en modo lectura y en modo escritura respectivamente. El modo lectura permite acceder al elemento a varios lectores al mismo tiempo, pero a ningún escritor, maximizando la concurrencia en los accesos incluso a nivel de elemento. En el modo escritura el acceso a un elemento es exclusivo. Los métodos del contenedor find() e insert() toman como primer parámetro un puntero inteligente como los descritos y como segundo parámetro un valor Key. Si existe algún elemento en el contenedor asociado a este valor, el puntero pasado como primer parámetro lo apuntará después de retornar el método. La principal diferencia entre estos métodos estriba en que insert() inserta el elemento en el contenedor si éste antes no estaba presente. Los objetos accesor y const_accesor usados como punteros llevan implícitos un bloqueo sobre el elemento accedido. Por lo tanto, es conveniente minimizar el tiempo de vida de estos objetos o invocar al método release() sobre los mismos para liberar los bloqueos lo antes posible. El método remove() elimina un elemento de la tabla y puede ser invocado concurrentemente con cualquier otro acceso al contenedor. Recibe como parámetro un valor de tipo Key que determina el elemento a eliminar. Antes de eliminar el elemento se finalizarán los accesos pendientes al mismo. concurrent_vector La clase concurrent_vector<t> implementa un array dinámico de elementos de tipo T. El vector puede crecer mientras otros hilos acceden a sus elementos, e incluso dos o más hilos pueden realizar operaciones de crecimiento simultáneas. Dos métodos permiten hacer crecer al vector dinámicamente. El método grow_by() hace crecer al vector por el final tantas posiciones como el número que se le pasa como parámetro. Devuelve el índice del primer elemento de la sección de crecimiento. Las nuevas posiciones son inicializadas al valor T(). El método grow_to_at_least() hace crecer el vector hasta el tamaño del parámetro con el que se invoca si el vector es 32

55 de menor tamaño. Ambos métodos pueden ser invocados concurrentemente por varios hilos. El método size() devuelve el tamaño del vector, y éste puede incluir posiciones que están siendo creadas por los métodos de crecimiento. Es legal recorrer mediante un iterador un vector que está creciendo, sin embargo, es posible acceder a elementos que se estén construyendo concurrentemente. Por lo tanto, se debe sincronizar el acceso a los elementos con su construcción. concurrent_queue La clase concurrent_queue<t> implementa una cola concurrente de elementos de tipo T. Múltiples hilos pueden añadir y extraer elementos de la cola simultáneamente. El orden de los elementos insertados por diferentes hilos no está garantizado. Sólo se garantiza que si un mismo hilo inserta dos elementos y algún otro hilo extrae esos elementos éstos estarán en el orden correcto. Para introducir un elemento en la cola utilizaremos el método push(). Para extraer un elemento podemos usar pop() o pop_if_present(). El método pop() es bloqueante. Si se llama y la cola tiene algún elemento que extraer lo extraerá, si no, espera hasta que algún otro hilo introduzca un elemento para poder devolverlo al llamante. El método pop_if_present() termina exista o no un elemento en la cola. Si éste existe, lo devuelve, en caso contrario, no hace nada. El tamaño de la cola se define como un entero con signo. Puede tomar valores negativos si más hilos han intentado extraer elementos de la cola que los hilos que han introducido elementos. Así el tamaño se define como el número de operaciones de inserción iniciadas menos el número de operaciones de extracción en curso. El método size() devuelve el tamaño de la cola. Si el valor de éste toma valores no positivos el método empty() devolverá cierto indicando que la cola está vacía. Las colas no están limitadas en tamaño inicialmente pero se puede establecer un tamaño máximo usando set_capacity(). En este caso si la cola está llena el método push() bloqueará la ejecución hasta que quede sitio libre en la cola para añadir el elemento. 33

56 Exclusión mutua Aunque la librería Intel TBB ha sido diseñada de forma que se minimice el uso de bloqueos en la sincronización de tareas, éstos siguen siendo necesarios cuando existen accesos de escritura a variables compartidas entre varias tareas. Por lo tanto, se ofrecen como parte de la librería mecanismos de exclusión mutua implementados mediante cerrojos y bloqueos 7. Un cerrojo es un objeto sobre el que se pueden definir bloqueos. En cada momento sólo puede haber un bloqueo activo sobre un determinado cerrojo. Antes de que un hilo de ejecución pueda acceder a una sección crítica, deberá solicitar la activación de un bloqueo sobre el cerrojo que proteje su acceso. Si ya hay un bloqueo activo sobre el cerrojo, el hilo tendrá que esperar a que se libere. Tipos de cerrojos Todos los tipos de cerrojos incluidos en la librería comparten una interfaz común, de forma que son fácilmente intercambiables entre sí. Los cerrojos pueden presentar diferentes características en su comportamiento según su implementación. Escoger el tipo de cerrojo más adecuado para cada situación puede tener fuertes implicaciones en el rendimiento. Se exponen a continuación algunas de las características que pueden describir el comportamiento de un cerrojo: Escalabilidad Un cerrojo es escalable si los hilos que mantiene en espera cuando se ha establecido un bloqueo no consumen demasiado tiempo del procesador o ancho de banda de la memoria. Los cerrojos escalables suelen ser más lentos en situaciones donde se den bloqueos breves. Equidad Cuando hay varios hilos de ejecución bloqueados en un cerrojo y éste queda libre, se puede desbloquear un hilo cualquiera o guardar el orden de llegada al cerrojo, desbloqueando el que más tiempo lleve en espera. Esta última táctica reparte equitativamente los accesos a la sección crítica entre los hilos y evita la inanición. Sin embargo, un cerrojo no equitativo puede ser más rápido ya que puede desbloquear un hilo que esté activo, mientras que uno equitativo sólo puede desbloquear al que le corresponde el turno, aunque éste pueda estar durmiendo. 7 Traducción libre de mutexes y locks 34

57 Recursividad Un cerrojo puede soportar la recursividad, de forma que un hilo de ejecución que ya posee un bloqueo sobre ese cerrojo, puede activar otro bloqueo sobre el mismo. Ésto puede ser útil en la implementación de algunos algoritmos recursivos, pero añade sobrecargas en su uso. Comportamiento en espera Un hilo en espera puede mantener una espera activa o pasiva. En una espera activa, el hilo encuesta repetidamente al cerrojo sobre si éste ha sido liberado, si no es así, puede ceder el procesador temporalmente. En una espera pasiva el hilo se duerme hasta que el cerrojo quede libre. Las esperas activas funcionan bien con bloqueos cortos, las pasivas son más eficientes en el caso de bloqueos largos. En base a estas características, Intel TBB implementa varios tipos de cerrojo que se comportan de diferente manera respecto a cada una de ellas. Dependiendo de la situación en la que se requiera el uso de un cerrojo, un tipo puede ser más adecuado y eficiente que otro. A continuación, se enumeran y describen los tipos de cerrojos soportados: spin_mutex: No es ni escalable, ni equitativo, ni permite recursividad, e implementa espera activa. No obstante, es un cerrojo muy rápido y liviano que funciona bien con bloqueos breves. spin_rw_mutex: Funciona de la misma manera que el spin_mutex, pero añade a su comportamiento la posibilidad de establecer bloqueos de sólo lectura, de forma que pueden existir varios lectores simultáneos en una determinada sección crítica. queuing_mutex: Es escalable, equitativo, no permite recursividad e implementa espera activa. Es recomendable en situaciones donde la escalabilidad y la equidad sean características importantes. queuing_rw_mutex: Es equivalente al tipo de cerrojo queuing_mutex pero añade la posibilidad de establecer bloqueos de sólo lectura. mutex: Es una clase adaptadora a los cerrojos nativos del sistema. Por lo tanto, sus características son heredadas de éstos. Frente al uso directo de los cerrojos nativos, aporta un comportamiento seguro frente a las excepciones y una interfaz común a la del resto de los cerrojos soportados en la librería. 35

58 recursive_mutex: Al igual que mutex, es una clase adaptadora a los cerrojos nativos del sistema. En este caso, añade además la posibilidad de bloqueos recursivos sobre el mismo cerrojo. Uso de los cerrojos Los cerrojos se declaran mediante un constructor sin parámetros. Una vez declarado el cerrojo se puede establecer un bloqueo sobre el mismo. Todos los tipos de cerrojo ofrecen el tipo anidado scoped_lock que permite declarar un bloqueo sobre ese tipo de cerrojo. Estos bloqueos se denominan bloqueos de ámbito pues se declaran como variables y el bloqueo no puede sobrevivir al ámbito donde se declara la variable. El destructor de la misma se invoca al salir de su ámbito de declaración y con ello se elimina el bloqueo, tanto si estaba activo como si no. Ésto constituye un seguro frente al tratamiento de excepciones, ya que si salimos del ámbito del bloqueo porque se ha lanzado una excepción, éste será destruido evitando que queden bloqueos pendientes sin cancelar. Para activar un bloqueo se invoca sobre el mismo el método adquire() que toma como parámetro el cerrojo sobre el que se activa dicho bloqueo. Para liberarlo se hace uso del método release(). Se puede activar un bloqueo en el mismo momento de declararlo pasándole como parámetro a su constructor el cerrojo sobre el que se activará. El destructor siempre liberará el bloqueo si éste estuviera activado. El tiempo de bloqueo sobre un cerrojo se debe minimizar. Para ello se suele declarar el bloqueo en una localidad lo más reducida posible, de forma que al salir se invoque su destructor y se libere el bloqueo. Los bloqueos declarados sobre cerrojos que admiten bloqueos de sólo lectura utilizan un segundo parámetro en la activación del bloqueo. Éste es un valor booleano que establece si el bloqueo es de lectura/escritura o sólo de lectura. Si se requiere acceso de escritura, el parámetro debe tomar el valor true, siendo éste el valor por defecto, mientras que el valor false indicará un acceso de sólo lectura. Es posible que una vez activado un bloqueo con un determinado modo de acceso, se quiera cambiar de modo. Para ello, se dispone de los metodos upgrade_to_writer y downgrade_to_reader, que cambian a modo escritura o sólo lectura respectivamente. Ambos devuelven un valor booleano que establece si el bloqueo ha sido liberado temporalmente en la transición de estado con el fin de evitar interbloqueos. 36

59 Gestor de tareas Cuando se trabaja con un patrón de paralelismo que no se ajusta a los soportados por las plantillas de la librería ( 3.2.3), se puede hacer uso del gestor de tareas para llevar a cabo la paralelización. El gestor de tareas es más general que las plantillas. A cambio es mucho más complejo, por lo que siempre que sea posible es recomendable usar las plantillas proporcionadas en la librería. Éstas han sido implementadas usando el gestor de tareas y, en base al mismo, es posible elaborar nuevas plantillas tan potentes como las originales que soporten patrones de paralelismo alternativos. El gestor de tareas trabaja directamente con tareas. Una tarea es un objeto de una clase que hereda de la clase base task. El código que ejecutará la tarea se incluye en su función execute(). Ésta función no recibe ningún parámetro y devuelve un puntero a la tarea que se debe ejecutar a continuación o un puntero nulo. De ser necesario pasar algún parámetro a la ejecución de la tarea, éstos se deben pasar en el constructor de la misma para ser almacenados en los atributos del objeto. Una tarea puede declarar nuevas tareas dentro de su método execute(). De esta forma se introduce un patrón recursivo de creación de tareas que permite generar una gran cantidad de paralelismo potencial de una forma muy eficiente. Las tareas se generan siguiendo una estructura arborescente en la que una tarea madre genera tareas hijas, y éstas, a su vez, generan sus propias tareas hijas. El gestor de tareas maneja esta estructura mediante un grafo de tareas. Cada nodo del grafo tiene un puntero a su tarea madre, un contador del número de tareas hijas y una profundidad que representa la distancia al nodo raíz, como se muestra en la Figura 3.2. Para declarar una nueva tarea se debe utilizar un new redefinido, proporcionado por la librería, que permitirá reutilizar el espacio de memoria eficientemente cuando la tarea concluya. Este new recibe como parámetro el valor de retorno de uno de estos cuatro métodos implementados en la clase task: allocate_root(): Indica que se ha de reservar espacio para la tarea raíz del grafo. Es un método de clase. allocate_child(): Permite declarar tareas hijas de una tarea dada sobre la que se invoca, siempre que no se haya lanzado a ejecución alguna otra tarea hija. allocate_additional_clid_of(): Permite declarar tareas hijas de una determinada tarea cuando ya existen otras tareas hijas en ejecución. 37

60 Figura 3.2: Árbol de tareas allocate_continuation(): Reserva espacio para una tarea que sustituirá a todos los efectos a una dada en el grafo de tareas. Antes de lanzar a ejecución las tareas hijas, se debe establecer el contador de referencias de la tarea madre al valor adecuado. Para ello la clase task nos provee del método set_ref_count(). Una vez establecido, el contador de referencias se decrementa automáticamente en uno cuando una tarea hija concluye su ejecución. Si en algún momento este valor llega a cero la tarea madre se lanzará a ejecución. Así se puede utilizar este valor como un disparador que ejecuta la tarea cuando todas sus hijas han concluido. En determinados contextos ésto puede resultar no deseable. En estos casos se fija el contador de referencias a un valor igual al número de tareas hijas más uno. Una vez declarada una tarea, y fijado el contador de referencias de su tarea madre en caso de existir, se puede lanzar su ejecución. Se dispone de tres métodos en la clase task para indicarle al gestor de tareas que una tarea está preparada para ser ejecutada: spawn_root_and_wait(): Lanza la tarea raíz y espera a que finalice su ejecución. Es un método de clase. spawn(): Lanza una tarea hija y devuelve inmediatamente. 38

61 spawn_and_wait_for_all(): Lanza una tarea hija y espera a que todas las tareas hijas se terminen de ejecutar. En términos generales podemos decir que las tareas madre están esperando a que se ejecuten sus tareas hijas, sea porque las han lanzado y esperan su finalización mediante spawn_and_wait_for_all() o porque su contador de referencias deba llegar a cero para ejecutarse. Son, por lo tanto, los nodos hoja del grafo de tareas los que están preparados para ser ejecutados. Planificación El gestor de tareas planifica la ejecución de las tareas de forma que se minimice el consumo de memoria, se maximice el aprovechamiento de los datos cargados en las cachés y se genere el suficiente paralelismo para utilizar los recursos disponibles. Si se escoge ejecutar las tareas recorriendo el grafo de tareas en anchura, teniendo en cuenta que cada tarea puede generar nuevas tareas, el crecimiento en el número de tareas generadas será exponencial, aumentando el paralelismo potencial y el consumo de memoria. Un recorrido en profundidad ahorra memoria y prioriza la ejecución de las tareas recién creadas con el consiguiente aprovechamiento de las cachés. El gestor de tareas mantiene un equilibrio entre el recorrido en profundidad y en anchura para generar el suficiente paralelismo sin consumir un exceso de recursos. Para ello realiza una ejecución primero en anchura, para generar suficiente paralelismo, y después en profundidad, haciendo más eficiente la ejecución secuencial dentro de cada hilo de ejecución. Para implementar este modus operandi el gestor asigna a cada hilo de ejecución un repositorio de tareas. Los repositorios consisten en un vector de pilas de tareas, en las que las tareas listas para ejecutar se asignan a las pilas según su profundidad. En la Figura 3.3 se esquematiza la estructura de un repositorio. Cuando una tarea se declara lista para ejecutarse se manda al repositorio correspondiente del hilo que la generó. Si un hilo solicita una tarea para ejecutar, el gestor de tareas primero intentará asignarle la tarea devuelta en el método execute() de la tarea que acaba de finalizar. Si no hubiera una tarea anterior o ésta hubiera devuelto un puntero nulo, se asignará la tarea más reciente de la pila de mayor profundidad de su repositorio. Si su repositorio está vacío se intentará asignar una tarea del repositorio de otro hilo 8. En este caso se 8 Esta técnica se conoce como task stealing o robo de tareas. 39

62 Figura 3.3: Repositorio de tareas. toma la tarea más reciente de la pila de menor profundidad. Técnicas de programación El rendimiento del gestor de tareas se puede mejorar en gran medida utilizando en lo posible la serie de técnicas de programación que se detallan a continuación. Reacción en cadena por recursividad El gestor de tareas trabaja mejor con grafos de tareas estructurados en forma de árbol en los que cada tarea crea nuevas tareas hijas. Es así cuando la técnica de recorrer el grafo primero en anchura y después en profundidad funciona mejor. Así mismo, de esta forma se favorece la creación de un gran número de tareas en poco tiempo. Crear N tareas mediante una tarea maestra requiere O(N) pasos, mientras que con una táctica recursiva sólo requeriría O(logN) pasos. Tarea de continuación El uso de spawn_and_wait_for_all() puede provocar algunas ineficiencias al detener la ejecución de la tarea madre hasta la finalización de sus tareas hijas. Para evitarlo, la tarea madre puede declarar una nueva tarea llamada de continuación mediante el método allocate_continuation() anteriormente presentado. Esta tarea reemplazará a la tarea madre en el grafo de tareas, teniendo su misma profundidad y tarea madre. Las tareas que antes se declaraban como hijas ahora se han de declarar hijas de la tarea de continuación. El contador de referencias de esta última se fijará a un valor igual a su número de hijas, de forma que cuando éstas terminen se dispare su ejecución. La tarea de continuación realizará el trabajo que antes se hacía en la tarea madre después de que finalizaran sus hijas. Si no existe trabajo 40

63 pendiente, se puede usar la tarea vacía empty_task que no hace ningún trabajo real excepto esperar a que las tareas hijas concluyan. De esta forma se evita que ninguna tarea tenga que esperar por sus tareas hijas una vez lanzada su ejecución. Atajo en la planificación Es posible decirle al gestor de tareas cual va a ser la siguiente tarea a ejecutar después de finalizar la actual. Para ello, se debe devolver un puntero a la misma al finalizar la ejecución del método execute(). De esta forma, se evita el paso de lanzar la tarea a ejecución mediante spawn para que el gestor la introduzca en el repositorio de tareas y justo después la vuelva a sacar para su ejecución. En muchas ocasiones surge la oportunidad de utilizar esta técnica al hacer uso de la tarea de continuación. La tarea original, al no esperar por la ejecución de sus tareas hijas, puede sugerirle al gestor cual de ellas debe ejecutar al final de su propia ejecución. Reciclado Se puede evitar, en cierta medida, los pasos de reserva y liberación de memoria cuando una tarea se declara o termina. Para ello, se reutiliza o recicla el espacio de memoria de una tarea cuando concluye para contener una nueva tarea. Las tareas, en general, sólo difieren en el valor de sus parámetros y en su posición en el grafo de tareas. Por lo tanto, si se configuran éstos adecuadamente al término de la ejecución de una tarea, la misma tarea puede cumplir el papel de otra nueva. Dependiendo de si la tarea se recicla como tarea de continuación o como tarea hija, se usarán respectivamente las funciones recycle_as_child_of() o recycle_as_continuation(). La primera toma como parámetro la tarea de la que pasará a ser hija la tarea reciclada, fijando la profundidad al valor adecuado. Ambas funciones evitan que la tarea se destruya automáticamente al concluir su ejecución. Copia perezosa En ocasiones, las tareas usan estructuras de datos que son costosas de replicar para cada una de las tareas. Es común que sólo sea estrictamente necesaria su replicación cuando dos tareas acceden simultáneamente a dicha estructura. En estos casos, no interesa replicar la estructura si la ejecución de una tarea se produce en el mismo hilo al que está asignada su tarea madre. O lo que es equivalente, interesa que sólo se replique la estructura en el caso de que la tarea haya sido robada por otro hilo. La función is_stolen_task() informa de si la tarea sobre la que se invoca se está ejecutando en el mismo hilo que su tarea dependiente (su madre en el grafo de tareas). 41

64 De esta forma, se dispone de un mecanismo para establecer si es necesario replicar una determinada estructura de datos Un ejemplo sencillo de uso En esta sección se muestra un ejemplo de uso de la librería Intel TBB y un estudio pormenorizado del rendimiento que se obtiene con su uso. Con este fin, se ha desarrollado una pequeña librería matemática, se ha paralelizado usando la librería Intel TBB y se ha comprobado el rendimiento obtenido. La librería desarrollada implementa un tipo matriz con las operaciones de suma y multiplicación. Realizar una suma de matrices necesita dos bucles anidados. Uno de ellos recorre las filas de los sumandos y otro las columnas. Cada elemento de una matriz se suma con el elemento correspondiente de la otra y éste valor será almacenado en la posición correspondiente de la matriz resultado. En el Listado 3.1 se muestra el código que implementa esta función. Paralelizar esta operación requiere dividir la operación suma de matrices en subtareas. Una posible estrategia consiste en que cada una de las subtareas se corresponda con la suma de una o varias de las filas de las matrices. Ésto proporciona un buen grado de paralelismo en la mayoría de los casos. Para ellos, se puede utilizar la plantilla parallel_for() con el fin de paralelizar el bucle exterior de la suma original. La función parallel_for() divide el espacio de iteración del bucle exterior en subespacios que pueden ser iterados independientemente. Las operaciones a realizar en cada uno de los subespacios se empaquetan en un functor. En el Listado 3.2 se muestra el código de la suma de matrices ya paralelizado. El código de esta operación se reduce ahora a invocar la función parallel_for() (línea 3) con los parámetros apropiados. El primer parámetro representa el espacio de iteración que queremos subdividir definido por su principio, fin y el tamaño de grano. El espacio de iteración comienza en el índice de la primera fila de la matriz y termina en el índice de la última fila más uno 9, definido por el número de filas de la matriz _row. El tamaño de grano _gsz se define en algún otro sitio y especifica el tamaño máximo de un subintervalo. El segundo parámetro es el functor que realizará el trabajo en cada una de las iteraciones de los subintervalos. El tercer parámetro lo constituye el particionador a utilizar ( 3.2.2) y se define por defecto al valor simple_partitioner. 9 Intel TBB define los intervalos como abiertos por la derecha al estilo de la STL. 42

65 1 template < c l a s s T> 2 matrix <T>& matrix <T > : : operator +=( c o n s t matrix <T>& a ) { 3 f o r ( s i z e _ t i =0; i <_row ; i ++) { 4 f o r ( s i z e _ t j =0; j < _ c o l ; j ++) 5 _mat [ i ] [ j ]+= a. _mat [ i ] [ j ] ; 6 return t h i s ; 7 } Listado 3.1: Función suma de matrices 1 template < c l a s s T> 2 matrix <T>& matrix <T > : : operator +=( c o n s t matrix <T>& a ) { 3 p a r a l l e l _ f o r ( b l o c k e d _ r a n g e < s i z e _ t >(0, _row, _gsz ), plus <T>( t h i s, a ) ) ; 4 return t h i s ; 5 } Listado 3.2: Función suma de matrices paralelizada Éste divide el rango inicial hasta obtener subrangos de un tamaño menor o igual al tamaño de grano. También se han realizado otras versiones con distintos particionadores con el fin de establecer comparativas. La clase que implementa el functor se define en el Listado 3.3. El functor tiene que guardar en sus atributos los parámetros necesarios para realizar el trabajo definido en el bucle original. En este caso se almacenan punteros a las matrices que participan en la suma inicializados en el constructor (líneas 3 y 4). El functor también debe definir el operador operator() (líneas 8-12). Este método es el que define la computación a realizar y se invoca sobre cada uno de los subrangos obtenidos al particionar el rango origen. El único parámetro que recibe este operador es precisamente el subrango sobre el que trabajará. En este caso se representan los subrangos mediante la clase blocked_range proporcionada por la librería. Para cada uno de los subrangos, que aquí representan rangos de filas de las matrices a sumar, se realizan las operaciones que se realizaban en el bucle exterior de la función original. Así, recorriendo las filas desde r.begin() hasta r.end() (líneas 9-11), se suman entre sí los elementos de las matrices mediante el bucle interno que itera sobre las columnas. Puede que dos subrangos se procesen en hilos diferentes. Para maximizar la localidad de los datos y aprovechar las cachés en la medida de lo posible, es probable que el gestor de tareas replique el functor inicial para cada uno de los hilos o subrangos. En este caso el constructor copia y el destructor definidos por el compilador por de- 43

66 1 template < c l a s s T> 2 c l a s s p l u s { 3 matrix <T> _m ; 4 c o n s t matrix <T> _a ; 5 p u b l i c : 6 p l u s ( matrix <T>& m, c o n s t matrix <T>& a ) : _m(&m), _a(&a ) {} 7 8 void operator ( ) ( c o n s t b l o c k e d _ r a n g e < s i z e _ t > & r ) c o n s t { 9 f o r ( s i z e _ t i = r. b e g i n ( ) ; i!= r. end ( ) ; i ++) 10 f o r ( s i z e _ t j =0; j <_m >c o l ( ) ; j ++) 11 ( _m ) ( i, j )+= ( _a ) ( i, j ) ; 12 } 13 } ; Listado 3.3: Functor para la paralelización fecto son suficientes para este tipo de propósitos. Si no fuera así es responsabilidad del programador proporcionar los constructores y destructores necesarios Estudio del rendimiento Para realizar las pruebas de rendimiento se han planteado tres escenarios diferentes en tres plataformas distintas. En el primero de ellos se compara el tiempo de ejecución de la operación definida utilizando la versión secuencial y la paralela con diferentes tamaños de matriz. En un segundo escenario se estudia la evolución de los tiempos de ejecución conforme varía el tamaño de grano. Por último, se comparan los tiempos de ejecución según se utilice un particionador afín o no para distintos tamaños de matriz. Las plataformas de ejecución utilizadas son: (1) un PC con procesador Intel R Core TM 2 Duo E GHz (2 núcleos), (2) un nodo del cluster nm [32] dotado de 2 Intel R Xeon TM dual-core 5060 a 3.20GHz (2x2 núcleos) y (3) un nodo del cluster nm dotado con 4 Intel R Xeon TM hexa-core E7450 a 2.40GHz (4x6 núcleos). Las tres plataformas pertenecen al Grupo de Arquitectura de Computadores (GAC) de la Universidade da Coruña. El compilador utilizado ha sido el gcc en el PC y gcc en el cluster. En ninguno de los dos casos se han utilizado flags de optimización. En el primer experimento se han tomado matrices cuadradas de diferentes tamaños y se han sumado repetidamente con el fin de estimar el tiempo de ejecución de la función suma en sus versiones paralela y secuencial. El tamaño de grano en la versión paralela se determina automáticamente mediante auto_partitioner ( 3.2.2). En el Cuadro 3.1 se muestran los tiempos de ejecución medidos en el PC multinúcleo con- 44

67 Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 0,69 0,44 0,50 0,68 0,76 Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 1,18 1,57 1,56 1,56 1,56 Cuadro 3.1: Tiempos de ejecución (seg.) - Intel R Core TM 2 Duo E GHz (2 núcleos) Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 0,37 0,83 1,41 1,82 1,99 Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 2,07 2,08 2,09 2,1 2,1 Cuadro 3.2: Tiempo de ejecución (seg.) - 2 Intel R Xeon TM dual-core 5060 a 3.20GHz (2x2 núcleos) Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 0,23 0,66 1,61 3,19 4,44 Tamaño Secuencial 0, , , , , Paralelo 0, , , , , Aceleración 4,44 6,28 8,42 7,72 6,42 Cuadro 3.3: Tiempo de ejecución (seg.) - 4 Intel R Xeon TM hexa-core E7450 a 2.40GHz (4x6 núcleos) 45

68 Aceleración 9 8 Sin aceleración Tamaño (filas) Figura 3.4: Aceleración - Intel R Core TM 2 Duo E GHz (2 núcleos) Aceleración 9 8 Sin aceleración Tamaño (filas) Figura 3.5: Aceleración - 2 Intel R Xeon TM dual-core 5060 a 3.20GHz (2x2 núcleos) Aceleración 9 8 Sin aceleración Tamaño (filas) Figura 3.6: Aceleración - 4 Intel R Xeon TM hexa-core E7450 a 2.40GHz (4x6 núcleos) 46

69 vencional y la aceleración respecto a la versión secuencial calculada a partir de estos tiempos (ver Figura 3.4). En cada columna de la tabla el número de filas y columnas se duplica, o lo que es lo mismo, el número de elementos se cuadriplica. Se puede apreciar que para tamaños menores a 512 por 512 elementos la versión serie de la operación es más rápida. Ésto es debido a las sobrecargas que supone la gestión de la paralelización. En tamaños superiores, la versión paralelizada se impone, alcanzando pronto aceleraciones próximas a 1,5 y eficiencias cercanas a 0,8 10. En el caso del nodo del cluster nm con 4 núcleos la evolución de los tiempos es muy parecida (ver Cuadro 3.2 y Figura 3.5). En tamaños reducidos de la matriz las sobrecargas de la gestión del paralelismo provocan que la versión serie se comporte mejor. En tamaños superiores a 64 por 64 la versión paralela consigue un mejor rendimiento. Se alcanzan rápidamente aceleraciones de 2,1 y se rondan eficiencias de 0,5. Es posible que la aceleración y la eficiencia se resientan por el hecho de que las computaciones a realizar por cada elemento son muy sencillas. Ésto provoca que los procesadores empleen poco tiempo en computar y mucho en acceder a memoria. Con todos los procesadores accediendo a memoria simultáneamente, el acceso a memoria se convierte en un cuello de botella, mayor cuantos más procesadores existan. Éste es un problema común cuando se dispone de muchos elementos a los que se aplican operaciones sencillas, lo cual puede lastrar la aceleración y la escalabilidad. En el Cuadro 3.3 y la Figura 3.6 se presentan los resultados obtenidos en el nodo del cluster nm con 24 núcleos. De nuevo, en tamaños reducidos de la matriz la versión paralela se comporta peor debido a las cargas de gestión de paralelismo, en este caso incluso peor que en los anteriores debido a que existe más paralelismo que gestionar. En tamaños superiores a 64 por 64 la versión paralela tiene un mayor rendimiento que la versión serie, llegando a alcanzar aceleraciones de 8,42. Sin embargo, el rendimiento es tan sólo de 0,35 en su máximo valor ya que en este caso el problema del cuello de botella en el acceso a memoria se agrava debido al gran número de procesadores que acceden concurrentemente a memoria. En este caso, la evolución de la aceleración es un tanto diferente a la observada en las plataformas estudiadas anteriormente. La aceleración obtenida mediante la paralelización crece con el tamaño de la matriz, pero en lugar de estabilizarse en torno a un valor, ésta alcanza un máximo con matrices de 2048 por 2048 elementos para después 10 La eficiencia es igual a la aceleración dividida entre el número de procesadores empleados en la paralelización. 47

70 Tiempo de ejecución (seg.) Tamaño de grano Figura 3.7: Tiempo de ejecución (seg.) frente a tamaño de grano - Intel R Core TM 2 Duo E GHz (2 núcleos) 5 Tiempo de ejecución (seg.) Tamaño de grano Figura 3.8: Tiempo de ejecución (seg.) frente a tamaño de grano - 2 Intel R Xeon TM dual-core 5060 a 3.20GHz (2x2 núcleos) decrecer. Este efecto es consecuencia del aprovechamiento de las memorias cachés con tamaños medios de la matriz. El uso de las cachés palía en parte el ahora muy notable problema del cuello de botella en el acceso a memoria, pero conforme las matrices se vuelven más grandes éstas no caben en la memoria caché y el problema se manifiesta de nuevo. En el segundo escenario planteado se estudia la evolución de los tiempos de computación al variar el tamaño de grano. Para ello se ha realizado la suma de dos matrices cuadradas de filas, tomando como tamaño de grano todas las potencias de 2 entre 1 y En la Figura 3.7 se muestran los tiempos de ejecución para el PC multinúcleo con dos procesadores. Se puede observar como el tiempo de ejecución permanece estable con los diferentes tamaños de grano, salvo para un tamaño igual 48

71 6 Tiempo de ejecución (seg.) Tamaño de grano Figura 3.9: Tiempo de ejecución (seg.) frente a tamaño de grano - 4 Intel R Xeon TM hexa-core E7450 a 2.40GHz (4x6 núcleos) a todo el espacio de iteración en el que se incrementa bruscamente. Con tamaños de grano iguales o inferiores a la mitad del espacio de interacción es posible dividir éste en por lo menos dos subespacios que mantendrán ocupados a ambos procesadores. Con un tamaño de grano igual a todo el espacio de iteración, el trabajo no se podrá dividir y un sólo procesador realizará todo el trabajo, dejando el otro procesador ocioso. De ahí que el tiempo de ejecución se duplique. En la Figura 3.8 se presentan los tiempos de ejecución obtenidos para el nodo del cluster con 4 núcleos. Los resultados muestran la misma tendencia. Se observa como cuando el tamaño de grano crece hasta alcanzar un tamaño igual a la mitad del espacio de iteración, el tiempo de ejecución se duplica ya que el espacio de iteración sólo se podrá dividir en dos mitades, ocupando dos de los cuatro procesadores disponibles. Cuando el tamaño de grano incluye todo el espacio de iteración el tiempo se cuadriplica con respecto a los valores obtenidos con tamaños de grano pequeños, ya que sólo un procesador de los cuatro se mantiene ocupado. Siguiendo este razonamiento cabría esperar otro comportamiento para los resultados obtenidos en el nodo del cluster con 24 núcleos (ver Figura 3.9). Con un tamaño de grano igual a todo el espacio de iteración sólo un procesador está activo. Sería esperable que cada vez que se divide el tamaño de grano entre dos, el número de subintervalos generados se multiplicara por dos, pudiendo mantener ocupados el doble de procesadores, y por lo tanto dividiendo el tiempo de procesado entre dos, hasta alcanzar un tamaño en el que se ocuparían los 24 núcleos del sistema, momento en el que reducciones mayores del tamaño de grano no implicarían mejores rendimientos. Sin 49

72 embargo, en este caso con tamaños de grano menores que 2048 no se obtienen mejores resultados, a pesar de que este tamaño de grano permite obtener tan sólo 8 subintervalos que serán procesados por otros tantos procesadores, dejando los 16 procesadores restantes ociosos. Ésto se debe a que, aunque tamaños de grano menores que 2048 permiten un mayor uso de los procesadores disponibles en el sistema, el uso de más procesadores no supone una mejora significativa en los tiempos de ejecución debido al problema del cuello de botella en el acceso a memoria que se manifiesta en este sistema y que ya se ha comentado. En los tres casos estudiados es destacable que sólo para tamaños de grano muy grandes el tiempo de ejecución varía notablemente, mientras que para todo el resto de tamaños apenas varía, incluso cuando el tamaño de grano es tan pequeño como uno. El motivo, es que habiendo paralelizado únicamente el bucle más externo de la función original, una de las iteraciones de ese bucle realiza el procesado de una fila completa. Si se hubiera paralelizado el bucle interno, utilizar un tamaño de grano excesivamente pequeño hubiera provocado que cada subintervalo de iteración incluyera tan sólo unos pocos elementos de la matriz, generando un gran número de subintervalos a procesar y gestionar por separado. Ésto provocaría una gran sobrecarga que dispararía los tiempos para tamaños de grano muy pequeños. No obstante, de las gráficas podemos deducir que existe un rango de valores muy amplio en el que el tamaño de grano funciona adecuadamente. El rendimiento sólo se degrada notablemente para valores extremos. En el tercer escenario planteado, se estudia cómo afecta al rendimiento el aprovechamiento de las cachés mediante el uso del affinity_partitioner. En el estudio se repite varias veces la suma de dos matrices cuadradas. De este modo, el aprovechamiento de las cachés puede suponer una ventaja. Así mismo, se varía el tamaño de la matriz. Dependiendo del tamaño de ésta, los datos que procesa cada núcleo en cada una de las ejecuciones pueden caber o no en la caché. Se comparan los tiempos de ejecución utilizando un affinity_partitioner y un auto_partitioner. La Figura 3.10 muestra los resultados obtenidos en el PC multinúcleo con dos procesadores. Se puede observar como el particionador afín no supone una ventaja en ningún caso. Incluso merma el rendimiento para tamaños de matriz pequeños, debido a las sobrecargas que introduce. En la Figura 3.11 se muestran las mismas ejecuciones pero usando el nodo con 4 núcleos. Aunque se repiten los malos resultados del particionador afín para matrices pequeñas, en este caso sí que se aprecia una leve mejoría con tamaños de matriz medianos. Llegándose a obtener una aceleración de 1,3 para ma- 50

73 2 1.5 Aceleración Sin aceleración Tamaño (filas) Figura 3.10: Aceleración con particionador afín/no afín - Intel R Core TM 2 Duo E GHz (2 núcleos) Aceleración Sin aceleración Tamaño (filas) Figura 3.11: Aceleración con particionador afín/no afín - 2 Intel R Xeon TM dual-core 5060 a 3.20GHz (2x2 núcleos) Aceleración Sin aceleración Tamaño (filas) Figura 3.12: Aceleración con particionador afín/no afín - 4 Intel R Xeon TM hexa-core E7450 a 2.40GHz (4x6 núcleos) 51

74 trices de 64 filas. La mejora producida por el particionador afín es mucho mayor para el caso del nodo con 24 núcleos (ver Figura 3.12). Aunque con matrices pequeñas se producen pérdidas de rendimiento, con matrices de entre 128 y 4096 filas se aprecian aceleraciones mayores que 1, alcanzándose una aceleración próxima a 2 con matrices de 512 por 512 elementos. Es conocido que utilizar un affinity_partitioner puede suponer una ventaja cuando [8]: Se realiza poca computación por cada acceso de datos. En caso contrario el peso relativo de los accesos a memoria no será significativo como para que la utilización de las cachés suponga una gran mejora. Los datos computados caben en la caché. La operación paralela se repite en numerosas ocasiones sobre los mismos datos. Existen más de dos núcleos de computación. Con dos núcleos de computación, la afinidad caché proporcionada por el particionado automático es suficiente. Estos factores explican los resultados obtenidos. En el caso del PC multinúcleo no se produce mejoría alguna ya que sólo disponemos de dos núcleos y el particionador automático ya toma decisiones suficientemente buenas. En los experimentos realizados en el nodo con cuatro núcleos sí que se ha obtenido una mejoría notable. Tal como está diseñado el experimento, se realiza poca computación por cada dato (una suma) y la computación se repite, ya que se llama repetidamente a la operación suma sobre las mismas matrices. La segunda condición expuesta sólo se cumple a veces. En concreto cuando las matrices no son excesivamente grandes. Si las matrices son muy pequeñas la sobrecarga introducida por el particionador también anula las posibles mejoras. De aquí que se obtenga una mejora sólo para matrices de tamaño medio. En las ejecuciones en el nodo con 24 núcleos los accesos a memoria cobran más importancia en relación con la computación realizada debido a que el acceso a memoria constituye un cuello de botella por lo que las operaciones de memoria resultan más costosas. Por lo tanto, el buen uso de las cachés cobra en este caso especial importancia, lo que explica los buenos resultados obtenidos por el particionador afín con matrices de tamaño medio. 52

75 Capítulo 4 El tipo Concurrent_Set El objetivo central de este proyecto ha sido el desarrollo de un contenedor de datos de tipo conjunto, paralelizado de forma transparente al usuario, que proporcione las funcionalidades más comúnmente demandadas en las aplicaciones que hacen uso intensivo de este tipo de contenedores. En este capítulo se lleva a cabo un análisis de los requisitos y funcionalidades que debe satisfacer la librería, y se aborda la construcción de la misma con el tipo concurrent_set. La Sección 4.1 expone los requisitos generales que se han de cumplir. La Sección 4.2 estudia las operaciones que debe implementar el contenedor. La Sección 4.3 plantea y justifica una estructura interna para el conjunto. La Sección 4.4 analiza el planteamiento del contenedor como una plantilla de clase. Finalmente, la Sección 4.5 y la Sección 4.6 presentan las soluciones de diseño e implementación adoptadas en la construcción y paralelización de las distintas operaciones del contenedor Requisitos Antes de analizar el conjunto de operaciones que debería de ofrecer un contenedor de tipo conjunto, es conveniente establecer una serie de requisitos deseables que puedan guiar el análisis y el posterior diseño de la librería. A continuación, se enumeran y detallan algunos de los requerimientos más importantes que se han planteado en el desarrollo de este contenedor de datos: Eficiencia La motivación principal de este proyecto es la de proporcionar una herramienta que permita al programador aprovechar el paralelismo hardware presente 53

76 en los procesadores multinúcleo. La explotación del paralelismo tiene como fin principal disminuir los tiempos de ejecución y aumentar la eficiencia. Por lo tanto, se exigirá al contenedor desarrollado un aumento de la eficiencia y una mejora en los tiempos de ejecución respecto a las soluciones tradicionales. Transparencia La mejora obtenida en la eficiencia no debe ser a costa de complicar la programación. El paralelismo subyacente en la implementación del contenedor debe ser transparente al usuario, de forma que el uso del contenedor desarrollado no difiera excesivamente del uso de un contenedor habitual. Genericidad Un requisito indispensable en un contenedor de datos es que éste sea genérico. Los elementos almacenados en el contenedor de datos pueden tener cualquier tipo, sea éste primitivo o definido por el usuario, sin que el comportamiento del contenedor varíe ni se obligue al programador a utilizar rodeos en la programación. Funcionalidad La funcionalidad que ofrece el contenedor debe ser lo suficientemente completa para cubrir los casos de uso más habituales para el contenedor desarrollado. Éste debe constituir una herramienta útil al programador. Para ello debe ofrecer un conjunto amplio y completo de operaciones. Estandarización Es deseable que el contenedor desarrollado pueda ser utilizado en aplicaciones ya construidas. Si estas aplicaciones hacen uso de contenedores estándar, se debería poder sustituir el uso de éstos por el nuevo contenedor desarrollado, sin que ésto suponga grandes cambios en la programación. En consecuencia, el contenedor debe respetar la interfaz de los contenedores estándar en la medida de lo posible. Configurabilidad El contenedor debe proporcionar mecanismos que permitan al programador guiar en cierto grado el proceso de paralelización para realizar ajustes finos en situaciones particulares. De esta forma, se permite al programador intervenir en el proceso de paralelización, permitiéndole mejorar la selección por defecto de determinados parámetros de configuración de la librería. 54

77 4.2. Operaciones a implementar Uno de los puntos importantes del análisis lo constituye el problema de determinar qué funcionalidad debe soportar el contenedor desarrollado. Se ha establecido en el punto anterior que sería deseable que el contenedor implemente la interfaz de los contenedores de la librería estándar. En concreto, se tomará como referencia el tipo std:set 1 de la STL, por ser éste el contenedor más próximo, semánticamente hablando, al que se quiere construir. A mayores, se pretende ofrecer una amplia funcionalidad que cubra una buena parte de los usos que se le dan a los conjuntos en problemas de computación intensiva. Para ello, se han estudiado las operaciones más comunes del tipo de dato conjunto, tanto desde el punto de vista algebraico [2], como desde el punto de vista de la programación. En este último caso, se han estudiado tanto las operaciones ofrecidas por otras implementaciones del tipo conjunto [25], como las operaciones requeridas en algoritmos que hacen uso intensivo de este tipo de contenedores de datos [31]. Se define un conjunto como una colección bien definida de objetos, a los que se denomina elementos o miembros del conjunto. Un conjunto está completamente determinado por sus elementos, de forma que dos conjuntos con los mismos elementos se consideran iguales. Los elementos pertenecientes a un conjunto pueden determinarse por extensión, enumerando todos los elementos, o por comprensión, indicando una regla que los caracteriza. Dentro de las operaciones que es posible definir sobre los conjuntos podemos destacar las siguientes: Pertenencia Sea e un elemento y A un conjunto. Se dice que e A (e pertenece a A) si e = x i para algún i en la definición por extensión A := {x 1, x 2,..., x n }, o si se cumple P red(e) para la definición por comprensión A := {x : P red(x)}, siendo P red un predicado sobre los elementos del dominio. En el caso de un contenedor de tipo conjunto, éste siempre estará definido por extensión. La operación de pertenencia determina si un elemento forma parte de los incluidos en el contenedor. Inclusión Sean A y B conjuntos. Se dice que A B (A es subconjunto de B) si para cualquier e tal que e A, se cumple que e B. La operación de inclusión com- 1 Los componentes de la STL se definen bajo el espacio de nombres std. En adelante se omite por brevedad. 55

78 prueba si esta condición se cumple para todos los elementos de un contenedor de tipo conjunto, estableciendo si un conjunto es subconjunto de otro. Unión Sean A y B conjuntos. Se define el conjunto unión (A B) como aquél en que cualquier elemento e cumple que e A o e B. La operación de unión añade a un contenedor de tipo conjunto los elementos de otro que antes no tenía, obteniendo el conjunto unión. Intersección Sean A y B conjuntos. Se define el conjunto intersección (A B) como aquél en que cualquier elemento e cumple que e A y e B. La operación de intersección elimina de un contenedor de tipo conjunto los elementos que no están incluidos en un segundo conjunto, obteniendo el conjunto intersección. Diferencia Sean A y B conjuntos. Se define el conjunto diferencia (A\B) como aquél en que cualquier elemento e cumple que e A pero e / B. La operación de diferencia elimina de un contenedor de tipo conjunto los elementos que también están incluidos en otro conjunto, obteniendo el conjunto diferencia. Diferencia simétrica Sean A y B conjuntos. Se define el conjunto diferencia simétrica (A B) como aquél en que cualquier elemento e cumple que e A y e / B, o e B y e / A. La operación de diferencia simétrica elimina de un contenedor de tipo conjunto los elementos que también están incluidos en otro conjunto, y añade los que están en el segundo pero no en el primero, obteniendo el conjunto diferencia simétrica. Selección Sea un conjunto A de datos de tipo T y un predicado definido sobre los elementos del mismo P red : T {verdadero, falso}. Se puede determinar un subconjunto B del conjunto A definiéndolo por comprensión mediante la expresión B := {x : P red(x : T )/x A}. La operación de selección aplica un predicado a todos los elementos de un contenedor de tipo conjunto, generando un subconjunto con todos aquellos elementos para los que el predicado resulte verdadero. Aplicación Sea un conjunto A de datos de tipo T y una función f : T T que realiza alguna modificación sobre los elementos de dicho tipo. La operación de aplicación aplica dicha función f sobre todos los elementos de un contenedor de tipo conjunto. 56

79 Reducción Sea un conjunto A := {e 1, e 2,..., e n } de datos de tipo T y una función de reducción r : (T, T ) T con elemento neutro e. Se define la reducción de A mediante r como el valor r n, siendo r(e i, r i 1 ) si i = 2,..., n r i = r(e i, e) si i = 1 La operación de reducción aplica una función de reducción sobre cada elemento de un conjunto, combinando todos los elementos del mismo para obtener un único valor de retorno. Mapeo Sea un conjunto A de datos de tipo T y una función f : T S que realiza una determinada operación sobre elementos de tipo T devolviendo nuevos elementos de un tipo S. Se define la operación de mapeo como el resultado de aplicar dicha función f sobre todos los elementos del conjunto, generando un nuevo conjunto con los resultados obtenidos. Relación Sean A y B conjuntos. Se define el conjunto producto cartesiano (A B) como el conjunto formado por todos los pares posibles (a, b) en el que a A y b B. Una relación R entre los conjuntos A y B es un subconjunto del producto cartesiano A B. Cada uno de los pares (a, b) pertenecientes a R determina que a se relaciona con b de alguna manera. Se definen las siguientes propiedades relevantes de la relación R sobre un conjunto A: R es reflexiva si y sólo si a A : (a, a) R R es simétrica si y sólo si a, b A : (a, b) R (b, a) R R es transitiva si y sólo si a, b, c A : (a, b), (b, c) R (a, c) R La operación de relación construye el conjunto formado por los pares de elementos relacionados de dos conjuntos, a partir de un predicado que define la relación, y de sus propiedades Estructura de datos Es fundamental que el conjunto se construya de forma que sea fácilmente paralelizable. Para ello, se propone una estructura en la que un conjunto se forma a partir 57

80 de una colección de subconjuntos. De esta manera, surge un patrón de paralelización sencillo, en el que aplicar una operación a un conjunto consiste en aplicar esa operación a cada uno de sus subconjuntos. El tratamiento de cada uno de los subconjuntos constituye una subtarea. Así, se pretende distribuir el procesamiento del conjunto inicial sobre los diferentes núcleos de procesamiento, aprovechando todo el paralelismo hardware disponible. La implementación de este diseño utiliza los mecanismos de la biblioteca estándar de C++. En concreto, se utilizan los contenedores de la librería estándar set y vector, ya que éstos aportan [24]: Fiabilidad Al ser estándar y muy ampliamente utilizados y soportados, están probados exhaustivamente. Asimismo, han sido diseñados de forma que minimizan la propensión a errores frente a estructuras de datos más primitivas o elaboradas específicamente para este proyecto. Eficiencia Puesto que todo el diseño de la STL ha sido planteado de forma que sus componentes no presenten sobrecargas perceptibles respecto a soluciones más primitivas. Portabilidad Ya que ofrecen utilidades no primitivas que son independientes de la plataforma de ejecución, y es la propia implementación de la STL la que asume los cambios necesarios de portabilidad. Configurabilidad Puesto que permiten al usuario la posibilidad de proporcionar políticas de asignación de memoria como parámetros 2. Completitud Ya que han sido diseñados de forma que ofrecen funcionalidad suficiente como para que los usuarios no necesiten reemplazarlos para hacer el trabajo básico. Genericidad Puesto que están implementados utilizando el mecanismo de plantillas de C++. Cada uno de los subconjuntos de un concurrent_set es un set, el tipo de contenedor que representa los conjuntos en la librería estándar. Estos subconjuntos están dispuestos en una estructura indexada de tipo vector, implementada por la clase 2 Se necesitarán políticas que soporten la reserva concurrente de memoria. 58

81 Figura 4.1: Estructura de datos de un concurrent_set vector de la STL (ver Figura 4.1). El número de subconjuntos generado por defecto coincide con el paralelismo hardware disponible 3. De esta forma, se facilita la correspondencia del procesamiento de cada subconjunto con un núcleo de computación. Un número de subconjuntos menor provoca que en el procesamiento del conjunto global queden núcleos ociosos. Un número mayor puede afectar al balanceo de carga o acelerar las operaciones que utilizan bloqueos sobre los subconjuntos. Por lo tanto, la librería permite al usuario realizar ajustes en el número de subconjuntos utilizados en la implementación de cada conjunto con el fin de que pueda optimizar su funcionamiento en diferentes circunstancias. Sin embargo, operar dos conjuntos con diferente número de subconjuntos puede ralentizar la ejecución de las operaciones ( 4.5). Es esencial que el reparto de los elementos del conjunto global entre los subconjuntos esté bien balanceado, ya que de este balanceo depende directamente el balanceo de las tareas. El reparto debe ser determinista ya que para acceder a un elemento, es necesario saber dónde se encuentra el mismo o exponernos a hacer costosas búsquedas que lastrarían la eficiencia. Además, debe estar basado en el contenido puesto que los elementos serán accedidos por su valor. Una función de dispersión cumple estos tres requisitos. Sin embargo, la STL no ofrece una función de dispersión estándar 4, y no es razonable obligar al usuario a proporcionar una cada vez que use un 3 Se puede obtener este valor mediante tbb_thread::hardware_concurrency() 4 Aunque la propuesta de estándar TR1 y diversas implementaciones de la STL ya la incluyen. 59

82 concurrent_set. Por lo tanto, la librería ofrece diversas funciones de dispersión, útiles en diferentes escenarios. Los elementos de los conjuntos de la biblioteca estándar son accedidos por contenido. Como consecuencia, el acceso a un elemento puede llegar a hacerse prohibitivamente costoso. Con el fin de evitarlo, la clase set almacena sus elementos ordenados, de forma que el tiempo de acceso se reduzca, pasando de complejidades lineales 5 a logarítmicas en los accesos individuales. De hecho, la implementación tradicional de los conjuntos en la STL se basa en el uso de árboles binarios de búsqueda balanceados. Ésta y otras características deseables de la implementación estándar de los conjuntos será directamente heredada por el tipo concurrent_set. Por otra parte, es aconsejable que la clase concurrent_set contenga un puntero al vector que contiene los subconjuntos y no el vector mismo. Ésta es una táctica común en los contenedores, que persigue que la operación swap() 6 sólo requiera el intercambio de dos punteros. De otra forma, esta operación precisaría la copia e intercambio de cantidades masivas de datos, repercutiendo fuertemente en la eficiencia. La operación swap() tiene una gran relevancia, ya que es utilizada internamente en la implementación de otras operaciones importantes del conjunto, en las que se construyen conjuntos temporales que reemplazarán al actual Soporte de elementos variables La estructura propuesta presenta problemas para soportar la implementación de las operaciones que pueden variar el valor de los elementos contenidos en el conjunto. Como ya se ha visto, los subconjuntos almacenan los elementos ordenados. Al modificar un elemento, se corre el riesgo de alterar el orden establecido en los elementos, dejando el elemento ilocalizable. Por otra parte, si un elemento varía, su valor de dispersión 7 puede cambiar también, de forma que quede almacenado en un subconjunto que en realidad no le corresponde. El usuario puede suministrar una función de dispersión basada en algún rasgo invariable del elemento. Sin embargo, este rasgo no siempre existe. Para evitar estos problemas, es posible ofrecer dos versiones de las operaciones que pueden variar el valor de los elementos. Una de ellas, más eficiente, confía en que la variación aplicada no afecta ni a la función de dispersión ni a la relación de orden 5 En el caso de una busqueda secuencial a lo largo del conjunto. 6 Esta operación produce el intercambio de elementos entre dos contenedores. 7 El valor devuelto por la función de dispersión. 60

83 sobre los elementos. La otra, más segura, lleva a cabo un reordenamiento de los elementos después de aplicar la operación, de forma que éstos sean de nuevo localizables. Obviamente, la implementación de la versión segura de las operaciones tiene efectos perniciosos sobre el rendimiento de las mismas. Por lo tanto, si se puede garantizar que la variación no afecta ni a la función de dispersión ni a la relación de orden, es mejor utilizar la versión más eficiente. La librería proporciona cuatro versiones para la función de dispersión que pueden influir en este último punto: content_hash<t>: Función de dispersión basada en el contenido de los elementos. La modificación de los mismos puede dejarlos descolocados e ilocalizables dentro del contenedor. Es la usada por defecto. content_hash<t*>: Función de dispersión basada en el contenido de los elementos cuando el conjunto sólo almacena sus punteros. Esta función previene errores sustituyendo a la anterior cuando los elementos del conjunto son punteros. Así, se utilizará para el cálculo de la función de dispersión el contenido de los elementos apuntados por los punteros y no el valor de los punteros mismos. address_hash<t>: Función de dispersión basada en las direcciones físicas de los elementos. No se ve alterada por la variación en el valor de los mismos, sin embargo, la calidad de la dispersión puede ser pobre en algunos casos. address_hash <T*>: Función de dispersión basada en valor de los elementos cuando éstos son punteros. Esta función previene errores cuando los elementos del conjunto son punteros. Así, se utilizará para el cálculo de la función de dispersión el valor de los propios punteros y no la dirección donde se almacenan éstos La clase concurrent_set como plantilla A la hora de abordar la construcción de un contenedor, el uso de plantillas (templates) permite seleccionar el tipo de los elementos almacenados como un parámetro en su definición. De esta forma, se consigue genericidad y uniformidad en el tratamiento de los tipos de elementos soportados. Así, un objeto no necesita una clase base especial o un campo enlace especial para pertenecer a un contenedor. Un contenedor con 61

84 esta característica se denomina no intrusivo. Los contenedores no intrusivos funcionan bien con los tipos primitivos y con las estructuras que tienen formatos impuestos externamente. Los contenedores así definidos también son seguros en tipos y homogéneos (todos los elementos son del mismo tipo). Por lo tanto, se evitan conversiones de tipos explícitas cuando se recuperan los elementos del contenedor. Consecuentemente concurrent_set se define como una plantilla en la que el tipo de sus elementos se especifica mediante un parámetro. Además, las plantillas permiten construir contenedores independientes de las políticas, de forma que pueden aceptar una determinada política como parámetro de plantilla. En el caso de los contenedores, es común definir como parámetro de plantilla la política de asignación de memoria [24]. Ésto proporciona al usuario una alta capacidad de configuración sobre el contenedor. En términos generales, los contenedores no obligan al usuario a proporcionar un asignador de memoria, sino que ofrecen uno por defecto que funcionará correctamente en la mayoría de los casos. Sólo en determinadas situaciones se utilizará un asignador definido por el usuario que aporte alguna característica deseable. A la hora de trabajar con un conjunto paralelizado, es deseable utilizar un asignador que permita realizar la reserva de memoria concurrentemente y sin bloqueos. La librería Intel TBB, con la que se paraleliza el contenedor, proporciona el asignador scalable_allocator con estas características. Por lo tanto, concurrent_set tomará como parámetro de plantilla la política de asignación de memoria, definiéndola por defecto del tipo scalable_allocator. Las clases vector y set utilizadas en la implementación de la estructura de concurrent_set ( 4.3) son, a su vez, definidas como plantillas de clase. La plantilla de clase vector tiene dos parámetros de plantilla. El primero determina el tipo de dato contenido y, en este caso, es el tipo set, que representa el tipo de los subconjuntos de implementación. El segundo determina el asignador de memoria que se utiliza. Éste es el utilizado por concurrent_set. La plantilla de clase set tiene tres parámetros de plantilla. El primero de ellos determina el tipo de dato contenido. En este caso, coincide con el tipo de los elementos almacenados por el conjunto concurrente. El segundo es un comparador que permite establecer un orden en los elementos del conjunto. El tercer parámetro de la plantilla set determina el asignador de memoria, que de nuevo será el utilizado por concurrent_set. Es preciso, por lo tanto, tomar como parámetro de plantilla de concurrent_set un comparador con el que trabajarán los subconjuntos que lo forman. Éste se define 62

85 por defecto como el functor de comparación less<t> definido en la STL, siendo T el tipo de los elementos del conjunto. Asimismo, recordando que es preciso una función de dispersión para balancear el reparto de elementos entre los subconjuntos de implementación ( 4.3), se toma como parámetro de plantilla un tipo, posiblemente una clase functor, que implementa dicha función. La función de dispersión se define por defecto como content_hash<t>, siendo T el tipo de los elementos del conjunto. En conclusión, la plantilla concurrent_set<key,h,cmp,a> toma cuatro parámetros. Key representa el tipo de los elementos del conjunto, H el tipo de la función de dispersión, Cmp es el tipo de un comparador definido sobre los elementos del conjunto y A la política de asignación de memoria. Sólo el primero de estos cuatro parámetros es imprescindible, los otros tres toman valores por defecto válidos para una gran mayoría de las situaciones en las que se usa el contenedor Estrategias de paralelización Antes de abordar la implementación de las operaciones que soporta el contenedor, es conveniente estudiar los posibles patrones de extracción del paralelismo que se pueden usar en concurrent_set. Estos patrones servirán de referencia en el momento de concretar las estrategias utilizadas en las operaciones implementadas. La Sección analiza la paralelización de las operaciones sobre los elementos de un conjunto que no modifican el valor de los elementos de forma tal que invalide su localización (o indexación) en el conjunto. La Sección estudia el caso complementario en el que se aplica una operación sobre los elementos del conjunto que sí puede cambiar su valor de distribución o su relación de orden en el dominio, invalidando así la indexación de los elementos en el conjunto. La Sección describe la paralelización de las operaciones sobre dos conjuntos en las que para cada elemento de los conjuntos se busca su igual en el otro conjunto. La Sección estudia las operaciones que requieren combinar todos los elementos de un conjunto con todos los elementos de otro conjunto. La Sección analiza la paralelización de las operaciones que generan como resultado un conjunto con nuevos elementos. La Sección estudia la paralelización de las operaciones de reducción. 63

86 1 void a p p l y _ o p e r a t i o n ( ) { 2 t y p e d e f typename s e t <_Key, _Cmp, _A > : : i t e r a t o r i t ; 3 4 f o r ( s i z e _ t i =0; i <n ; i ++){ 5 i t f i r s t = ( r e p ) [ i ]. b e g i n ( ) ; 6 i t l a s t = ( r e p ) [ i ]. end ( ) ; 7 while ( f i r s t!= l a s t ) { 8 o p e r a t i o n ( f i r s t + + ) ; 9 } 10 } 11 } Listado 4.1: Aplicación de una operación Operaciones paralelas que no modifican el indexado En un primer análisis, podemos suponer un escenario donde una operación sobre todos los elementos de un conjunto, se puede convertir en la aplicación independiente de esa misma operación sobre los elementos de cada uno de los subconjuntos. En el Listado 4.1 se muestra el código que implementa la aplicación de la operación operation() sobre los elementos de un conjunto en una situación como la descrita. El bucle representado en las líneas 4-10 recorre los índices de los n subconjuntos del conjunto actual. En las líneas 5-6 se declaran los iteradores que permiten recorrer cada subconjunto desde su inicio hasta su fin, siendo la variable rep el puntero al vector sobre el que se indexan los subconjuntos. Finalmente, el bucle de las líneas 7-9 recorre los elementos de cada subconjunto aplicándoles la operación operation. Paralelizar este código es muy sencillo utilizando las plantillas de función de la librería Intel TBB ( 3.2.3). Con la función parallel_for(), se puede paralelizar el bucle exterior convirtiendo el procesado de los elementos de cada subconjunto en subtareas independientes. El código resultante se puede ver en el Listado 4.2. Ahora el código de la función apply_operation() queda reducido a una llamada a parallel_for() (líneas 18-20). El primer parámetro de la llamada especifica el rango de iteración del bucle. No se especifica un tamaño de grano determinado, con lo que éste queda definido por defecto al valor uno. Ésto es precisamente lo que se busca para que el tratamiento de cada subconjunto constituya una subtarea. Tampoco se especifica un tercer parámetro para parallel_for(), de forma que se toma el particionador por defecto simple_partitioner ( 3.2.2). Ésto nos garantiza que el rango se subdividirá hasta un tamaño menor o igual al del grano. Siendo el tamaño 64

87 1 template < c l a s s _Key, c l a s s _Cmp, c l a s s _A> 2 c l a s s body { 3 v e c t o r < s e t <_Key, _Cmp, _A>,_A > _rep ; 4 p u b l i c : 5 body ( v e c t o r < s e t <_Key, _Cmp, _A>,_A > r e p ) : _rep ( r e p ) { } 6 7 void operator ( ) ( c o n s t b l o c k e d _ r a n g e < s i z e _ t >& r ) c o n s t { 8 f o r ( s i z e _ t i = r. b e g i n ( ) ; i < r. end ( ) ; i ++){ 9 i t f i r s t = ( _ rep ) [ i ]. b e g i n ( ) ; 10 i t l a s t = ( _ rep ) [ i ]. end ( ) ; 11 while ( f i r s t!= l a s t ) { 12 o p e r a t i o n ( f i r s t ) ; 13 } 14 } 15 } 16 } ; void a p p l y _ o p e r a t i o n ( ) { 19 p a r a l l e l _ f o r ( b l o c k e d _ r a n g e < s i z e _ t >(0, n ), body <_Key, _Cmp, _A>( r e p ) ) ; 20 } Listado 4.2: Paralelización de la aplicación de una operación de grano uno, el rango se subdivide hasta formar subrangos de una iteración, lo que equivale a decir que el procesado de cada subconjunto constituirá una tarea. Cuando coinciden el número de subconjuntos con el número de núcleos del procesador y el número de hilos del gestor de tareas, es probable que el gestor de tareas asigne dinámicamente el procesado de cada subconjunto a un núcleo del procesador. En las líneas 1-16 del Listado 4.2, se implementa la clase functor que recorre el subrango de iteración (línea 8) procesando todos los elementos de cada subconjunto del subrango (líneas 11-13). El constructor de la clase (línea 5) toma como parámetros los valores necesarios para implementar la operación. En este caso, se reducen a un puntero al vector que contiene los subconjuntos del concurren_set Operaciones paralelas que modifican el indexado Puede ser que la operación que se aplica a los elementos varíe el valor de éstos, de forma que sea necesaria una reordenación de los mismos ( 4.3.1). Un primer planteamiento para abordar esta reordenación consiste en que cada vez que se opere sobre un elemento, éste se inserte sobre un conjunto auxiliar en su posición correcta. Al final 65

88 Figura 4.2: Estructura en forma de rejilla para evitar bloqueos. de todo el proceso, intercambiaríamos 8 el conjunto auxiliar con el actual, desechando el actual. Sin embargo, éste planteamiento limita el paralelismo. Cuando se inserta un elemento que ha sido modificado, no se conoce a priori en qué subconjunto del conjunto auxiliar se va a insertar, ya que puede haber cambiado su valor de dispersión. Si se están procesando varios elementos de otros tantos subconjuntos concurrentemente, puede que se intente insertar el valor resultado de los mismos en el mismo subconjunto del conjunto auxiliar. La clase set de la librería estándar no soporta inserciones concurrentes. Por lo tanto, se requerirían bloqueos sobre los subconjuntos del conjunto auxiliar. Los bloqueos causan ineficiencias, y el rendimiento de la librería Intel TBB es especialmente vulnerable a la utilización de los mismos [8]. Es necesario plantear una solución no bloqueante al problema de la reordenación. Una posible solución consiste en usar una estructura en forma de rejilla en la que las filas se corresponden con el subconjunto que está siendo procesado actualmente, y las columnas representan el subconjunto destino del elemento después de su procesamiento (ver Figura 4.2). Así, cada subconjunto se procesa por separado, de forma que va clasificando sus elementos dentro de la estructura en función del subconjunto 8 Mediante la función swap(). 66

89 destino que les corresponde. Es posible hacerlo de forma concurrente, ya que cada subconjunto origen tiene una fila en la estructura, independiente de los otros subconjuntos origen. Al final del procesamiento, los elementos, ya clasificados en la rejilla, se insertan en los subconjuntos de un conjunto auxiliar. Cada subconjunto destino recibirá los elementos que están situados en su correspondiente columna de la estructura en forma de rejilla. De nuevo es posible hacerlo de forma concurrente para cada subconjunto destino. El último paso consiste en intercambiar el conjunto actual por el auxiliar, desechando el actual. Es importante destacar que la estructura auxiliar no guarda los elementos mismos, pues ésto puede ser extremadamente caro, sino punteros a los elementos que después de modificados y antes de ser reinsertados en el conjunto auxiliar, siguen almacenados en el conjunto original. En este caso, la paralelización sería muy similar a la planteada en el primer caso. La única diferencia consistiría en que ahora es necesario paralelizar dos bucles consecutivos. El primero, que recorrería los subconjuntos origen clasificando sus elementos en la estructura intermedia, y el segundo, que insertaría los elementos ya ordenados de la estructura intermedia en los subconjuntos destino Operaciones igual a igual Otro grupo de operaciones similar a las planteadas estaría formado por aquellas operaciones que comparan igual a igual los elementos de dos conjuntos, de forma que la operación que aplicamos a cada elemento de un conjunto requiere buscar ese mismo elemento en el otro conjunto. Buscar cada elemento del primer conjunto en el segundo implica complejidades de O(N log(m)), siendo N la cardinalidad del primer conjunto y M la del segundo. Una táctica más eficiente es aprovechar que las secuencias de elementos de ambos conjuntos están ordenadas, para evitar buscar cada elemento separadamente. Buscar el primer elemento de la secuencia de un conjunto en otro, consiste en recorrer la segunda secuencia desde el principio hasta encontrar un elemento igual o mayor. Para buscar el siguiente elemento de la secuencia del primer conjunto, se procede de igual forma, pero empezando en el punto de la segunda secuencia donde finalizó la búsqueda del elemento anterior. Recíprocamente, se pueden buscar los elementos del segundo conjunto en el primero. Por lo tanto, si las secuencias se avanzan escogiendo a cada paso la secuencia con el elemento menor y comparando, se puede comprobar en una sola pasada sobre ambas secuencias, si un elemento de un conjunto está incluido en el otro y viceversa. Se reduce por lo tanto la complejidad a O(N +M). 67

90 Utilizar esta táctica de una forma eficiente sobre la estructura de subconjuntos planteada para concurrent_set requiere que los dos conjuntos operandos repartan los elementos sobre los subconjuntos siguiendo la misma política. Así, se garantiza que un elemento situado en un determinado subconjunto del primer comparando, sólo podrá estar o no estar, en el subconjunto correspondiente del segundo comparando y en ningún otro. De esta forma, la operación se simplifica a comparar los subconjuntos con igual índice, sin tener que buscar en todos los posibles pares de subconjuntos. Para que los conjuntos distribuyan los elementos de igual manera, deben tener el mismo número de subconjuntos, la misma función de dispersión y el mismo comparador. Paralelizar esta operación se reduce a paralelizar un bucle que itera sobre cada par de subconjuntos con el mismo índice. Para ello se utiliza la plantilla de función parallel_for() de manera equivalente a la estudiada en el primer caso. Así, el procesado de cada par de subconjuntos constituye una subtarea Operaciones de todos con todos Dos conjuntos también pueden operarse de forma que se evalúen todas las combinaciones de los elementos de ambos conjuntos. La paralelización vuelve a seguir el mismo esquema que hasta ahora. Un bucle externo recorre todos los subconjuntos del primer operando y otro anidado recorre sus elementos. Para cada uno de estos elementos del primer operando, se recorren los subconjuntos y elementos del segundo. Se paraleliza el bucle externo utilizando la plantilla parallel_for(), de forma que el procesado de cada subconjunto del primer operando constituya una subtarea. Si la operación modifica los elementos, pueden aparecer problemas de concurrencia. La modificación de los elementos del primer conjunto no supone ningún problema pues el procesado de cada subconjunto es una tarea independiente. Sin embargo, puede que dos elementos de subconjuntos diferentes del primer operando se estén combinando simultáneamente con el mismo elemento del segundo, ya que para cada subconjunto del primer operando se recorren todos los subconjuntos del segundo. Será necesario utilizar bloqueos en el acceso a los subconjuntos del segundo operando. En este escenario, es posible hacer una pequeña optimización con el fin de minimizar los bloqueos. Si para cada subconjunto del primer operando comenzamos a recorrer los subconjuntos del segundo desde el índice más bajo y en orden, tendremos que al empezar el recorrido, sólo un hilo de ejecución tendrá acceso al primer subconjunto y el resto quedarán bloqueados. Al terminar con este subconjunto, el hilo activo seguirá 68

91 Figura 4.3: Secuencia ejecución con cruce en orden. Figura 4.4: Secuencia ejecución con cruce en ciclo. 69

92 con el segundo subconjunto. Uno de los que estaban esperando podrá acceder ahora al primer subconjunto, pero el resto seguirán esperando bloqueados. Siguiendo este patrón (ver Figura 4.3), se desaprovecha gran parte del paralelismo disponible. Se pueden mejorar las cosas si cada subconjunto del primer operando se comienza a cruzar con el subconjunto correspondiente del segundo, siguiendo en orden con los sucesivos subconjuntos del segundo operando hasta que dé la vuelta a todos (ver Figura 4.4). Esta táctica no elimina por completo los bloqueos, pero sí los reduce notablemente. La estrategia de paralelización planteada no contempla el caso en el que un conjunto se combine consigo mismo. Cuando se combina un conjunto consigo mismo, un elemento procesado como primer componente de un par de elementos combinados no puede ser procesado concurrentemente como segundo componente de otro par, pues en este caso ambos componentes provienen del mismo conjunto. Por lo tanto, si se utilizan bloqueos a nivel de subconjunto, sería incorrecto combinar el subconjunto i con un subconjunto j concurrentemente a la combinación de un subconjunto k con el subconjunto i, como efectivamente ocurre en la estrategia presentada. Para solventar este problema se plantea una táctica de paralelización alternativa (ver Figura 4.5). Ésta comienza dividiendo el rango de subconjuntos en dos mitades. En un primer paso, se combinan los subconjuntos de la primera mitad con los de la segunda y, una vez completado este paso, se combinan los subconjuntos de la segunda mitad con los de la primera. El procesado de cada subconjunto de una de las partes constituye una subtarea. El acceso a los subconjuntos de la otra parte se controla mediante bloqueos. En los sucesivos pasos, cada una de estas mitades se subdivide nuevamente, procediendo de la misma forma en cada una de las mitades generadas. Este proceso se repite hasta que las subdivisiones lleguen al tamaño de un solo subconjunto. En este momento, cada subconjunto se combina consigo mismo de forma que no hay problemas de concurrencia ya que el procesado de cada subconjunto constituye una tarea independiente, por lo que no son necesarios los bloqueos. En la Figura 4.5 se representa este proceso en el caso de un conjunto implementado mediante 8 subconjuntos. Se muestran los pasos consecutivos del cruce de elementos de arriba hacia abajo, señalando con fondo gris los subconjuntos cuyo procesado constituye una subtarea y con fondo blanco los subconjuntos con los que se cruzan, sobre los cuales se definen bloqueos para evitar accesos concurrentes a sus elementos. Este método garantiza que se procesan todos los pares de elementos posibles sin que ningún elemento del conjunto se pueda procesar concurrentemente. Como contra- 70

93 Figura 4.5: Cruce de un conjunto consigo mismo. 71

4. Programación Paralela

4. Programación Paralela 4. Programación Paralela La necesidad que surge para resolver problemas que requieren tiempo elevado de cómputo origina lo que hoy se conoce como computación paralela. Mediante el uso concurrente de varios

Más detalles

Elementos requeridos para crearlos (ejemplo: el compilador)

Elementos requeridos para crearlos (ejemplo: el compilador) Generalidades A lo largo del ciclo de vida del proceso de software, los productos de software evolucionan. Desde la concepción del producto y la captura de requisitos inicial hasta la puesta en producción

Más detalles

INSTRODUCCION. Toda organización puede mejorar su manera de trabajar, lo cual significa un

INSTRODUCCION. Toda organización puede mejorar su manera de trabajar, lo cual significa un INSTRODUCCION Toda organización puede mejorar su manera de trabajar, lo cual significa un incremento de sus clientes y gestionar el riesgo de la mejor manera posible, reduciendo costes y mejorando la calidad

Más detalles

Los mayores cambios se dieron en las décadas de los setenta, atribuidos principalmente a dos causas:

Los mayores cambios se dieron en las décadas de los setenta, atribuidos principalmente a dos causas: SISTEMAS DISTRIBUIDOS DE REDES 1. SISTEMAS DISTRIBUIDOS Introducción y generalidades La computación desde sus inicios ha sufrido muchos cambios, desde los grandes equipos que permitían realizar tareas

Más detalles

ARQUITECTURA DE DISTRIBUCIÓN DE DATOS

ARQUITECTURA DE DISTRIBUCIÓN DE DATOS 4 ARQUITECTURA DE DISTRIBUCIÓN DE DATOS Contenido: Arquitectura de Distribución de Datos 4.1. Transparencia 4.1.1 Transparencia de Localización 4.1.2 Transparencia de Fragmentación 4.1.3 Transparencia

Más detalles

Capítulo 5. Cliente-Servidor.

Capítulo 5. Cliente-Servidor. Capítulo 5. Cliente-Servidor. 5.1 Introducción En este capítulo hablaremos acerca de la arquitectura Cliente-Servidor, ya que para nuestra aplicación utilizamos ésta arquitectura al convertir en un servidor

Más detalles

Ciclo de vida y Metodologías para el desarrollo de SW Definición de la metodología

Ciclo de vida y Metodologías para el desarrollo de SW Definición de la metodología Ciclo de vida y Metodologías para el desarrollo de SW Definición de la metodología La metodología para el desarrollo de software es un modo sistemático de realizar, gestionar y administrar un proyecto

Más detalles

UN ENTORNO A MEDIDA PARA EL DISEÑO Y LA SIMULACIÓN DE MAQUINARIA POR COMPUTADOR

UN ENTORNO A MEDIDA PARA EL DISEÑO Y LA SIMULACIÓN DE MAQUINARIA POR COMPUTADOR UN ENTORNO A MEDIDA PARA EL DISEÑO Y LA SIMULACIÓN DE MAQUINARIA POR COMPUTADOR Manuel González y Javier Cuadrado Departamento de Ingeniería Industrial II, Campus de Esteiro, 15403 Ferrol Universidad de

Más detalles

PROGRAMACIÓN ORIENTADA A OBJETOS Master de Computación. II MODELOS y HERRAMIENTAS UML. II.2 UML: Modelado de casos de uso

PROGRAMACIÓN ORIENTADA A OBJETOS Master de Computación. II MODELOS y HERRAMIENTAS UML. II.2 UML: Modelado de casos de uso PROGRAMACIÓN ORIENTADA A OBJETOS Master de Computación II MODELOS y HERRAMIENTAS UML 1 1 Modelado de casos de uso (I) Un caso de uso es una técnica de modelado usada para describir lo que debería hacer

Más detalles

Modificación y parametrización del modulo de Solicitudes (Request) en el ERP/CRM Compiere.

Modificación y parametrización del modulo de Solicitudes (Request) en el ERP/CRM Compiere. UNIVERSIDAD DE CARABOBO FACULTAD DE CIENCIA Y TECNOLOGÍA DIRECCION DE EXTENSION COORDINACION DE PASANTIAS Modificación y parametrización del modulo de Solicitudes (Request) en el ERP/CRM Compiere. Pasante:

Más detalles

Gestión de la Configuración

Gestión de la Configuración Gestión de la ÍNDICE DESCRIPCIÓN Y OBJETIVOS... 1 ESTUDIO DE VIABILIDAD DEL SISTEMA... 2 ACTIVIDAD EVS-GC 1: DEFINICIÓN DE LOS REQUISITOS DE GESTIÓN DE CONFIGURACIÓN... 2 Tarea EVS-GC 1.1: Definición de

Más detalles

Metodologías de diseño de hardware

Metodologías de diseño de hardware Capítulo 2 Metodologías de diseño de hardware Las metodologías de diseño de hardware denominadas Top-Down, basadas en la utilización de lenguajes de descripción de hardware, han posibilitado la reducción

Más detalles

PRUEBAS DE SOFTWARE TECNICAS DE PRUEBA DE SOFTWARE

PRUEBAS DE SOFTWARE TECNICAS DE PRUEBA DE SOFTWARE PRUEBAS DE SOFTWARE La prueba del software es un elemento crítico para la garantía de la calidad del software. El objetivo de la etapa de pruebas es garantizar la calidad del producto desarrollado. Además,

Más detalles

CAPÍTULO I. Sistemas de Control Distribuido (SCD).

CAPÍTULO I. Sistemas de Control Distribuido (SCD). 1.1 Sistemas de Control. Un sistema es un ente cuya función es la de recibir acciones externas llamadas variables de entrada que a su vez provocan una o varias reacciones como respuesta llamadas variables

Más detalles

K2BIM Plan de Investigación - Comparación de herramientas para la parametrización asistida de ERP Versión 1.2

K2BIM Plan de Investigación - Comparación de herramientas para la parametrización asistida de ERP Versión 1.2 K2BIM Plan de Investigación - Comparación de herramientas para la parametrización asistida de ERP Versión 1.2 Historia de revisiones Fecha VersiónDescripción Autor 08/10/2009 1.0 Creación del documento.

Más detalles

Figure 7-1: Phase A: Architecture Vision

Figure 7-1: Phase A: Architecture Vision Fase A Figure 7-1: Phase A: Architecture Vision Objetivos: Los objetivos de la fase A son: Enfoque: Desarrollar una visión de alto nivel de las capacidades y el valor del negocio para ser entregado como

Más detalles

COMPUTADORES MULTINUCLEO. Stallings W. Computer Organization and Architecture 8ed

COMPUTADORES MULTINUCLEO. Stallings W. Computer Organization and Architecture 8ed COMPUTADORES MULTINUCLEO Stallings W. Computer Organization and Architecture 8ed Computador multinucleo Un computador multinúcleocombina dos o mas procesadores (llamados núcleos) en una única pieza de

Más detalles

Funcionalidades Software SAT GotelGest.Net (Software de Servicio de Asistencia Técnica)

Funcionalidades Software SAT GotelGest.Net (Software de Servicio de Asistencia Técnica) Funcionalidades Software SAT GotelGest.Net (Software de Servicio de Asistencia Técnica) Servinet Sistemas y Comunicación S.L. www.softwaregestionsat.com Última Revisión: Octubre 2014 FUNCIONALIDADES SAT

Más detalles

SISTEMAS Y MANUALES DE LA CALIDAD

SISTEMAS Y MANUALES DE LA CALIDAD SISTEMAS Y MANUALES DE LA CALIDAD NORMATIVAS SOBRE SISTEMAS DE CALIDAD Introducción La experiencia de algunos sectores industriales que por las características particulares de sus productos tenían necesidad

Más detalles

Proceso Unificado de Rational PROCESO UNIFICADO DE RATIONAL (RUP) El proceso de desarrollo de software tiene cuatro roles importantes:

Proceso Unificado de Rational PROCESO UNIFICADO DE RATIONAL (RUP) El proceso de desarrollo de software tiene cuatro roles importantes: PROCESO UNIFICADO DE RATIONAL (RUP) El proceso de desarrollo de software tiene cuatro roles importantes: 1. Proporcionar una guía de actividades para el trabajo en equipo. (Guía detallada para el desarrollo

Más detalles

Introducción. Ciclo de vida de los Sistemas de Información. Diseño Conceptual

Introducción. Ciclo de vida de los Sistemas de Información. Diseño Conceptual Introducción Algunas de las personas que trabajan con SGBD relacionales parecen preguntarse porqué deberían preocuparse del diseño de las bases de datos que utilizan. Después de todo, la mayoría de los

Más detalles

18. Camino de datos y unidad de control

18. Camino de datos y unidad de control Oliverio J. Santana Jaria Sistemas Digitales Ingeniería Técnica en Informática de Sistemas Curso 2006 2007 18. Camino de datos y unidad de control Un La versatilidad una característica deseable los Los

Más detalles

Universidad acional Experimental Del Táchira Decanato de Docencia Departamento de Ingeniería en Informática

Universidad acional Experimental Del Táchira Decanato de Docencia Departamento de Ingeniería en Informática Universidad acional Experimental Del Táchira Decanato de Docencia Departamento de Ingeniería en Informática Metodología Evolutiva Incremental Mediante Prototipo y Técnicas Orientada a Objeto (MEI/P-OO)

Más detalles

Módulo: Modelos de programación para Big Data

Módulo: Modelos de programación para Big Data Program. paralela/distribuida Módulo: Modelos de programación para Big Data (título original: Entornos de programación paralela basados en modelos/paradigmas) Fernando Pérez Costoya Introducción Big Data

Más detalles

Resolución de problemas en paralelo

Resolución de problemas en paralelo Resolución de problemas en paralelo Algoritmos Paralelos Tema 1. Introducción a la computación paralela (segunda parte) Vicente Cerverón Universitat de València Resolución de problemas en paralelo Descomposición

Más detalles

No se requiere que los discos sean del mismo tamaño ya que el objetivo es solamente adjuntar discos.

No se requiere que los discos sean del mismo tamaño ya que el objetivo es solamente adjuntar discos. RAIDS MODO LINEAL Es un tipo de raid que muestra lógicamente un disco pero se compone de 2 o más discos. Solamente llena el disco 0 y cuando este está lleno sigue con el disco 1 y así sucesivamente. Este

Más detalles

MICROSOFT PROJECT 2010

MICROSOFT PROJECT 2010 MICROSOFT PROJECT 2010 PRESENTACIÓN Curso de administración de proyectos utilizando la herramienta informática Microsoft Project. El curso presenta conceptos teóricos de la administración de proyectos

Más detalles

PROCEDIMIENTO ESPECÍFICO. Código G083-01 Edición 0

PROCEDIMIENTO ESPECÍFICO. Código G083-01 Edición 0 Índice 1. TABLA RESUMEN... 2 2. OBJETO... 2 3. ALCANCE... 2 4. RESPONSABILIDADES... 3 5. ENTRADAS... 3 6. SALIDAS... 3 7. PROCESOS RELACIONADOS... 3 8. DIAGRAMA DE FLUJO... 4 9. DESARROLLO... 5 9.1. DEFINICIÓN...

Más detalles

Unidad 1. Fundamentos en Gestión de Riesgos

Unidad 1. Fundamentos en Gestión de Riesgos 1.1 Gestión de Proyectos Unidad 1. Fundamentos en Gestión de Riesgos La gestión de proyectos es una disciplina con la cual se integran los procesos propios de la gerencia o administración de proyectos.

Más detalles

LA LOGÍSTICA COMO FUENTE DE VENTAJAS COMPETITIVAS

LA LOGÍSTICA COMO FUENTE DE VENTAJAS COMPETITIVAS LA LOGÍSTICA COMO FUENTE DE VENTAJAS COMPETITIVAS Los clientes compran un servicio basandose en el valor que reciben en comparacion con el coste en el que incurren. Por, lo tanto, el objetivo a largo plazo

Más detalles

Plantilla para Casos de Éxito

Plantilla para Casos de Éxito Plantilla para Casos de Éxito Nombre/Actividad de la EMPRESA objeto de estudio: INSIGNA Sector al que pertenece: Presidente o gerente de la empresa: Antonio Gil Moreno Localización: Valencia Facturación

Más detalles

A continuación resolveremos parte de estas dudas, las no resueltas las trataremos adelante

A continuación resolveremos parte de estas dudas, las no resueltas las trataremos adelante Modulo 2. Inicio con Java Muchas veces encontramos en nuestro entorno referencias sobre Java, bien sea como lenguaje de programación o como plataforma, pero, que es en realidad Java?, cual es su historia?,

Más detalles

CAPÍTULO 1 Instrumentación Virtual

CAPÍTULO 1 Instrumentación Virtual CAPÍTULO 1 Instrumentación Virtual 1.1 Qué es Instrumentación Virtual? En las últimas décadas se han incrementado de manera considerable las aplicaciones que corren a través de redes debido al surgimiento

Más detalles

Capítulo 4. Requisitos del modelo para la mejora de la calidad de código fuente

Capítulo 4. Requisitos del modelo para la mejora de la calidad de código fuente Capítulo 4. Requisitos del modelo para la mejora de la calidad de código fuente En este capítulo definimos los requisitos del modelo para un sistema centrado en la mejora de la calidad del código fuente.

Más detalles

La Digitalización del Ayuntamiento. Gestión Integral

La Digitalización del Ayuntamiento. Gestión Integral prosoft.es La Digitalización del Ayuntamiento. Gestión Integral Desarrollamos su proyecto para el Fondo de Inversión Local El Real Decreto-ley, que crea el Fondo de 5.000 millones de euros, fue aprobado

Más detalles

Análisis de aplicación: Virtual Machine Manager

Análisis de aplicación: Virtual Machine Manager Análisis de aplicación: Virtual Machine Manager Este documento ha sido elaborado por el Centro de Apoyo Tecnológico a Emprendedores bilib, www.bilib.es Copyright 2011, Junta de Comunidades de Castilla

Más detalles

Guía de los cursos. Equipo docente:

Guía de los cursos. Equipo docente: Guía de los cursos Equipo docente: Dra. Bertha Patricia Legorreta Cortés Dr. Eduardo Habacúc López Acevedo Introducción Las organizaciones internacionales, las administraciones públicas y privadas así

Más detalles

Introducción En los años 60 s y 70 s cuando se comenzaron a utilizar recursos de tecnología de información, no existía la computación personal, sino que en grandes centros de cómputo se realizaban todas

Más detalles

3.1 INGENIERIA DE SOFTWARE ORIENTADO A OBJETOS OOSE (IVAR JACOBSON)

3.1 INGENIERIA DE SOFTWARE ORIENTADO A OBJETOS OOSE (IVAR JACOBSON) 3.1 INGENIERIA DE SOFTWARE ORIENTADO A OBJETOS OOSE (IVAR JACOBSON) 3.1.1 Introducción Este método proporciona un soporte para el diseño creativo de productos de software, inclusive a escala industrial.

Más detalles

Novedades en Q-flow 3.02

Novedades en Q-flow 3.02 Novedades en Q-flow 3.02 Introducción Uno de los objetivos principales de Q-flow 3.02 es adecuarse a las necesidades de grandes organizaciones. Por eso Q-flow 3.02 tiene una versión Enterprise que incluye

Más detalles

Actividad 4. Justificación de la oportunidad y análisis de necesidades. Concreción de la propuesta

Actividad 4. Justificación de la oportunidad y análisis de necesidades. Concreción de la propuesta Actividad 4 Justificación de la oportunidad y análisis de necesidades Autor: José Manuel Beas (jbeasa@uoc.edu) Concreción de la propuesta La propuesta que ha sido acordada con la consultora de esta segunda

Más detalles

Introducción. Metadatos

Introducción. Metadatos Introducción La red crece por momentos las necesidades que parecían cubiertas hace relativamente poco tiempo empiezan a quedarse obsoletas. Deben buscarse nuevas soluciones que dinamicen los sistemas de

Más detalles

LINEAMIENTOS ESTÁNDARES APLICATIVOS DE VIRTUALIZACIÓN

LINEAMIENTOS ESTÁNDARES APLICATIVOS DE VIRTUALIZACIÓN LINEAMIENTOS ESTÁNDARES APLICATIVOS DE VIRTUALIZACIÓN Tabla de Contenidos LINEAMIENTOS ESTÁNDARES APLICATIVOS DE VIRTUALIZACIÓN... 1 Tabla de Contenidos... 1 General... 2 Uso de los Lineamientos Estándares...

Más detalles

Plan de estudios ISTQB: Nivel Fundamentos

Plan de estudios ISTQB: Nivel Fundamentos Plan de estudios ISTQB: Nivel Fundamentos Temario 1. INTRODUCCIÓN 2. FUNDAMENTOS DE PRUEBAS 3. PRUEBAS A TRAVÉS DEL CICLO DE VIDA DEL 4. TÉCNICAS ESTÁTICAS 5. TÉCNICAS DE DISEÑO DE PRUEBAS 6. GESTIÓN DE

Más detalles

CAPITULO 3 DISEÑO. El diseño del software es el proceso que permite traducir los requisitos

CAPITULO 3 DISEÑO. El diseño del software es el proceso que permite traducir los requisitos 65 CAPITULO 3 DISEÑO 3.1. DISEÑO El diseño del software es el proceso que permite traducir los requisitos analizados de un sistema en una representación del software. 66 Diseño procedural Diseño de la

Más detalles

Patrones de software y refactorización de código

Patrones de software y refactorización de código Patrones de software y refactorización de código Introducción y antecedentes de los patrones de software Los patrones permiten construir sobre la experiencia colectiva de ingenieros de software habilidosos.

Más detalles

Metodología básica de gestión de proyectos. Octubre de 2003

Metodología básica de gestión de proyectos. Octubre de 2003 Metodología básica de gestión de proyectos Octubre de 2003 Dentro de la metodología utilizada en la gestión de proyectos el desarrollo de éstos se estructura en tres fases diferenciadas: Fase de Éjecución

Más detalles

Introducción a las redes de computadores

Introducción a las redes de computadores Introducción a las redes de computadores Contenido Descripción general 1 Beneficios de las redes 2 Papel de los equipos en una red 3 Tipos de redes 5 Sistemas operativos de red 7 Introducción a las redes

Más detalles

Modelo de aplicaciones CUDA

Modelo de aplicaciones CUDA Modelo de aplicaciones CUDA Utilización de GPGPUs: las placas gráficas se utilizan en el contexto de una CPU: host (CPU) + uno o varios device o GPUs Procesadores masivamente paralelos equipados con muchas

Más detalles

Metodología y Framework para el Desarrollo de Aplicaciones Científicas con Computación de Alto Rendimiento a través de Servicios Web

Metodología y Framework para el Desarrollo de Aplicaciones Científicas con Computación de Alto Rendimiento a través de Servicios Web Metodología y Framework para el Desarrollo de Aplicaciones Científicas con Computación de Alto Rendimiento a través de Servicios Web J.Corral-García, D.Cortés-Polo, C.Gómez-Martín, J.L.González-Sánchez

Más detalles

Planificación de Sistemas de Información

Planificación de Sistemas de Información Planificación de Sistemas de Información ÍNDICE DESCRIPCIÓN Y OBJETIVOS...1 ACTIVIDAD 1: INICIO DEL PLAN DE SISTEMAS DE INFORMACIÓN...4 Tarea 1.1: Análisis de la Necesidad del...4 Tarea 1.2: Identificación

Más detalles

Procedimiento de Sistemas de Información

Procedimiento de Sistemas de Información Procedimiento de Sistemas de Información DIRECCIÓN DE COORDINACIÓN TÉCNICA Y PLANEACIÓN VIEMBRE DE 2009 PR-DCTYP-08 Índice. 1. INTRODUCCIÓN.... 3 2. OBJETIVO.... 4 3. ALCANCE.... 4 4. MARCO LEGAL.... 4

Más detalles

Planificación de Sistemas de Información

Planificación de Sistemas de Información Planificación de Sistemas de Información ÍNDICE DESCRIPCIÓN Y OBJETIVOS... 1 ACTIVIDAD 1: INICIO DEL PLAN DE SISTEMAS DE INFORMACIÓN... 4 Tarea 1.1: Análisis de la Necesidad del... 4 Tarea 1.2: Identificación

Más detalles

Oficina Online. Manual del administrador

Oficina Online. Manual del administrador Oficina Online Manual del administrador 2/31 ÍNDICE El administrador 3 Consola de Administración 3 Administración 6 Usuarios 6 Ordenar listado de usuarios 6 Cambio de clave del Administrador Principal

Más detalles

Curso Online de Microsoft Project

Curso Online de Microsoft Project Curso Online de Microsoft Project Presentación El curso a distancia estudia conceptos generales sobre las tecnologías relacionadas con Internet. Conceptos que cualquier usuario de ordenadores debe conocer

Más detalles

Nicolás Zarco Arquitectura Avanzada 2 Cuatrimestre 2011

Nicolás Zarco Arquitectura Avanzada 2 Cuatrimestre 2011 Clusters Nicolás Zarco Arquitectura Avanzada 2 Cuatrimestre 2011 Introducción Aplicaciones que requieren: Grandes capacidades de cómputo: Física de partículas, aerodinámica, genómica, etc. Tradicionalmente

Más detalles

DISCOS RAID. Se considera que todos los discos físicos tienen la misma capacidad, y de no ser así, en el que sea mayor se desperdicia la diferencia.

DISCOS RAID. Se considera que todos los discos físicos tienen la misma capacidad, y de no ser así, en el que sea mayor se desperdicia la diferencia. DISCOS RAID Raid: redundant array of independent disks, quiere decir conjunto redundante de discos independientes. Es un sistema de almacenamiento de datos que utiliza varias unidades físicas para guardar

Más detalles

Colección de Tesis Digitales Universidad de las Américas Puebla. Morales Salcedo, Raúl

Colección de Tesis Digitales Universidad de las Américas Puebla. Morales Salcedo, Raúl 1 Colección de Tesis Digitales Universidad de las Américas Puebla Morales Salcedo, Raúl En este último capitulo se hace un recuento de los logros alcanzados durante la elaboración de este proyecto de tesis,

Más detalles

Proyecto Fin de Carrera

Proyecto Fin de Carrera Proyecto Fin de Carrera Gestión del Proyecto para una Plataforma online de intercambio, compra o venta de ayudas técnicas. Consultora: Ana Cristina Domingo Troncho Autor: Álvaro Fanego Lobo Junio de 2013

Más detalles

CAPÍTULO 3 VISUAL BASIC

CAPÍTULO 3 VISUAL BASIC CAPÍTULO 3 VISUAL BASIC 3.1 Visual Basic Microsoft Visual Basic es la actual y mejor representación del viejo lenguaje BASIC, le proporciona un sistema completo para el desarrollo de aplicaciones para

Más detalles

CAPÍTUL07 SISTEMAS DE FILOSOFÍA HÍBRIDA EN BIOMEDICINA. Alejandro Pazos, Nieves Pedreira, Ana B. Porto, María D. López-Seijo

CAPÍTUL07 SISTEMAS DE FILOSOFÍA HÍBRIDA EN BIOMEDICINA. Alejandro Pazos, Nieves Pedreira, Ana B. Porto, María D. López-Seijo CAPÍTUL07 SISTEMAS DE FILOSOFÍA HÍBRIDA EN BIOMEDICINA Alejandro Pazos, Nieves Pedreira, Ana B. Porto, María D. López-Seijo Laboratorio de Redes de Neuronas Artificiales y Sistemas Adaptativos Universidade

Más detalles

Planificación, Gestión y Desarrollo de Proyectos

Planificación, Gestión y Desarrollo de Proyectos Planificación, Gestión y Desarrollo de Proyectos Conceptos básicos Planificación de un proyecto Gestión de un proyecto Desarrollo de un proyecto 1 Conceptos básicos: Proyecto Conjunto de actividades que

Más detalles

El objetivo principal del presente curso es proporcionar a sus alumnos los conocimientos y las herramientas básicas para la gestión de proyectos.

El objetivo principal del presente curso es proporcionar a sus alumnos los conocimientos y las herramientas básicas para la gestión de proyectos. Gestión de proyectos Duración: 45 horas Objetivos: El objetivo principal del presente curso es proporcionar a sus alumnos los conocimientos y las herramientas básicas para la gestión de proyectos. Contenidos:

Más detalles

Gestión de proyectos

Gestión de proyectos Gestión de proyectos Horas: 45 El objetivo principal del presente curso es proporcionar a sus alumnos los conocimientos y las herramientas básicas para la gestión de proyectos. Gestión de proyectos El

Más detalles

Fundamentos del diseño 3ª edición (2002)

Fundamentos del diseño 3ª edición (2002) Unidades temáticas de Ingeniería del Software Fundamentos del diseño 3ª edición (2002) Facultad de Informática necesidad del diseño Las actividades de diseño afectan al éxito de la realización del software

Más detalles

LiLa Portal Guía para profesores

LiLa Portal Guía para profesores Library of Labs Lecturer s Guide LiLa Portal Guía para profesores Se espera que los profesores se encarguen de gestionar el aprendizaje de los alumnos, por lo que su objetivo es seleccionar de la lista

Más detalles

UNIDADES FUNCIONALES DEL ORDENADOR TEMA 3

UNIDADES FUNCIONALES DEL ORDENADOR TEMA 3 UNIDADES FUNCIONALES DEL ORDENADOR TEMA 3 INTRODUCCIÓN El elemento hardware de un sistema básico de proceso de datos se puede estructurar en tres partes claramente diferenciadas en cuanto a sus funciones:

Más detalles

El proceso unificado en pocas palabras

El proceso unificado en pocas palabras El Proceso Unificado de Desarrollo de Software Ivar Jacobson Grady Booch James Rumbaugh Addison Wesley Resumen Capítulo 1. El proceso unificado: dirigido por casos de uso, centrado en la arquitectura,

Más detalles

Capitulo V Administración de memoria

Capitulo V Administración de memoria Capitulo V Administración de memoria Introducción. Una de las tareas más importantes y complejas de un sistema operativo es la gestión de memoria. La gestión de memoria implica tratar la memoria principal

Más detalles

Unidad III. Planificación del proyecto de software

Unidad III. Planificación del proyecto de software Planificación del proyecto de software Unidad III 3.1. Aplicación de herramientas para estimación de tiempos y costos de desarrollo de software: GANTT, PERT/CPM, uso de software para la estimación de tiempos

Más detalles

Cómo seleccionar el mejor ERP para su empresa Sumario ejecutivo

Cómo seleccionar el mejor ERP para su empresa Sumario ejecutivo Índice completo de la Guía Índice completo de la Guía 1. Quién debe leer esta guía? 3 2. Qué es un ERP? 7 2.2. Qué es un ERP?... 9 2.3. Cuál es el origen del ERP?... 10 2.4. ERP a medida o paquetizado?...

Más detalles

E-learning: E-learning:

E-learning: E-learning: E-learning: E-learning: capacitar capacitar a a su su equipo equipo con con menos menos tiempo tiempo y y 1 E-learning: capacitar a su equipo con menos tiempo y Si bien, no todas las empresas cuentan con

Más detalles

Análisis de aplicación: TightVNC

Análisis de aplicación: TightVNC Análisis de aplicación: TightVNC Este documento ha sido elaborado por el Centro de Apoyo Tecnológico a Emprendedores bilib, www.bilib.es Copyright 2011, Junta de Comunidades de Castilla La Mancha. Este

Más detalles

DEPARTAMENTO: Informática. MATERIA: Programación. NIVEL: 1º Desarrollo de Aplicaciones Multiplataforma

DEPARTAMENTO: Informática. MATERIA: Programación. NIVEL: 1º Desarrollo de Aplicaciones Multiplataforma DEPARTAMENTO: Informática MATERIA: Programación NIVEL: 1º Desarrollo de Aplicaciones Multiplataforma 1. Objetivos. Competencias Profesionales, Personales y Sociales 1.1 Objetivos del ciclo formativo La

Más detalles

SISTEMAS DE INFORMACIÓN II TEORÍA

SISTEMAS DE INFORMACIÓN II TEORÍA CONTENIDO: EL PROCESO DE DISEÑO DE SISTEMAS DISTRIBUIDOS MANEJANDO LOS DATOS EN LOS SISTEMAS DISTRIBUIDOS DISEÑANDO SISTEMAS PARA REDES DE ÁREA LOCAL DISEÑANDO SISTEMAS PARA ARQUITECTURAS CLIENTE/SERVIDOR

Más detalles

App para realizar consultas al Sistema de Información Estadística de Castilla y León

App para realizar consultas al Sistema de Información Estadística de Castilla y León App para realizar consultas al Sistema de Información Estadística de Castilla y León Jesús M. Rodríguez Rodríguez rodrodje@jcyl.es Dirección General de Presupuestos y Estadística Consejería de Hacienda

Más detalles

Gestión de Configuración del Software

Gestión de Configuración del Software Gestión de Configuración del Software Facultad de Informática, ciencias de la Comunicación y Técnicas Especiales Herramientas y Procesos de Software Gestión de Configuración de SW Cuando se construye software

Más detalles

El Futuro de la Computación en la Industria de Generación Eléctrica

El Futuro de la Computación en la Industria de Generación Eléctrica El Futuro de la Computación en la Industria de Generación Eléctrica Retos a los que se enfrenta la industria de generación La industria de generación eléctrica se enfrenta a dos retos muy significativos

Más detalles

BASES DE DATOS OFIMÁTICAS

BASES DE DATOS OFIMÁTICAS BASES DE DATOS OFIMÁTICAS Qué es una Bases de Datos Ofimática?. En el entorno de trabajo de cualquier tipo de oficina ha sido habitual tener un archivo con gran parte de la información necesaria para el

Más detalles

Análisis y Diseño de Aplicaciones

Análisis y Diseño de Aplicaciones Análisis y Diseño de Aplicaciones Ciclo de Vida Docente: T/RT Gonzalo Martínez CETP EMT Informática 3er Año Introducción En el desarrollo de sistemas, el ciclo de vida son las etapas por las que pasa un

Más detalles

1. Descripción y objetivos

1. Descripción y objetivos Pruebas 1 1. Descripción y objetivos Las pruebas son prácticas a realizar en diversos momentos de la vida del sistema de información para verificar: El correcto funcionamiento de los componentes del sistema.

Más detalles

General Parallel File System

General Parallel File System General Parallel File System Introducción GPFS fue desarrollado por IBM, es un sistema que permite a los usuarios compartir el acceso a datos que están dispersos en múltiples nodos; permite interacción

Más detalles

revista transparencia transparencia y... 3.3. UNIVERSIDADES

revista transparencia transparencia y... 3.3. UNIVERSIDADES revista transparencia transparencia y... 3.3. UNIVERSIDADES 35 revista transparencia Mónica López del Consuelo Documentalista Open Data Universidad de Granada 3.3.1. El filtro básico de la transparencia.

Más detalles

RESUMEN CUADRO DE MANDO

RESUMEN CUADRO DE MANDO 1. Objetivo Los objetivos que pueden alcanzarse, son: RESUMEN CUADRO DE MANDO Disponer eficientemente de la información indispensable y significativa, de modo sintético, conectada con los objetivos. Facilitar

Más detalles

SERVICIOS. Reingeniería. Instalación / Puesta en marcha. Personalización. Cursos de formación. Servicio técnico. Servicio de mantenimiento

SERVICIOS. Reingeniería. Instalación / Puesta en marcha. Personalización. Cursos de formación. Servicio técnico. Servicio de mantenimiento Instalación / Puesta en marcha Reingeniería Personalización Cursos de formación Servicio técnico Servicio de mantenimiento Desarrollo de software Área reservada en la web Los Servicios de Software de PYV

Más detalles

Mesa de Ayuda Interna

Mesa de Ayuda Interna Mesa de Ayuda Interna Documento de Construcción Mesa de Ayuda Interna 1 Tabla de Contenido Proceso De Mesa De Ayuda Interna... 2 Diagrama Del Proceso... 3 Modelo De Datos... 4 Entidades Del Sistema...

Más detalles

INFORME Nº 052-2012-GTI INFORME TÉCNICO PREVIO DE EVALUACIÓN DE SOFTWARE

INFORME Nº 052-2012-GTI INFORME TÉCNICO PREVIO DE EVALUACIÓN DE SOFTWARE INFORME Nº 052-2012-GTI INFORME TÉCNICO PREVIO DE EVALUACIÓN DE SOFTWARE 1. Nombre del Área El área encargada de la evaluación técnica para la actualización (en el modo de upgrade) del software IBM PowerVM

Más detalles

SOFTWARE & SYSTEMS PROCESS ENGINEERING METAMODEL SPECIFICATION V.20 SPEM 2.0

SOFTWARE & SYSTEMS PROCESS ENGINEERING METAMODEL SPECIFICATION V.20 SPEM 2.0 SPEM 2.0 SOFTWARE & SYSTEMS PROCESS ENGINEERING METAMODEL SPECIFICATION V.20 SPEM 2.0 Metamodelo para modelos de procesos de ingeniería de software y de ingeniería de sistemas. La idea central de SPEM

Más detalles

GUÍA TÉCNICA PARA LA DEFINICIÓN DE COMPROMISOS DE CALIDAD Y SUS INDICADORES

GUÍA TÉCNICA PARA LA DEFINICIÓN DE COMPROMISOS DE CALIDAD Y SUS INDICADORES GUÍA TÉCNICA PARA LA DEFINICIÓN DE COMPROMISOS DE CALIDAD Y SUS INDICADORES Tema: Cartas de Servicios Primera versión: 2008 Datos de contacto: Evaluación y Calidad. Gobierno de Navarra. evaluacionycalidad@navarra.es

Más detalles

Unidades temáticas de Ingeniería del Software. Fases del proceso de desarrollo 4ª edición (2008)

Unidades temáticas de Ingeniería del Software. Fases del proceso de desarrollo 4ª edición (2008) Unidades temáticas de Ingeniería del Software Fases del proceso de desarrollo 4ª edición (2008) Facultad de Informática organización del desarrollo El ciclo de vida del software abarca el proceso de desarrollo,

Más detalles

Una computadora de cualquier forma que se vea tiene dos tipos de componentes: El Hardware y el Software.

Una computadora de cualquier forma que se vea tiene dos tipos de componentes: El Hardware y el Software. ARQUITECTURA DE LAS COMPUTADORAS QUE ES UNA COMPUTADORA (UN ORDENADOR)? Existen numerosas definiciones de una computadora, entre ellas las siguientes: 1) Una computadora es un dispositivo capaz de realizar

Más detalles

RECOMENDACIONES PARA EL DESARROLLO DE UNA PROCEMIENTO PARA LA GESTIÓN DE PROYECTOS

RECOMENDACIONES PARA EL DESARROLLO DE UNA PROCEMIENTO PARA LA GESTIÓN DE PROYECTOS CENTRO DE EXCELENCIA DE SOFTWARE LIBRE DE CASTILLA-LA MANCHA JUNTA DE COMUNIDADES DE CASTILLA LA MANCHA. RECOMENDACIONES PARA EL DESARROLLO DE UNA PROCEMIENTO PARA LA GESTIÓN DE PROYECTOS Autor del documento:

Más detalles

Propuesta de Portal de la Red de Laboratorios Virtuales y Remotos de CEA

Propuesta de Portal de la Red de Laboratorios Virtuales y Remotos de CEA Propuesta de Portal de la Red de Laboratorios Virtuales y Remotos de CEA Documento de trabajo elaborado para la Red Temática DocenWeb: Red Temática de Docencia en Control mediante Web (DPI2002-11505-E)

Más detalles

Guías _SGO. Gestione administradores, usuarios y grupos de su empresa. Sistema de Gestión Online

Guías _SGO. Gestione administradores, usuarios y grupos de su empresa. Sistema de Gestión Online Guías _SGO Gestione administradores, usuarios y grupos de su empresa Sistema de Gestión Online Índice General 1. Parámetros Generales... 4 1.1 Qué es?... 4 1.2 Consumo por Cuentas... 6 1.3 Días Feriados...

Más detalles

Unidad VI: Supervisión y Revisión del proyecto

Unidad VI: Supervisión y Revisión del proyecto Unidad VI: Supervisión y Revisión del proyecto 61. Administración de recursos La administración de recursos es el intento por determinar cuánto, dinero, esfuerzo, recursos y tiempo que tomará construir

Más detalles

Planificación en Team Foundation Server 2010

Planificación en Team Foundation Server 2010 Planificación en Team Foundation Server 2010 Planificación y Seguimientos en Proyectos Agile con Microsoft Visual Studio Team Foundation Server 2010 Dirigido a: Todos los roles implicados en un proyecto

Más detalles

2 EL DOCUMENTO DE ESPECIFICACIONES

2 EL DOCUMENTO DE ESPECIFICACIONES Ingeniería Informática Tecnología de la Programación TEMA 1 Documentación de programas. 1 LA DOCUMENTACIÓN DE PROGRAMAS En la ejecución de un proyecto informático o un programa software se deben de seguir

Más detalles

Análisis de aplicación: Cortafuegos de la distribución Zentyal

Análisis de aplicación: Cortafuegos de la distribución Zentyal Análisis de aplicación: Cortafuegos de la distribución Zentyal Este documento ha sido elaborado por el Centro de Apoyo Tecnológico a Emprendedores bilib, www.bilib.es Copyright 2011, Junta de Comunidades

Más detalles

SEMANA 12 SEGURIDAD EN UNA RED

SEMANA 12 SEGURIDAD EN UNA RED SEMANA 12 SEGURIDAD EN UNA RED SEGURIDAD EN UNA RED La seguridad, protección de los equipos conectados en red y de los datos que almacenan y comparten, es un hecho muy importante en la interconexión de

Más detalles