1. Tarea01: goldbach_serial [11-set]

Todas las tareas son estrictamente individuales. Se entrega en su repositorio de control de versiones individual.

1.1. Descripción del problema

En 1742 Christian Goldbach de Prusia le escribió una carta al matemático suizo Leonhard Euler con una conjetura:

Todo número entero mayor que 5:

  1. Si es par se puede escribir como suma de dos números primos (conjetura fuerte). Por ejemplo: 6=3+3, 8=3+5, 14=3+11=7+7.

  2. Si es impar se puede escribir como suma de tres números primos (conjetura débil). Por ejemplo.: 7=2+2+3, 9=2+2+5, 9=3+3+3.

Euler no pudo demostrar la conjetura, ni tampoco cientos de matemáticos convirtiéndolo en el problema abierto más intrigante en la historia de la teoría de números. Algunas novelas y películas hacen alusión a la conjetura, y entre los años 2000 y 2002 se ofreció un premio de un millón de dólares a quien lograse demostrarla, premio que no fue reclamado. En el 2013, tras 271 años de su aparición, el matemático peruano Harald Helfgott propuso una demostración para la conjetura débil que está actualmente sometida a revisión de pares, y en caso de validarse convertiría la conjetura débil en teorema.

Para esta entrega se necesita un programa procedimental en C que reciba una lista de números enteros en la entrada estándar y calcule todas las sumas de Goldbach que equivalen a cada número, de manera serial. Su programa producirá como resultado en la salida estándar un resumen de cuántos números fueron ingresados y cuántas sumas en total se encontraron.

Luego imprimirá una lista con los números en el el mismo orden en que fueron ingresados. Cada línea de la lista contiene un número ingresado seguido por la cantidad de sumas de Goldbach que tiene. Si el número ingresado es negativo, se considerará como que el usuario quiere que el programa además liste todas las sumas del correspondiente número positivo. A continuación se muestra un ejemplo de la salida esperada para na entrada dada.

Ejemplo de entrada:

1
2
6
7
-8
-9
10
11
12
13
-14
-21

Salida esperada:

Total: 12 numbers 19 sums

1: NA
2: NA
6: 1 sums
7: 1 sums
-8: 1 sums: 3 + 5
-9: 2 sums: 2 + 2 + 5, 3 + 3 + 3
10: 2 sums
11: 2 sums
12: 1 sums
13: 2 sums
-14: 2 sums: 3 + 11, 7 + 7
-21: 5 sums: 2 + 2 + 17, 3 + 5 + 13, 3 + 7 + 11, 5 + 5 + 11, 7 + 7 + 7

Note que las sumas deben estar ordenadas por sus valores, del menor a mayor. Su programa debe poder calcular números de 64 bits con signo, es decir, entre 0 y 2^63-1 . Si se provee un número fuera de este rango, el programa debe imprimir el texto "NA" en la salida estándar. Si se provee un texto que no sea un número, el programa debe reportar el mensaje "invalid input" en el error estándar, y terminar su ejecución. Es decir, no continuar procesando más números.

1.2. Entregables

En su repositorio individual cree una carpeta tareas/goldbach_serial/ para esta tarea. Debe mantenerse la estructura de archivos y directorios característica de un proyecto de Unix, resumido en la siguiente tabla. En las secciones siguientes encontrará detalles de cada archivo o carpeta.

Recurso Descripción Versionado

bin/

Ejecutables

No

build/

Código objeto (.o) temporal

No

design/

Diseño de la solución en UML, redes de Petri, pseudocódigo, u otros

doc/

Documentación generada por doxygen

No

src/

Código fuente (.h y .c)

test/

Casos de prueba

Makefile

Compila, prueba, genera documentación

Doxyfile

Configura Doxygen para extraer documentación del código fuente

README.md

Describe el problema resuelto y el manual de usuario. Puede usar AsciiDoc si prefiere.

.gitignore

Ignora los recursos que NO deben estar en control de versiones (columna Versionado). Puede omitirse si ya tiene un .gitignore en la raíz del repositorio.

Haga buen uso del repositorio de control de versiones. Cada commit debe tener un cambio con un significado, un mensaje significativo, y “no romper el build”. No haga “megacommits” que implementan muchas funcionalidades distintas y acumuladas en varios días de desarrollo. Recuerde los lineamientos estipulados en la carta al estudiante.

Sea severamente cuidadoso(a) al agregar archivos a control de versiones. No agregue archivos ejecutables, código binario, o carpetas que surgieron de la salida de un programa automatizado. Además de los archivos que tienen “No” en la columna “Versionado”, haga que su .gitignore ignore archivos generados por el sistema operativo, como .DS_Store en macOS o thumbs.db en Windows.

Su solución debe ser una obra intelectual propia por la naturaleza individual de la tarea. Si reutiliza código de alguna fuente, dé el respectivo crédito. Por ejemplo, si son unas pocas líneas, basta un comentario en el código fuente que diga: // Adaptado de <URL>. Si reutiliza varios archivos, la referencia debe estar en su README.md, en una sección de “Créditos”, donde liste el nombre de la biblioteca, el creador, la dirección electrónica, la licencia, si la tiene. Nota: no es necesario dar crédito a los ejemplos y materiales provistos por los docentes en este curso.

1.3. Análisis

En un archivo README.md en notación Markdown, o si lo prefiere readme.adoc en notación AsciiDoc (es indiferente el uso de mayúsculas o minúsculas en el nombre del archivo), plasme el resultado de la fase de análisis. Si no conoce el formato Markdown o AsciiDoc, asegúrese de seguir un manual o buscar un tutorial, ya que la buena calidad del código también será evaluada. Por ejemplo, un párrafo en estas notaciones se forma al separar por dos cambios de línea y no por uno. Su documento de análisis debe incluir:

  1. Una descripción del problema a resolver. Puede adaptar partes de este enunciado pero no copiarlas exactamente igual, ya que el enunciado va dirigido al estudiantado, mientras que el README va dirigido a cualquier persona que visite el proyecto y quiera entender el problema que la solución resuelve.

  2. Un manual de uso, que indica cómo construir (compilar) la solución, y cómo correrla. Conviene proveer ejemplos de cómo correr el programa, sea de forma interactiva, o en lote redireccionando la entrada estándar. También contra números primos, compuestos, e inválidos. No es necesario hacer capturas de pantalla de la terminal, se puede copiar y pegar la salida en un bloque de código de Markdown o AsciiDoc.

  3. Una sección de créditos, donde indica su nombre e información de contacto (correo institucional). Si utiliza recursos de terceros, como imágenes o archivos de código fuente, provea en esta sección el respectivo crédito.

1.4. Diseño

