Está en la página 1de 10

MPP1 - Paralelización con OpenMP

Enrique Kessler Martínez, Grupo 1.2, 23300350Q


Carlos Pacheco Valverde, Grupo 1.2, 49183979C
Rida Youb Yahyaoui, Grupo 1.3, 34331062C

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

2. Proceso de mejora de individuos 3

3. Paralelización de la función fitness 4

4. Diferentes cláusulas de Scheduling 6

5. Uso de directivas 7

6. Cruzar con sections 7

7. Reemplazo de qsort con mergeSort 9

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.

1.8. Funciones de comparación


Consideramos que no es necesario ni interesante la paralización de las funciones de comparación, ya que solo
van a ser utilizadas para la ordenación de los indivíduos.

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.9.1. Bucle de creación de individuos


Podemos realizar la paralelización de este, ya que llamamos a funciones de forma individual, que pueden ser
accedidas por cada uno de los hilos.

1.9.2. Bucle cruzar


Para nuestra implementación concreta, podríamos paralelizar este, ya que no estamos modificando las mismas
posiciones de memoria, sino cruzando los padres y crear nuevas posiciones de memoria para los hijos.

1.9.3. Bucles mutar y fitness


Se pueden paralelizar de forma individual, ya que solo estamos haciendo cálculos sobre el individuo de cada
iteración, sin acceder a las otras iteraciones.

1.9.4. Bucle free


Consideramos que la liberación de memoria no cuenta con problemas de paralelización, especialmente al estar
dentro del for, y poder liberar cada uno de los individuos de forma individual.

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.11. distancia_ij y fitness


La función distanciaij no tiene especial interés de paralelizar, al ser código secuencial. Por otro lado, la función
fitness es paralelizable, acumulando el resultado en la variable fitness_value.

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.

2. Proceso de mejora de individuos


En esta sección hacemos cambios a la llamada de números aleatorios, a funcionar con rand_r, que es una función
thread-safe, es decir, sin provocar problemas funcionando con hilos.
double *generar_matriz_distancias(int n)
{
double f;
double *d = (double *) malloc(n * (n + 1) / 2 * sizeof(double));
int position;

for(int i = 0; i < n; i++) {


for(int j = 0; j < n; j++) {

3
unsigned int seed = (unsigned int) (time(NULL)+ getpid());
f = (double) rand_r(&seed) / ((double) RAND_MAX + 1);

// De esta forma tenemos una matriz triangular almacenada de forma eficiente,


// solo cogiendo 1 + 2 + 3 + ... + N = N * (N + 1) / 2 espacio, frente a N^2.
if (i < j) {
position = i + (j * (j - 1) / 2);
*(d + position) = (min + f*(max - min));
}
}
}
return d;
}

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.

3. Paralelización de la función fitness


Para hacer la paralelización de esta, contamos con distintas estrategias que procedemos a comparar. Estas son:
Uso de critical, que fuerza que la siguiente sentencia declarada se ejecute solo por un proceso.
Uso de atomic, hace que las operaciones con varios pasos (por ejemplo fitness_value += valor) se realicen por
el mismo hilo.
Uso de parallel for con reduction, para paralelizar la suma de los valores que se acumulan en fitness_value.

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.

Figura 1: Tabla de comparación de directivas

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.

Figura 2: Tabla de ejecuciones de directivas de scheduling.

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.

6. Cruzar con sections


En este caso, se nos indica que tenemos que paralelizar la función cruzar mediante la directiva sections. Para ello
tenemos que comprender cómo funciona nuestra implementación de la función descrita. Esta, emplea dos punteros
a nodos hijos en los que se volcarán de manera mezclada dos nodos padre, por lo que contamos con la ventaja de
que la mayoría de lecturas y escritura se hacen sobre punteros distintos.

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:

#pragma omp parallel


{
#pragma omp sections nowait
{
#pragma omp section

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.

a pesar de contar con más hilos, no vemos mejora clara.

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.

7. Reemplazo de qsort con mergeSort


En este caso, se nos pide el reemplazo de las llamadas de qsort con la nueva función mergeSort, que aplica
el famoso algoritmo de forma recursiva. Siendo este último aspecto importante, decimos usar el paradigma de
paralización mediante tareas, al ser especialmente interesante para las funciones recursivas.

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.

Figura 4: Tabla con los resultados de reemplazar qsort con mergeSort.

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.

También podría gustarte