Documentos de Académico
Documentos de Profesional
Documentos de Cultura
mpp1 Memoria
mpp1 Memoria
13 de octubre de 2021
Índice
1. Zonas paralelizables 2
1.1. print_matrix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2. generar_matriz_distancias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3. print_solucion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4. aleatorio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5. hay_iguales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.6. find_element . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.7. crear_individuo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.8. Funciones de comparación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.9. aplicar_mh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.9.1. Bucle de creación de individuos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.9.2. Bucle cruzar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.9.3. Bucles mutar y fitness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.9.4. Bucle free . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.10. Cruzar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.11. distancia_ij y fitness . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.12. mutar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
5. Uso de directivas 7
1
1. Zonas paralelizables
En este ejercicio, hacemos un análisis de las zonas del código que sean paralelizables. Empezando con el fichero
io.c
1.1. print_matrix
Consideramos que al no haber dependencias de datos entre las distintas iteraciones, se puede paralelizar sin
problema. Contamos con un bucle for, que podremos paralelizar.
1.2. generar_matriz_distancias
Dentro de esta función, encontramos primeramente una llamada a la función srand, que genera una semilla para
las consecuentes funciones rand. Esta última no es thread safe, siendo su hermana con esta característica rand_r.
Obviando el párrafo anterior, consideramos que el bucle es paralelizable, al no contar con sentencias que accedan
a posiciones de memoria en espacio similar, de forma que no tenemos colisión entre los accesos.
1.3. print_solucion
En este caso, consideramos que esta función no es paralelizable, al tener especial énfasis en la ordenación de
los individuos en la solución. Tenemos varias funciones para ordenar. Además, se podría forzar que todos los hilos
impriman en orden, pero no tendríamos el beneficio de la paralelización.
1.4. aleatorio
Esta función puede ser paralelizada en caso de que usemos la alternativa thread-safe rand_r.
1.5. hay_iguales
Como solo estamos haciendo una comparación entre dos elementos del array de enteros de un individuo. Además,
consideramos que no debería haber problema de programación teniendo en cuenta que tenemos que programar a
los hilos para que en cuanto uno retorne 0, todos deben salir o morir.
1.6. find_element
Esta función es especial ya que cuenta con un bucle while. De forma individual, no podríamos paralelizarla, pero
podemos transformarla a un for, retornando todos los hilos en caso de que encuentres el elemento buscado.
1.7. crear_individuo
Al contar con un bucle while, deberemos realizar una transformación a for para poder paralelizar. De todas
formas, consideramos que no tenemos mucha mejora de tiempo, de forma que tenemos que plantearnos si realmente
merece la pena la paralización de esta función/bucle.
2
1.9. aplicar_mh
Dentro de esta función, al ser la principal de definición de nuestro algoritmo genético, dividiremos las distintas
partes para realizar la paralelización de forma individual. Además, como queremos que algunas partes sean realizadas
solo por el padre, como puede ser la ordenación.
1.10. Cruzar
La función cruzar es totalmente paralelizable, pudiendo hacer uso de directivas parallel for para cada uno de
los bucles internos.
1.12. mutar
Para realizar la paralelización de la función mutar, tendremos que hacer el cambio del bucle while a for, de forma
que podamos acceder al número de mutantes actuales de forma privada.
3
unsigned int seed = (unsigned int) (time(NULL)+ getpid());
f = (double) rand_r(&seed) / ((double) RAND_MAX + 1);
int aleatorio(int n) {
int seed = (int) rand() % RAND_MAX;
return ((int) (rand_r(&seed) % n)); // genera un numero aleatorio entre 0 y n-1
}
Una vez se hacen estos cambios, podemos encontrar errores de compilación, al ser una función algo especial.
Dicho esto, la forma de resolver estos problemas (compilando con el fichero Makefile) es el cambio de las siguientes
flags:
-std=c99: cambiamos a -std=gnu99 para contemplar la función rand_r.
-Wall: esta función no funciona con -Wall añadido, la eliminamos.
Para probar en todo momento que los cambios realizados no inducen a error o resultados no deseados hemos
empleado una serie de asserts, los primeros fueron destinados a comprobar que tanto la función paralelizada como
la no paralelizada no divergían el resultado, posteriormente empleamos un assert acompañado de una pequeña
medición del tiempo que comprobaba que la paralelización mejoraba el mismo ya que al presentar un mayor número
de recursos y complejidad es requisito indispensable que haya una mejora de velocidad. Si bien este segundo grupo
de asserts daba resultados contradictorios mientras se ejecutaban en un ordenador personal, al llevarlo al clúster
Heterosolar se cumplió todas las veces.
Para poder hacer las pruebas de cara al ejercicio cuatro así como tener un mejor control de todas las variaciones
del código creamos distintas ramas que parten y modificaban la función no paralelizable.
En caso de la rama atomic incluimos la instrucción #pragma omp atomic sobre la línea fitness_value +=
valor, de esta manera nos aseguramos de que un mismo hilo ejecute las dos instrucciones que recaen sobre la línea
anteriormente mencionada (temp = fitness_value + valor y fitness_value = temp).
4
Por otro lado en la rama critical se ha añadido la instrucción #pragma omp critical encima de la línea
anteriormente mencionada, esto lo que hace es marcar como sección crítica un bloque de código para que haya
especial cuidado en no modificar o acceder a variables que pueden ser modificadas por otro hilo en un orden que
pueda inducir a error. Poniendo la instrucción justo antes de la línea marcamos una pequeña parte del código por
lo que evitamos uno de los grandes problemas de usar critical, realizar una sobrecarga mayor a la necesaria con
esta directiva.
En último lugar empleamos reduction para esto nos vamos a la línea inmediatamente superior a los bucles
for anidados e incluimos #pragma omp parallel for private(fitness_value) reduction(+:fitness_value).
Esto marca como variable privada la variable fitness_value, ya que al ser local, puede ser sobrescrita de una
manera incorrecta. Y también marcamos como reduction la operación que involucra esta misma variable. Ya que
como mencionamos en atomic, este tipo de instrucciones que involucran más de un paso pueden generar errores a
la hora de la paralelización.
Como vemos en la tabla adjuntada, incluimos los distintos tiempos de las ejecuciones de los programas, contando
como variables tanto el número de hilos (que hemos configurado con omp_set_num_thread(x)) como las directivas
reduction/critical/atomic.
Cabe mencionar que para todos los casos, al ser directivas que fuerzan la ejecución respetando las secciones
críticas o derivados, contamos con resultados similares, especialmente para las directivas reduction y atomic, que
ambas aplican paralelización similar. critical, al ser una directiva que fuerza que el código sea ejecutado por solo
un subproceso a la vez, obtiene resultados subpares, tardando algo más que los otros dos.
5
4. Diferentes cláusulas de Scheduling
Contamos con diferentes cláusulas para poder configurar el orden de ejecución de los procesos. En este caso,
hacemos pruebas con:
static: en orden de creación.
dynamic: en cuanto terminan, cogen más trabajo, de forma que es posible que algunos terminen antes que
otros.
guided: parecido a dynamic, pero se realiza cierta computación para encontrar cual es el mejor hilo en cada
momento.
Cómo bien vemos en la tabla anterior, para el estudio de las diferentes cláusulas de Scheduling, hemos realizado
mediciones con distintos valores de entrada, que van desde valores pequeños (500,200,100,50) a valores más grandes
(2000,800,400,200), por otro lado para una mejor muestra también variamos el número de hilos que si bien nos
hubiese gustado tener más muestras, con la finalidad de no sobrecargar el clúster compartido hemos tomado 4
valores distintos (2,8,16 y 24).
Podemos ver que según la cláusula aplicada tenemos distintos tiempos bien remarcados conforme cambiamos
tamaño de población o hilos. Al emplear dos hilos vemos que salvo en el tamaño mas grande de muestra donde
guided obtiene un rendimiento muy superior los tres tipos de cláusulas tienen un rendimiento muy superior, esto
puede ser debido a que al tener simplemente dos procesos la estrategia usada no repercute tanto como cabría esperar.
Por otro lado con el uso de ocho hilo, podemos apreciar que la cláusula guided para pequeños tamaños de
población pierde rendimiento frente a static esto puede ser debido a que al reservar recursos tiene un sobre coste
que no puede ser compensado en ejecuciones cortas, mientras que en en las dos últimas ejecuciones obtiene el mismo
tiempo y uno mucho mejor respectivamente. Siendo static recomendado para ejecuciones rápidas y dynamic un
termino intermedio que no sobresale
En el siguiente caso de 16 hilos, vemos que se repite la conclusión anteriormente expuesta, static o tiene un mejor
rendimiento para ejecuciones rápidas mientras que guided va ganando rendimiento conforme avanza la complejidad
del problema.
6
En este último caso de 24 hilos, obtenemos unas métricas aún más parejas en las que se puede apreciar una
ligera ventaja de la cláusula guided mientras que dynamic sigue sin despuntar en este código.
Podemos concluir que para este código en caso de emplear tamaños de población y un gran número de individuos
conviene emplear guided ya que la ganancia en rendimiento es notable mientras que sí las ejecuciones son de
parámetros de entrada pequeños se puede emplear tanto static como guided con un rendimiento similar siendo
ligeramente superior para static. Siendo recomendado emplear guided en este caso como predeterminado.
5. Uso de directivas
En esta sección, hacemos un estudio sobre las distintas sentencias, que nos sirven respectivamente para forzar la
sincronización mediante espera de los hilos en caso de barrier. single para la ejecución de cierta zona mediante
un solo hilo. master es un directiva que hace que cierta zona del código sea ejecutada por el nodo principal. ordered
fuerza el orden de las ejecuciones entre los hilos.
Haciendo pruebas de la paralelización de la creación de individuos, encontramos que no se puede hacer con
varios hilos a la vez. Dicho esto, podemos aplicar en este caso una de las dos sentencias de ejecución secuencial:
single o master. Ambas consiguen resultados similares, con la ventaja de master de no crear hilos adicionales si
no habíamos paralelizado antes, como es el caso actual.
Para este bucle que mencionamos, no podemos paralelizarlo como si fuera una sección normal, ya que necesitamos
que este sea ejecutado antes de entrar a cualquiera de los otros dos bucles. Por ello, lo paralelizamos con #pragma
omp parallel for schedule(guided).
// Los primeros genes del padre1 van al hijo1. Idem para el padre2 e hijo2.
for(int i=0;i<corte;i++){
hijo1->array_int[i]=padre1->array_int[i];
hijo2->array_int[i]=padre2->array_int[i];
}
Sin embargo, los otros dos bucles incluidos son perfectamente paralelizables mediante sections,ya que son
bucles completamente equivalentes que hacen una función con hijo1 u hijo2 de manera independiente. Esto nos
facilita de forma simple la paralelización. Además, cabe mencionar que en este caso tiene sentido el uso de nowait,
que es un cláusula que nos permite hacer que los hilos no esperen cuando terminen su trabajo asignado, además de
evitar la sincronización al final.
En nuestro caso, al trabajar el cambio de los hijos de forma independiente, podemos usar nowait en sections.
Como nota, cabe mencionar que no funciona la combinación de las directivas parallel sections en caso de que
decidamos usar nowait y por ello las separamos en el código siguiente:
7
{
for(int i=corte;i<m;i++){
int elemento_padre2=padre2->array_int[i];
// Si al introducir un elemento del padre2 tras el conjunto
// del padre1 no hay coincidencias se inserta.
if(!find_element(hijo1->array_int,corte,elemento_padre2)){
hijo1->array_int[i]=elemento_padre2;
}else{
// Si hay coincidencias se recorren todos
// lo números hasta que se encuentre uno que no está dentro del
// nuevo conjunto que se esta creando.
// De esta forma, se asegura no introducir repetidos
int encontrado=0;
int numero=0;
while(!encontrado){
if(!find_element(hijo1->array_int,m,numero))
{
hijo1->array_int[i]=numero;
encontrado=1;
}
numero++;
}
}
}
}
#pragma omp section
{
for(int i=corte;i<m;i++){
int elemento_padre1=padre1->array_int[i];
if(!find_element(hijo2->array_int,corte,elemento_padre1)){
hijo2->array_int[i]=elemento_padre1;
}else{
int encontrado=0;
int numero=0;
while(!encontrado){
if(!find_element(hijo2->array_int,m,numero))
{
hijo2->array_int[i]=numero;
encontrado=1;
}
numero++;
}
}
}
}
}
}
Estos son los resultados que obtenemos, que hemos organizado en forma de tabla como en anteriores ejercicios.
Como vemos, en un tema que se repite conforme vamos paralelizando con el poco trabajo que asignamos, vemos
que no podemos llegar a mejores valores de rendimiento (en este caso representado por un tiempo menor), ya que
8
Figura 3: Tabla de resultados con la directiva sections.
Podemos encontrar mejora usando 8 hilos hasta el tercer programa, pero a partir de ahí, encontramos perdidas
para este. En cuanto al último programa, más caro computacionalmente hablando, encontramos mejora hasta 16
hilos, con pérdidas para 24.
Además, cabe mencionar que este reemplazo no es aplicable para todos las llamadas a qsort, ya que la función
mezclar, que mergeSort usa ordena mediante el valor de bondad, resultando inútil para la ordenación de los valores
de los individuos a la hora de presentar la solución. A la hora de hacer la implementación, hemos optado por hacer la
creación de un fichero mezclar.c que contiene la definición de la función de mezcla que usa el algoritmo mergeSort.
Por último, como apunte de especial interés comentamos que hemos modificado la implementación de la función
mezclar, al no interesarnos liberar los individuos en esta misma. Como parte del proceso de arreglo de errores de
memoria o heap, hemos liberado los individuos de la población. Dicho esto, en caso de mantener la llamada a la
función free en mezclar, teníamos problemas de liberación doble de memoria. Estos son los resultados conseguidos.
Al igual que con las distintas paralelizaciones que hemos hecho con las otras directivas, empezamos a ver mejora
conforme vamos añadiendo más trabajo a los hilos. Una vez hemos pasado el umbral de saturación del trabajo
dispuesto, ya no vemos mejoría, sino que incluso vemos cierto empeoramiento, como puede ser en el caso del tercer
programa, comparando 16 con 24 hilos.