Para que su código sea fácil de paralelizar en tareas posteriores, su diseño de esta tarea no debe entrelazar el cálculo de las sumas de Goldbach con las impresiones, sino que las subrutinas de lectura, cálculo de las sumas, e impresión deben estar separadas e invocarse en secuencia en ese orden. Por consiguiente, necesitará diseñar una estructura de datos que almacene en memoria dinámica los números ingresados, y las sumas encontradas que serán impresas.

Para este diseño elabore un diagrama de la estructura de datos para un ejemplo de entrada con al menos tres líneas (un par de al menos dos sumas, un impar de al menos dos sumas, y un número inválido o N/A). Puede pensar en este diagrama como un rastreo de memoria o como los diagramas típicos de estructuras de datos que realizó en cursos previos de programación. El modelo resultante, es decir, los tipos de datos de la estructura, represéntelo en UML.

Exporte sus diagramas a archivos .svg o .png y almacénelos en la carpeta design/. En esta misma carpeta cree un archivo design/README.md o design/readme.adoc. En este archivo incruste las imágenes de diseño. Además provea una descripción que ayude a una persona ajena al proyecto a comprender su diseño general (“comprender el bosque”). Tip: Puede poner un hiperenlace desde el README.md de la tarea hacia su design/readme.md, lo cual permitirá navegabilidad entre los documentos del proyecto.

Su solución debe ser modular. Debe al menos tener las subrutinas de lectura, cálculo de sumas de Goldbach, impresión, y subrutina principal. La del cálculo de las sumas probablemente necesitará subdividirla. Para cada subrutina diseñe una solución en pseudocódigo en uno o varios archivos con extensión .pseudo en la carpeta design/. Si usa notación AsciiDoc, incruste estos archivos en su documento de diseño (design/readme.adoc), sino enlácelos con hiperenlaces desde su archivo de Markdown.

1.5. Documentación

Las declaraciones de subrutinas y tipos de datos (e.j struct) públicos deben estar en archivos .h, y deben estar documentados con Doxygen en inglés o español. Doxygen requiere un archivo de configuración (por defecto llamado Doxyfile), que sí debe estar en control de versiones. Se puede generar un archivo de configuración con el comando doxygen -s -g. Para que este Doxyfile sea reutilizable, genérelo en la carpeta common/ de su repositorio:

cd <path/to/repo>
cd common
doxygen -s -g

Puede agregar el archivo generado a control de versiones y hacer un commita. Luego edite el Doxyfile para que extraiga símbolos del lenguaje de programación C. Estos son algunos cambios sugeridos:

PROJECT_NAME           = "Goldbach Sums"
PROJECT_NUMBER         = 1.0.0
PROJECT_BRIEF          = "Find Golbach sums of integers"
OUTPUT_DIRECTORY       = doc
CREATE_SUBDIRS         = YES
OPTIMIZE_OUTPUT_FOR_C  = YES
EXTRACT_ALL            = YES
EXTRACT_PRIVATE        = YES
EXTRACT_PRIV_VIRTUAL   = YES
EXTRACT_PACKAGE        = YES
EXTRACT_STATIC         = YES
EXTRACT_LOCAL_METHODS  = YES
QUIET                  = YES
INPUT                  = src
RECURSIVE              = YES
HAVE_DOT               = YES

Para reutilizar el Doxyfile, vaya a la carpeta de su tarea. Cree otro archivo vacío también con el nombre Doxyfile con el siguiente contenido:

@INCLUDE = ../../common/Doxyfile

Los archivos de salida de Doxygen deben generarse en una carpeta doc/ y ésta NO se debe agregar a control de versiones. Asegúrese de que el reporte que genera Doxygen en la salida estándar, no incluya diagnósticos (warnings). Además de documentar las interfaces, documente el código no trivial en los cuerpos de las subrutinas.

Nótese que la documentación de subrutinas tiene dos partes. La primera es la interfaz de la subrutina, que debe hacerse en Doxygen. Aquí importa el qué hace la subrutina: Una oración que resume qué hace la subrutina, y puede tener una sección de detalles; qué parámetros recibe (valores esperados), qué valor retorna, qué pasa cuando se envían valores no válidos (ej. fuera de rango), y cualquier otro detalle que alguien que invoque su subrutina necesita saber, por ejemplo, si el cálculo de números primos es por fuerza bruta o algún otro algoritmo. Un ejemplo corto de documentación de una subrutina podría ser:

/**
 @brief Read @a value_count double from stdin and store them in @a values array
 @param value_count Number of elements to be read from stdin
 @param values Array of elements. It must not be NULL
 @return An error code:
   0 for success.
   1 if EOF is found before @a value_count values are read
   2 if a valid double value cannot be read from stdin
*/
int read_values(const size_t value_count, double values[]);

Si tiene dos subrutinas que tienen prácticamente la misma documentación, por ejemplo, los mismos parámetros, no redunde la documentación. Haga referencia a la subrutina ya documentada. Por ejemplo:

/// @brief Print all values of @a values array to standard output
/// @see read_values
int print_values(const size_t value_count, double values[]);

Nótese que en este segundo ejemplo se usaron comentarios en línea (///) en lugar de multi-línea (/* …​ */), por ser pocas líneas. Pero es sólo con propósitos ilustrativos. En su código usted pude escoger entre cualquiera de las formas permitidas por Doxygen, pero trate de mantener la consistencia.

La segunda parte es la implementación de la subrutina. Esto se documenta como comentarios normales (NO de Doxygen) dentro del cuerpo de la subrutina. Aquí importa el cómo, de tal forma que si alguien lee los comentarios dentro del cuerpo, obtiene un algoritmo en alto nivel (lenguaje natural) de la solución implementada. Por lo tanto, tome el texto de sus archivos de diseño (.pseudo), péguelo en el archivo fuente (.c) correspondiente, y convierta el texto en comentarios. En la fase de implementación escribirá código C cada instrucción del pseudocódigo. Un ejemplo de este proceso puede verse en video “Punto de entrada, declaraciones de variables” que es parte de una secuencia de videos titulados "Diseño procedimental #".

1.6. Implementación

Implemente su diseño (diagramas UML y pseudocódigo) en el lenguaje de programación C. Recuerde aplicar buenas prácticas de programación. Por ejemplo, las variables globales están prohibidas en esta y todas las evaluaciones del curso. Corra el linter con frecuencia para apegarse a la convención de estilos de Google para C/C++. Esta convención usa dos espacios para indentar (sangría) y bloques de código entre llaves egipcias. Los siguientes son algunos detalles técnicos que encontrará.

1.6.1. Validación de entrada

Aplique la buena práctica de programación defensiva. Su programa debe defenderse de entradas inválidas o mal intencionadas, como números incorrectos, muy grandes, o textos que no son números. Para probar si hubo un error de lectura deben hacerse las verificaciones de la subrutina que esté usando para entrada/salida, por ejemplo el valor de retorno de scanf(). Además conviene revisar la variable global errno declarada en el encabezado <errno.h> disponible en ambientes Unix, ya que algunas subrutinas de entrada y salida escriben en ella códigos de error. Nota: Después de procesar una entrada inválida, conviene limpiar la variable errno para la siguiente lectura.

1.6.2. Enteros de tamaño fijo

El linter sugiere que use tipos de datos enteros cuyo tamaño no dependa de la arquitectura. Es decir, en lugar de usar int que en algunas arquitecturas es de 2 bytes y en otras es de 4 bytes, use un tipo que siempre es de 2 o 4 bytes indiferentemente de la arquitectura, como int16_t o int32_t respectivamente. La biblioteca estándar C define estos tipos en el encabezado <stdint.h>.

El paso seguido es hacer que la entrada y salida también se adapte a los tamaños fijos de los enteros. En C++ no hay que hacer cambio alguno, ya que la E/S es genérica, pero en C se usan constantes de formato definidas en el encabezado <inttypes.h>. Esas constantes tienen la forma SCNxN para lectura (scanf) y PRIxN para impresión (printf), donde x indica si es con signo (i) o sin signo (u), mientras que N indica la cantidad de bits. Por ejemplo, el siguiente programa de C usa un tipo de datos dependiente de la arquitectura (4 o 8 bytes):

unsigned long value = 0;
if (scanf("%lu", &value) == 1) {
  printf("value = %lu\n", value);
}

Si se quiere que el entero sea de 4 bytes sin signo, el mismo programa sería:

#include <stdint.h>
#include <inttypes.h>

uint32_t value = 0;
if (scanf("%" SCNu32, &value) == 1) {
  printf("value = %" PRIu32 "\n", value);
}

1.6.3. Estructuras de datos

La biblioteca estándar de C no tiene contenedores genéricos. Las personas programadoras normalmente los implementan de forma casera, o reutiliza código de terceros. Por ejemplo, un arreglo dinámico se puede implementar con realloc(), y colas simplemente enlazadas usando registros (struct) y punteros. En C estas estructuras no tienden a ser genéricas, sino estar "mezcladas" con los tipos de datos que almacenan. En los vídeos del taller de C++ a C se provee ejemplos de estos dos contenedores..

1.7. Pruebas

Asegúrese de verificar el funcionamiento de su programa con los casos de prueba que se adjuntan. Los casos están clasificados en tres grupos de acuerdo a la cantidad de procesamiento que requieren. Puede agregar los casos pequeños y medianos a control de versiones si gusta, pero no los casos de prueba grandes. Se recomienda enfáticamente crear casos de prueba propios y agregarlos a control de versiones.

El Makefile genérico, que probablemente ya tenga en la carpeta common/ de su repositorio, permite probar su solución contra todos los casos de prueba con la orden make test. Conviene instalar el programa icdiff para obtener una comparación con colores (sudo apt icdiff). Los siguientes commandos son de especial utilidad:

Su programa debe hacer buen uso de los recursos. Por ejemplo, no debe generar fugas de memoria, ni accesos inválidos, ni usar memoria no inicializada. Estas pruebas puede realizarlas herramientas de análisis dinámico de código como Valgrind y Google Sanitizers.

make clean asan test
make memcheck
make lint
make doc

Una vez que haya finalizado la solución, realice la entrega creando un tag con el identificador tarea01hw01) asociado al último commit de la entrega. Por ejemplo:

git tag -a tarea01 -m 'Entrega de la tarea01: goldbach serial'
git push --tags

1.7.1. Optimización

Enfóquese primero en resolver el problema por corrección (“correctitud”), y por tanto, pasar los casos de prueba. Una vez que el problema esté resuelto, mida la duración de su solución con todos los casos de prueba medianos en un nodo esclavo del clúster arenal. Los siguientes comandos muestran cómo puede realizar esta tarea. Los comentarios a la derecha no debe escribirlos.

ssh CARNET@arenal.ecci.ucr.ac.cr            # Conectarse al cluster
git clone url_mi_repositorio                # Solo la primera vez
git pull --rebase                           # Si hizo cambios
cd mi_repositorio/tareas/programa_a_medir   # Ir a la tarea01 en el nodo master
make clean release                          # Generar un ejecutable optimizado

ssh compute-0-N                             # Login en la maquina esclava
cd mi_repositorio/tareas/programa_a_medir   # Ir a la tarea01 en el nodo esclavo
top                                         # Revise que no haya 100% de uso de CPU
perf stat make test TST_DIR=tests_medium    # Medir cuanto dura la solucion

Los nodos esclavos de arenal están identificados de la forma compute-0-N, donde N es un entero entre 0 y 3. Escoja al azar una de las máquinas y antes de ejecutar las mediciones asegúrese de que nadie más está corriendo tareas pesadas. Puede usar el comando top que es un monitor de procesos para saber el consumo de CPU en esa máquina. Si alguien está usando todo el CPU de la máquina, cambie a otra.

El último comando de la lista anterior le indicará la duración en segundos de correr todos los casos de prueba. Esta duración debe ser menor a 10 minutos. Si su solución tarda más de este tiempo, debe incrementar la eficiencia de su código. A modo de recomendación:

  1. Tome una hoja de papel y encuentre manualmente todas las sumas de Goldbach para un número par (ej.: 10) y para un número impar (ej.: 9). Siga el algoritmo que implementó en su solución y no se brinque pasos.

  2. Trate de determinar qué pasos son innecesarios para encontrar la solución y reducir la cantidad de procesamiento innecesario. Modifique su algoritmo.

  3. Implemente su nuevo algoritmo y pruebe que pasa los casos de prueba en su máquina local y que tiene una duración menor.

  4. Repita la medición de tiempo en la máquina esclava de Arenal para determinar si supera o no el tiempo límite. Una vez que logre una duración inferior al límite, no continúe optimizando su código. En la tarea03 tendrá oportunidad de realizar una optimización serial de forma sistemática.

1.8. Evaluación

(Rubros en revisión)

  1. [10%] Buen uso de los commits, estructura de archivos y directorios, ignorar archivos generados.

  2. [10%] Análisis (README.md): descripción del problema, el manual de usuario, créditos.

  3. [20%] Diseño de la solución: diagrama de memoria, diagrama de UML, pseudocódigo.

  4. [10%] Documentación de subrutinas y registros (doxygen) y algoritmos en los cuerpos de subrutinas.

  5. [30%] Implementación y corrección de los algoritmos y estructuras de datos (pasar los casos de prueba).

  6. [10%] Buen uso de la memoria (memcheck, asan, msan, ubsan).

  7. [10%] Modularización en subrutinas y archivos (.h, .c). Apego a una convención de estilos (cpplint).

2. Tarea02: goldbach_pthread [01-oct]

Esta tarea es una secuencia de la tarea01 y debe apegarse a las reglas de entrega de la tarea01, a excepción de los cambios solicitados en este enunciado. En particular, se mantienen las siguientes secciones de la tarea01:

  • Descripción del problema

  • Entregables

  • Análisis

  • Documentación

Para esta tarea paralilice su programa en C usando Pthreads para que el cálculo de las sumas de Goldbach se haga de forma concurrente.

2.1. Correcciones a la anterior

Si no lo ha hecho, primero corrija su solución a la tarea01 en la carpeta tareas/goldbach_serial/ (no copie ni cree una carpeta nueva) para aplicar las observaciones que obtuvo durante la revisión de la misma, de manera que no se propaguen las deficiencias identificadas de la tarea01 en la tarea02. Recuerde utilizar issues de su sistema de control de versiones de la siguiente forma.

  1. Por cada observación que recibió durante la revisión (sea que las haya anotado o que visualice la grabación de la revisión), cree un issue en el sistema de alojamiento de control de versiones (git.ucr o github). Inicie el título del issue con el número de la tarea, por ejemplo "Tarea01: cambiar arreglo estático por dinámico". Describa en el issue el problema identificado puntualmente. Note que cada issue recibe un número que lo identifica.

  2. Corrija un issue a la vez en su repositorio. Modifique los archivos que necesite directamente (no cree una copia de carpeta). Para cada corrección cree un commit y en el mensaje refiera el número del issue. Si es el último commit que resuelve el issue, ciérrelo desde el mensaje de commit. Por ejemplo, el mensaje:

    git commit -m 'Store sums in dynamic array instead of a static one. Close #13'

    cerraría el issue identificado con 13. De esta forma el issue y el commit quedan ligados en el sistema de control de versiones y facilita la rastreabilidad.

Una vez que haya corregido la tarea01, cree un tag tarea01fixhw01fix) asociado al último commit de las correcciones, y asegúrese enviarlo al repositorio remoto (git push --tags). Luego pase a la versión concurrente (tarea02).

2.2. Descripción del problema (Tarea02)

En su repositorio personal copie la carpeta tareas/goldbach_serial/ y su contenido como tareas/goldbach_pthread. Haga un commit con este cambio.

El programa concurrente recibirá la lista de números enteros en la entrada estándar e imprimirá el resumen y las sumas de Goldbach de cada uno de ellos, en el mismo orden y de la misma forma que la versión serial lo hace. Es decir, el programa concurrente debe pasar los mismos casos de prueba que la versión serial. La diferencia con la versión serial, es que la solución a esta tarea debe calcular las sumas de Goldbach de manera concurrente utilizando hilos que se reparten el trabajo, y por lo tanto, debe tener un incremento en el desempeño corroborable.

Su programa concurrente debe permitir al usuario invocarlo con un número provisto como argumento de línea de comandos, el cual indica la cantidad de hilos de ejecución que realizarán el cálculo de las sumas de Goldbach. Si este número no se provee, se debe asumir la cantidad de núcleos (CPUs) disponibles en la máquina. Recuerde actualizar su README.md para indicar que su solución usa hilos, y que el usuario puede proveer este parámetro opcional.

2.3. Diseño concurrente

Importante. Al disponer de varios hilos de ejecución, debe repartir el trabajo lo más equitativamente posible entre ellos. Haga que los hilos trabajen de forma lo más paralela posible, y por consiguiente, evitar el control de concurrencia innecesario. Recuerde que además su solución debe imprimir los resultados en el orden esperado. Se recomienda evitar la serialización de los hilos y usar una estrategia de seguridad condicional (conditionally safe).

Su solución debe tener un buen diseño concurrente expresado en diagramas y pseudocódigo:

  1. Resalte en su diagrama de la estructura de datos qué datos le corresponden a cada hilo de ejecución. Puede suponer por sencillez que dispone de dos hilos de ejecución los cuales se reparten tres números almacenados en su estructura de datos.

  2. El diseño en pseudocódigo debe centrarse en la lógica concurrente (lo nuevo en esta tarea) y no los detalles algorítmicos utilizados para calcular las sumas de Goldbach de un número (lo ya resuelto en la tarea01). Debe usar la misma notación de pseudocódigo que se ha usado en las lecciones del curso. Su pseudocódigo debe considerar la creación de estructuras de datos, lectura de números, creación de hilos, e impresión de resultados. Los hilos secundarios se encargan de repartirse el trabajo en la estructura de datos. Los hilos invocarán subrutinas para encontrar las sumas que preferiblemente deben estar definidas en otros archivos de pseudocódigo.

2.4. Implementación concurrente

Implemente su diseño concurrente en el lenguaje de programación C con la tecnología Pthreads. Recuerde aplicar buenas prácticas de programación, estilo de código (linter), y documentar las subrutinas tanto su interfaz (Doxygen) como su implementación (pseudocódigo).

En esta versión no haga optimizaciones a los algoritmos para encontrar las sumas, la tarea03 se encargará de ello. La versión concurrente debe implementar el mismo algoritmo para encontrar las sumas que la versión serial. Simplemente debe repartir el trabajo lo más equitativamente posible entre los hilos de ejecución.

La versión concurrente debe registrar un incremento de velocidad. Es decir, la duración de la versión concurrente con un caso de prueba mediano o grande debe ser menor o igual que la duración de la versión serial. Importante: no agregue los casos de prueba grandes a control de versiones (por el contrario, puede incluirlos en su archivo .gitignore).

2.5. Aritmética de precisión arbitraria [Opcional]

Haga que su solución pueda encontrar las sumas de Goldbach de números de cualquier longitud (más de 64 bits). Puede utilizar una biblioteca de aritmética de precisión arbitraria, como The GNU Multiple Precision Arithmetic Library (GMP), para la cual hay disponibles tutoriales en línea (ej.: University de Colorado Boulder). También puede consultar este ../../2021b/faq/gmp/gmp.c[ejemplo de uso en C].

Asegúrese de instalar la biblioteca y su código fuente en su distribución de Linux (ej.: sudo apt install libgmp-dev). Actualice el Makefile para que el preprocesador pueda incluir los encabezados (si es necesario) y el linker pueda enlazar los binarios de la biblioteca (LIBS=-lgmp).

Su programa debe tratar los números usando la aritmética más eficiente. Por ejemplo, al leer los números de la entrada estándar, si estos son pequeños use la aritmética de precisión fija. Si el número en la entrada desborda esta aritmética, trate de usar aritmética de precisión arbitraria. Si tampoco cumple el formato de estos números, debe considerarse como una línea inválida.

De igual forma, los hilos deben poder calcular las sumas de Goldbach usando la aritmética correspondiente. Si el número es relativamente pequeño usar aritmética de precisión fija dado que es más eficiente porque es provista por el hardware. De lo contrario, usar aritmética de precisión arbitraria, que es más lenta dado que es provista por el software (la biblioteca de terceros).

2.6. Pruebas

Asegúrese de verificar el funcionamiento de su programa con los casos de prueba. Antes de implementar precisión arbitraria, use los mismos casos de prueba de la tarea01. Para probar la precisión arbitraria, use los que se adjuntan. Se recomienda enfáticamente crear más casos de prueba.

Además de probar el buen uso de memoria (asan, msan, ubsan, memcheck), asegúrese de hacer un buen uso de la concurrencia (tsan, helgrind). Apéguese a la convención de estilos y con frecuencia pruebe el código con el linter (make lint).

Recuerde que puede probar los ejecutables instrumentalizados contra los casos de prueba. Para generar un ejecutable con un sanitizer hay que compilar todos los fuentes con el sanitizer. Si ya se tiene un ejecutable construido sin sanitizer o un sanitizer diferente, es mejor borrar (make clean) todos los archivos objeto viejos (.o) y recompilar todos los fuentes. Por ejemplo, para construir un ejecutable con asan sanitizer y probarlo, y luego otro con thread sanitizer y probarlo sería:

make clean asan test
make clean tsan test

2.7. Evaluación

(Rubros en revisión)

  1. [5%] Buen uso del repositorio (commits, ignores, tags) y directorios.

  2. [5%] Análisis: agregar concurrencia al README.md.

  3. [15%] Diseño de la solución concurrente: diagrama de memoria y pseudocódigo.

  4. [5%] Documentación de interfaces (doxygen) e implementaciones (algoritmos).

  5. [30%] Implementación y corrección de la concurrencia (pasar los casos de prueba).

  6. [10%] Buen uso de la memoria (memcheck, asan, msan, ubsan).

  7. [10%] Buen uso de la concurrencia (helgrind, tsan).

  8. [10%] Incremento del desempeño con los casos de prueba medianos o grandes.

  9. [10%] Modularización en subrutinas y archivos (.h, .c). Apego a una convención de estilos (cpplint).

  10. [+10%] Opcional: Implementa arimética de precisión arbitraria.

Recuerde que en todos los rubros se evalúan las buenas prácticas de programación.

3. Tarea03: goldbach_optimized [23-oct]

Realice optimizaciones de sus soluciones para las sumas de Goldbach que incrementen el desempeño respecto a las versiones anteriores (goldbach_serial y goldbach_pthread). Es requerido proveer evidencia de este incremento con datos, gráficas, y su respectiva discusión textual, en un documento de reporte. Además compare el comportamiento de su solución ante diferentes niveles de concurrencia.

En todas las optimizaciones es obligatorio seguir el método sugerido para optimizar. Este método es cíclico, y se deberá recorrer al menos tres veces con las optimizaciones que logren incrementar el desempeño de la solución respecto a la versión previa.

3.1. Correcciones a las tareas previas

Esta tarea es una secuencia de tarea01 y tarea02. Debe apegarse a las reglas de entrega de la tarea02 y tarea01, a excepción de los cambios solicitados en este enunciado.

Si no lo ha hecho, primero corrija su solución a la tarea anterior (tarea02) en la carpeta tareas/goldbach_pthread/ (no copie ni cree una carpeta nueva) para aplicar las observaciones que obtuvo durante la revisión de la misma, de manera que no se propaguen las deficiencias identificadas de la tarea02 en la tarea03. Siga el mismo procedimiento indicado en Section 2.1.

3.2. Documento de reporte

Una vez concluidas las correcciones a la tarea02, copie su solución concurrente tareas/goldbach_pthread a la carpeta tareas/goldbach_optimized en su repositorio de control de versiones. Cree una carpeta tareas/goldbach_optimized/report/.

Dentro de la carpeta report cree un archivo readme.ext cuya estensión varía si es en formato de Markdown, AsciiDoc, o LaTeX. A este archivo se le referirá como documento de reporte. Conviene crear un enlace desde tareas/goldbach_optimized/README.md a su documento de reporte para que facilite la navegación.

Importante: El documento de reporte tendrá una sección por cada optimización solicitada en este enunciado. Dentro de cada optimización cree una subsección para cada iteración del método sugerido para optimizar. En cada iteración documente el diseño de la modificación que piensa incrementará el desempeño, el rendimiento (speedup y eficiencia) del código antes y después de modificarlo, y una discusión de las lecciones aprendidas.

3.3. Pruebas

Asegúrese de verificar el funcionamiento de su programa con los casos de prueba pequeños o medianos antes de iniciar las mediciones con los casos de prueba grandes. Pruebe el buen uso de memoria (asan, msan, ubsan, memcheck) y de la concurrencia (tsan, helgrind) junto con los casos de prueba pequeños y medianos. Por ejemplo, para construir un ejecutable con el Address Sanitizer, probarlo, y luego otro con Thread Sanitizer y probarlo se puede con:

make clean asan test
make clean tsan test

Recuerde mantener la convención de estilos (make lint).

3.4. ¿Cómo medir el tiempo de ejecución?

Note
Siempre antes de realizar una medición de rendimiento, su solución debe pasar los casos de prueba pequeños y medianos sin provocar mal uso de memoria o de la concurrencia.

Para todas las mediciones de rendimiento use todos los casos de prueba de la carpeta tests_large/ en una máquina con al menos 8 núcleos de CPU. Puede consultar la cantidad de núcleos de su máquina con el comando lscpu de Linux. Si no dispone de un equipo con estas características, use un nodo esclavo (y no la máquina máster) del clúster Arenal o una máquina del laboratorio de computadoras para el curso. Anote los resultados en una hoja de cálculo que le facilite realizar las comparaciones. Puede usar la hoja de cálculo modelo como punto de partida.

Para medir el tiempo de ejecución con precisión de nanosegundos, instale la herramienta perf (paquete linux-perf en Debian o perf en RedHat). En Debian requiere permisos de administración para permitir a perf inspeccionar a otros programas, puede requerir configuración adicional para correrlo como usuario normal (como crear el archivo sudo echo 1 >/proc/sys/kernel/perf_event_paranoid y reiniciar el sistema operativo). Para obtener la duración de todos los casos de prueba puede ejecutar perf stat make test ARG=8 TST_DIR=tests_large en su máquina de 8 o más núcleos de CPU.

Para cada medición concurrente deben realizarse tres corridas y tomar la menor de ellas (para la medición serial basta una corrida). Todas las ejecuciones deben realizarse en la misma máquina. Si usa Arenal, debe ser en un nodo esclavo (no la máquina máster), identificado con el número N entre 0 y 3 en los siguientes comandos:

ssh CARNET@arenal.ecci.ucr.ac.cr
git clone url_mi_repositorio                # Solo la primera vez
git pull --rebase                           # Si hizo cambios
cd mi_repositorio/tareas/programa_a_medir
make clean release

ssh compute-0-N
cd mi_repositorio/tareas/programa_a_medir
nohup perf stat make test ARG=8 TST_DIR=tests_large &
nohup perf stat make test ARG=8 TST_DIR=tests_large &
nohup perf stat make test ARG=8 TST_DIR=tests_large &
less nohup.out  # Tomar la menor de las tres duraciones anteriores

El programa nohup hace que el comando que le sigue, sea ejecutado en segundo plano y no se detenga si se cierra su conexión con la máquina remota, por ejemplo, por inactividad o un error de red. La salida que un programa genere es enviada al archivo nohup.out. Por lo tanto, usted puede cerrar la sesión, regresar una hora después y consultar la salida contenida en el archivo nohup.out (ej.: less nohup.out).

Si su programa corre en el clúster de Arenal y ocurre una de las siguientes situaciones: (1) está corriendo en el nodo máster, o (2) tiene más de una hora de ejecutarse en un nodo de esclavo. Entonces, debe detenerlo de inmediato como se indica a continuación.

Para detener un proceso, puede usar el comando ps -eu | grep make que le listará los procesos que tiene corriendo en la máquina que tienen make en su nombre. El primer número que vea de resultado es el process ID, usualmente referido como PID. Para matarlo puede usar el comando kill PID reemplazando PID por el número de proceso. Si eso no lo detiene, agregue kill -9 PID. También puede intentar con el comando pkill que en lugar del número de proceso recibe el nombre del mismo, algo como pkill make.

3.5. Optimización 1: serial

La primera optimización es propuesta libremente por usted, siempre y cuando demuestre empíricamente que incrementa el desempeño. Esta optimización se debe realizar sobre el cálculo de las sumas de Goldbach. Realice todos los pasos del ../material/#optimization[método sugerido para optimizar^], como se indica a continuación.

En el paso 1, para poder comparar posteriormente, tome como punto de partida los resultados de la versión serial. Es decir, realice una medición de tiempo como se indica en la Section 3.4 usando su solución serial de la tarea01 con los casos de prueba pequeños o medianos. Nota: Si usted modificó/optimizó el algoritmo para calcular las sumas de Goldbach en la tarea02, mida su solución de la tarea02 con 1 hilo de ejecución.

En el paso 2 realice un análisis dinámico de rendimiento (profiling). Ejecute su solución de goldbach_optimized con la herramienta callgrind con un caso de prueba mediano y sólo 1 hilo de ejecución. Visualice la salida con KCachegrind y determine las regiones de código que más demandan procesamiento o aquellas que las invocan. Haga una captura de pantalla y agréguela a su documento de reporte y explique resumidamente cómo el diseño de su optimización piensa disminuir este consumo.

En el paso 3 y 4 documente rápidamente (un párrafo o menos) en qué consiste la optimización en su documento de reporte. Luego implemente en código la potencial mejora.

En el paso 5 recuerde siempre hacer tres corridas, es decir: en la misma máquina, con los mismos casos de prueba, con sólo 1 hilo secundario de ejecución, y tomar la menor duración de tres corridas. Anote los resultados en su hoja de cálculo y estime el incremento de velocidad y eficiencia. Escriba en su documento de reporte estos valores. Si no logró incrementar el rendimiento, conjeture en su discusión a qué se podría deber ese resultado y repita el ciclo de optimización.

3.6. Optimización 2: Pthreads

Paralelizar el consumo de procesamiento de una solución es ya una optimización. Por lo tanto, el trabajo realizado en la tarea02 es una optimización de paralelismo de datos. Esta optimización está también implementada en goldbach_optimized. Corra esta solución con tantos hilos de ejecución como CPUs tenga disponible su sistema contra los casos de prueba pequeños o medianos. Asegúrese de pasarlos, no provocar fugas o accesos inválidos de memoria, y mantener las convenciones de estilo.

Realice la segunda medición de tiempo como se indica en la Section 3.4 usando goldbach_optimized. Use las mismas condiciones que la optimización 1, los mismos casos de prueba, y con tantos hilos secundarios como CPUs hay disponibles en la máquina. Agregue sus resultados a la hoja de cálculo. Más adelante esta medición le proveerá evidencia del incremento de desempeño logrado gracias a la concurrencia.

3.7. Optimización 3: Mapeo dinámico

La tercera optimización es implementar mapeo dinámico para la asignación de trabajo a los hilos de ejecución. Usted debe escoger la unidad de descomposición que provea el mayor incremento de desempeño. Por ejemplo, si escoge como unidad de descomposición los números que se leyeron de la entrada estándar, estos serán repartidos de forma dinámica entre los hilos secundarios.

Note
Si en la tarea02 (optimización 2) usted realizó mapeo dinámico, puede omitir la optimización 3, tanto en esta sección como las comparaciones y en el reporte de resultados. Es decir, usted deberá reportar los resultados de la versión serial y dos optimizaciones.

Su implementación del mapeo dinámico debe seguir el patrón productor-consumidor, donde el productor puede ser el hilo principal, y los consumidores son los hilos secundarios. La cantidad de hilos secundarios debe poderse ajustar mediante un argumento de línea de comandos. Si éste se omite, se debe asumir la cantidad de núcleos de procesador disponibles en el sistema.

Sugerencias. De acuerdo al patrón, la comunicación entre los productores y consumidores se realiza mediante un buffer acotado o no, que puede ser un arreglo circular o una cola thread-safe. Sin embargo, si implementa además seguridad condicional (un arreglo de resultados), puede simplificar el buffer, por ejemplo, mediante un contador entero protegido por mutex.

Siga todos los pasos del método sugerido para optimizar. Como paso 1, la medición que realizó en la optimización 2 servirá como punto de partida. Nota: Para la optimización 3 no es necesario realizar el paso 2 de profiling. El paso 3 corresponde a implementar este mapeo dinámico. Las mediciones en el paso 5 debe realizarlas en la misma máquina que las dos optimizaciones previas, con los mismos casos de prueba, tantos hilos como CPUs, y siempre tomando la menor de tres corridas.

3.8. Comparación01: de optimizaciones

Ya que ha realizado varias optimizaciones, compárelas entre ellas para determinar cuál aportó más al incremento del desempeño o genera mejor eficiencia. Todas las mediciones han de ser comparables, por tanto, deben realizarse en la misma máquina, con los mismos casos de prueba, y tomar la menor duración de tres corridas. Las versiones de sus programas goldbach_x a medir son:

Versión Descripción

serial

La versión serial: tarea01

optim1

La optimización al algoritmo serial de calcular sumas

pthread

La versión concurrente: tarea02

dynamic

La versión concurrente con mapeo dinámico

Nota: A la tabla anterior puede agregar optim04, optim05, y subsiguientes, si realizó varias iteraciones por el método sugerido para optimizar que lograran incrementar el desempeño.

Todas las mediciones deben realizarse con los casos de prueba grandes, y tantos hilos como la cantidad de núcleos disponibles en el sistema (ocho para el nodo esclavo del clúster Arenal), a excepción de la versión serial. Si no tiene alguna de las mediciones anteriores realícela y agréguela a la hoja de cálculo. Para todas las mediciones calcule el incremento de velocidad respecto a la versión serial, y la eficiencia.

Cree un gráfico en la hoja de cálculo. En el eje X muestre la versión del programa, correspondiente a la columna “Versión” de la tabla (serial, optim1, pthread, dynamic). El gráfico debe ser combinado con dos ejes Y. El eje Y primario, ubicado a la izquierda del gráfico, indica la duración de las mediciones en segundos. Este debe ser el eje de referencia para serie de mediciones de tiempo. El eje Y secundario, a la derecha del gráfico, indica el incremento de velocidad (speedup) y aunque no tiene unidades puede indicar la palabra “veces”. Este debe ser el eje de referencia para la serie de incrementos de velocidad.

Distinga visualmente las series (duración, speedup, eficiencia). Por ejemplo, puede usar líneas continuas para las primeras y punteadas para las segundas.

Cree en su hoja de cálculo un segundo gráfico que permita comparar el speedup en el eje Y primario contra la eficiencia en el eje Y secundario.

Incluya una sección “Comparación de optimizaciones” en su documento de reporte. Haga una discusión de máximo 500 palabras, en la que compare las versiones de su solución e indique qué produjo el mayor incremento de desempeño. Exporte los gráficos a formato SVG o PNG e incrústelos en la sección de discusión.

3.9. Comparación02: grado de concurrencia

Ahora que dispone de programas medibles, uno de los aprendizajes más enriquecedores es poder compararlos en diversas circunstancias, como diferente hardware, casos de prueba, y grado de concurrencia, entre otros. En esta sección podrá comparar sus soluciones respecto a este último rubro.

Todas las mediciones han de ser comparables, por tanto, deben realizarse en la misma máquina, con los casos de prueba grandes, y tomar la menor duración de tres corridas. Lo que varía en cada medición es la cantidad de hilos de ejecución, obtenidas en función de la constante C definida como la cantidad de núcleos de procesador disponibles en un sistema. Por ejemplo, para un nodo esclavo del clúster Arenal, C equivale a 8 núcleos.

Para realizar las comparaciones, se usarán 6 niveles de concurrencia en función de C, es decir, usted registrará en su hoja de cálculo 6 mediciones de la duración de sus programas, listadas en la tabla a continuación. Dado que para lograr una medición más fiable, debe ejecutar su programa al menos tres veces y tomar la duración de menor duración, en total realizará alrededor de 16 ejecuciones de sus programas. En la última columna de la siguiente tabla se muestra la cantidad de hilos para el nodo esclavo del clúster Arenal.

# Leyenda Descripción Hilos

1

1

Un solo hilo

1

2

hC

Tantos hilos como la mitad de CPUs hay en la computadora que ejecuta el programa

4

3

1C

Tantos hilos como CPUs hay en la computadora que ejecuta el programa

8

4

2C

Dos hilos por cada CPU que hay en la computadora que ejecuta el programa

16

5

4C

Cuatro hilos por cada CPU que hay en la computadora que ejecuta el programa

32

6

D

Tantos hilos como números recibe el programa en la entrada estándar

D

La medición 1 en la tabla (1) corresponde a la medición que realizó de su programa serial (goldbach_serial) con los casos de prueba grandes. Las restantes seis mediciones deben realizarse con goldbach_optimized con tres ejecuciones de los casos de prueba. Lo que varía en cada medición es la cantidad de hilos a crear, el cual es el argumento con que se invoca su programa en línea de comandos o make test ARG=#. Anote la menor de las tres (o más) ejecuciones en su hoja de cálculo. Posiblemente ya tiene varias de estas mediciones realizadas.

Una vez que tenga todas las mediciones calcule el incremento de velocidad (speedup) de cada medición respecto a la serial (incluso la serial misma). Calcule también la eficiencia de cada medición.

Cree un gráfico en la hoja de cálculo. En el eje X muestre la cantidad de hilos que realizaron los cálculos durante la ejecución del programa, correspondiente a la columna “Leyenda” de la tabla (1, hC, 1C, 2C, 4C y D). El gráfico debe ser combinado con dos ejes Y. El eje Y primario, ubicado a la izquierda del gráfico, indica el incremento de velocidad (speedup) y aunque no tiene unidades puede indicar la palabra “veces”. Éste debe ser el eje de referencia para la serie de incrementos de velocidad. El eje Y secundario, a la derecha del gráfico, indica la eficiencia obtenida por cada nivel de concurrencia. De esta forma permitirá comparar el incremento de desempeño (speedup) contra la eficiencia. Distinga visualmente las series de speedup de las de eficiencia. Por ejemplo, puede usar líneas continuas para las primeras y punteadas para las segundas.

Finalmente, cree una sección "Grado de concurencia" en su reporte, y realice un análisis de los resultados obtenidos. Exporte los gráficos a formato SVG o PNG e incrústelos como imágenes. Esta discusión debe ser concisa, de máximo 500 palabras, con su interpretación de los gráficos. A partir de sus resultados responda a la pregunta ¿cuál es la cantidad de hilos óptima para conseguir el mejor rendimiento?. Considere tanto el incremento de velocidad como la eficiencia en su respuesta.

3.10. Evaluación

  1. [20%] Optimización 1 (propuesta libre): Implementación y corrección. Pasar los casos de prueba y obtener incremento de desempeño.

  2. [20%] Optimización 3 (mapeo dinámico): Implementación y corrección. Pasar los casos de prueba y obtener incremento de desempeño.

  3. [10%] Documentación en el reporte de las iteraciones realizadas siguiendo el método sugerido para optimizar (al menos las dos optimizaciones anteriores) que incluye: diseño de la optimización, speedup/eficiencia antes y después, y lecciones aprendidas.

  4. [15%] Comparación de optimizaciones: Hoja de cálculo con las mediciones, incrementos de velocidad, eficiencia, y gráficos combinados. Discusión de resultados y gráficos en el documento de reporte.

  5. [15%] Comparación del grado de concurrencia: Hoja de cálculo con las mediciones, incrementos de velocidad, eficiencia, y gráficos combinados. Discusión de resultados y gráficos en el documento de reporte.

  6. [10%] Buen uso de la memoria (memcheck, asan, msan, ubsan) y la concurrencia (helgrind, tsan).

  7. [5%] Documentación de interfaces (doxygen) e implementaciones (algoritmos).

  8. [5%] Apego a una convención de estilos (cpplint). Modularización en subrutinas y archivos (.h, .c). Buen uso del repositorio (commits, ignores, tags) y directorios, como se ha indicado en tareas anteriores.

Recuerde que en todos los rubros se evalúan las buenas prácticas de programación. En todos los rubros que generan documentación se evalúa el buen uso de la comunicación escrita, tal como lo estipula la carta al estudiante.

4. Tarea04: goldbach_openmp_mpi [20-nov]

En las tareas anteriores usted escribió programas que reciben números enteros en la entrada estándar y calculan sus sumas de Goldbach de forma serial (tarea01), concurrente con Pthreads (tarea02), y comparó su rendimiento (tarea03). Estas soluciones aprovechan los recursos concurrentes de una única máquina, pero no escalan, ni aprovechan varias computadoras disponibles en una organización o un clúster de computadoras.

En esta tarea su objetivo es evaluar una tecnología declarativa y distribuir el trabajo de las sumas entre varias máquinas usando dos tecnologías: OpenMP y MPI. Su programa recibirá los números en la entrada estándar en el mismo formato, y deberá producir los resultados en el mismo orden, de tal forma que pase los casos de prueba.

4.1. Correcciones a la Tarea03

Si no lo ha hecho, primero corrija su solución a la tarea03 en la carpeta tareas/goldbach_optimization/ (no copie ni cree una carpeta nueva) para aplicar las observaciones que obtuvo durante la revisión de la misma, de manera que no se propaguen las deficiencias identificadas de la tarea03 en la tarea04. Siga el mismo procedimiento indicado en Section 2.1.

4.2. Concurrencia declarativa

Una vez concluidas las correcciones a la tarea03, copie su solución concurrente tareas/goldbach_optimization a la carpeta tareas/goldbach_omp_mpi en su repositorio de control de versiones. Recuerde que gracias a la tarea03, esta carpeta ya incluye el documento de reporte al cual se le harán modificaciones en esta tarea04.

Reemplace la concurrencia imperativa implementada con Pthreads, por concurrencia declarativa provista por la tecnología de paralelismo de datos OpenMP, para paralelizar el trabajo de las sumas de Goldbach. Asegúrese de que su implementación pasa los casos de prueba, y no genera diagnósticos del linter (es probable que otras herramientas de análisis dinámico de código no funcionen con esta tecnología).

Al igual que la tarea02, haga que los hilos trabajen de forma lo más paralela posible, y por consiguiente, evitar control de concurrencia innecesario. Recuerde que además su solución debe imprimir los resultados en el orden esperado. Se recomienda evitar la serialización de los hilos y usar una estrategia de seguridad condicional (conditionally safe).

La cantidad de hilos es controlada por argumento de línea de comandos. En caso de omitirse se debe suponer la cantidad de CPU disponibles en la máquina donde corre el programa.

4.3. Comparación Pthreads-OpenMP

Como hubo un cambio en la implementación de la concurrencia, compare el rendimiento de su implementación con OpenMP contra la de Pthreads siguiendo el mismo procedimiento para obtener mediciones que aplicó en la tarea03. Utilice tantos hilos como núcleos de CPU hay en el sistema con los casos de prueba grandes que usó para la tarea03.

Calcule el incremento de velocidad (speedup) y la eficiencia respecto a la versión serial (tarea01). Agregue este resultado al gráfico combinado de la Comparación01 con el identificador openmp en el eje-X. Agregue al documento de reporte en la sección de "Comparación de optimizaciones" un párrafo que compare los resultados de la concurrencia declarativa (OpenMP) contra la concurrencia imperativa (Pthreads).

4.4. Distribución

Para hacer su solución escalable y aprovechar un clúster de computadoras, distribuya el trabajo de calculo de sumas utilizando la tecnología MPI. Tome en cuenta que su programa debe leer los números de la entrada estándar y no de archivos ni argumentos de línea de comandos. Esto impone restricciones que afectan el diseño de su solución.

Descomponga la unidad de trabajo a distribuir, ésta puede tener la misma o mayor granularidad que la unidad usada para OpenMP. Implemente un mapeo dinámico usando paso de mensajes para repartir las unidades de trabajo entre los procesos o hilos, de tal forma que los recursos del clúster se aprovechen lo más equitativamente posible.

Asegúrese en su máquina local de que su solución distribuida con MPI pase los casos de prueba pequeños y medianos usando distintas cantidades de procesos e hilos. Además su implementación no debe generar diagnósticos del linter.

4.5. Comparación OpenMP-MPI

Compare el rendimiento de su solución distribuida con los casos de prueba grandes que usó en la tarea03. En este caso, es necesario usar un ambiente distribuido (como el clúster de Arenal), y no una máquina individual. Cree tantos procesos como nodos esclavos hayan en el clúster, y en cada proceso tantos hilos como CPUs hayan disponibles en el nodo. Dado que uno de los procesos se encargará del mapeo exclusivamente, éste puede correr en el nodo máster.

Mida la duración de la versión distribuida, calcule el incremento de velocidad y la eficiencia respecto a la versión OpenMP. Agregue los resultados a su gráfico de la Comparación01, de tal forma que en el eje-x se pueda comparar la versión distribuida (mpi) con las versiones previas (OpenMP, Pthreads, serial) que sean comparables, y por lo tanto, que hayan corrido en el mismo hardware. Agregue a su discusión un párrafo con el análisis de los hallazgos que obtuvo con la versión distribuida.

Note
Si realizó las mediciones de rendimiento de la tarea03 en una máquina distinta al clúster, haga nuevas mediciones en el clúster de al menos las versiones: dynamic, openmp, y mpi. Cree nuevos gráficos y una sección "Comparación de rendimiento distribuido" en su reporte, en lugar de agregar mediciones no comparables a las secciones y gráficos hechos en tareas previas.

4.6. Evaluación

  1. [20%] Concurrencia declarativa: Implementación y corrección. Pasar los casos de prueba y obtener incremento de desempeño respecto de su versión serial.

  2. [20%] Distribución: Implementación y corrección. Pasar los casos de prueba y obtener incremento de desempeño respecto de la versión no distribuida.

  3. [20%] Mapeo dinámico de la carga de trabajo usando paso de mensajes.

  4. [15%] Comparación Pthreads-OpenMP: Hoja de cálculo con las mediciones, incrementos de velocidad, eficiencia, y gráfico combinado. Coherencia de la discusión de resultados y el correspondiene gráfico.

  5. [15%] Comparación OpenMP-MPI: Hoja de cálculo con las mediciones, incrementos de velocidad, eficiencia, y gráfico combinado. Coherencia de la discusión de resultados y el correspondiene gráfico.

  6. [5%] Documentación de interfaces (doxygen) e implementaciones (algoritmos).

  7. [5%] Apego a una convención de estilos (cpplint). Modularización en subrutinas y archivos (.h, .c). Buen uso del repositorio (commits, ignores, tags) y directorios, como se ha indicado en tareas anteriores.

Recuerde que en todos los rubros se evalúan las buenas prácticas de programación. En los rubros que generan documentación se evalúa el buen uso de la comunicación escrita, tal como lo estipula la carta al estudiante.