Академический Документы
Профессиональный Документы
Культура Документы
CONVOLUCIÓN DE IMÁGENES
PARALELIZACIÓN MPI – PTHREAD&MUTEX - HIBRIDA
AUTORES:
NÉCTOR ROSALES ORTUÑO - RAZVAN ANDREI LISMANU - JEFFERSON MAX TOMALA VILLARREAL
MULTIPROCESADORES
1
Contenido
VERSIÓN – PTHREADS&MUTEX ....................................................................... 3
conbordes.c ................................................................................................................................................................... 3
Más información ........................................................................................................................................................... 6
Pruebas de eficiencia .................................................................................................................................................... 8
Valores obtenidos ..................................................................................................................................................... 8
Comprobaciones en el terminal .................................................................................................................................... 9
VERSIÓN - HÍBRIDA....................................................................................... 17
hibrido.c ...................................................................................................................................................................... 17
Más información ......................................................................................................................................................... 21
Pruebas de eficiencia .................................................................................................................................................. 22
Valores obtenidos ................................................................................................................................................... 22
Comprobaciones en el terminal .................................................................................................................................. 23
Comprobaciones con MPI con 2,3 y 4 procesadores .............................................................................................. 23
Comprobaciones HÍBRIDO con 2,3 y 4 procesadores y con 1 hilo .......................................................................... 24
Comprobaciones HÍBRIDO con 2,3 y 4 procesadores y con 4 hilos ........................................................................ 24
Comprobaciones HÍBRIDO con 2,3 y 4 procesadores y con 8 hilos ........................................................................ 25
2
VERSIÓN – PTHREADS&MUTEX
Nuestro programa en líneas generales lee una imagen, dicha imagen es una matriz donde cada pixel corresponde a
una posición de la misma, sobre esta imagen se aplicará un filtro de desenfoque, para realizar la versión paralela, lo
que hemos realizado ha sido dividir la matriz por columnas, dividiendo el “ancho” de la imagen entre el “número de
hilos” que quisiéramos generar, los parámetros que recogeremos para procesar la imagen “Final” son el nombre de
la imagen, el número de hilos y el tamaño del núcleo o filtro. El resultado que obtenemos será una imagen con la
misma resolución que la original, pero dependiendo del tamaño del núcleo aplicado esta imagen será más borrosa o
menos como veremos más detalladamente a continuación:
conbordes.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pgm.h"
#include <pthread.h>
// gettimeofday
#include <sys/time.h>
3
int y, suma, i, j, x, k = 0;
datos *p = (datos*)estructura;
int ini = p->ini_col;
int fin = p->fin_col;
int ini_alto = p->ini_rows;
int fin_alto = p->fin_rows;
int alto = p->alto_;
int dim = p->dim_nucleo;
/*Hallamos el valor de k a través del nucleo generado*/
for (i = 0; i < dim; i++)
for (j = 0; j < dim; j++)
k = k + nucleo[i][j]; //para un nucleo de 3x3 k = -7, para 5x5 k = -22...
/*Ahora calculamos el inicio y fin de los ejes "x" e "y" dependiendo de la
dimensión del nucleo
* para determinar el recorrido donde aplicaremos el filtro*/
if(ini == 0){ ini += (dim-2);}
if(fin == alto){ fin -= (dim-2);}
if(ini_alto == 0){ ini_alto += (dim-2);}
if(fin_alto == alto){ fin_alto -= (dim-2);}
4
pthread_attr_t attr;
if(nucleo_size%2==0 || nucleo_size<3){
printf ("\nError, debe introducir un tamaño de nucleo con valor impar y mayor o
igual a 3:");
printf ("\n./conbordes numero_procesos nucleo_size\n");
exit (1);
}
datos *datosArray;
/*Reservamos un espacio de memoria para el array de la estructura,
* en la cual pasaremos los datos a cada hilo*/
datosArray = (datos *)malloc(numeroHilos*sizeof(datos));
int rc;
int inicio = 0;
int fin = 0;
int cociente = ancho_img/numeroHilos;
int resto = ancho_img%numeroHilos;
int t;
5
datosArray[t].ini_col = inicio;
datosArray[t].ini_rows = 0;
datosArray[t].fin_col = fin;
datosArray[t].fin_rows = alto_img;
datosArray[t].alto_ = alto_img;
datosArray[t].dim_nucleo = nucleo_size;
//printf("hilo[%d] --> ini_x: %d, fin_x %d, ini_y: %d, fin_y %d\n", t, inicio,
fin, 0, alto_img);
/*Para continuar será necesario esperar a que acaben todos los hilos*/
for(t = 0; t < numeroHilos; t++) {
rc = pthread_join(threads[t], NULL);
}
/*Generamos el archivo "lena_procesada1.pgm" a traves de Salida*/
pgmwrite(Final, "lena_procesada_pthread&mutex.pgm", ancho_img, alto_img);
/*liberamos la memoria*/
Free2D((void**) nucleo, nucleo_size);
Free2D((void**) Original, ancho_img);
Free2D((void**) Salida, ancho_img);
Free2D((void**) Final, ancho_img);
/*limpiamos y finalizamos los hilos*/
pthread_attr_destroy(&attr);
pthread_mutex_destroy(&exc_mut);
pthread_exit(NULL);
}
Más información
Como hemos mencionado anteriormente si probamos ejecutar el código anterior comprobamos que necesitamos
introducir el nombre de la imagen + un numero de procesos + un número para la dimensión del núcleo cuyo valor
deberá ser mayor o igual a 3 (en este caso el valor será para un núcleo de 3x3) como vemos en la siguiente imagen:
6
Comprobamos realizarlo para un núcleo de 5x5 y con un solo hilo:
Si vemos las imágenes que tenemos a continuación comprobamos como para aplicar un núcleo de 3x3 recorremos
toda la parte blanca dejando un borde proporcional al filtro aplicado en este caso 1px, en el caso de un núcleo de
5x5 podemos observar como el borde de la imagen aumentaría a 2px y así sucesivamente, esto lo contemplamos
para que el recorrido del núcleo no acabe solapándose con el de la imagen original.
IMAGEN DE 12X12 CON UN FILTRO DE 3X3 IMAGEN DE 12X12 CON UN FILTRO DE 5X5
7
A medida que vamos aumentando el filtro comprobamos como la imagen acaba siendo más borrosa y el borde de la
imagen acaba siendo más grueso como hemos explicado anteriormente.
Pruebas de eficiencia
La dimensión del núcleo ha sido fijada en 23x23 para hallar estos valores y no ha sido cambiada para valorar el
rendimiento en el uso de los hilos, pero a medida que este valor lo aumentemos los tiempos también serán mayores
ya que son más iteraciones que deberíamos recorrer.
Valores obtenidos
RESOLUCIÓN (px) 1 HILO (seg) 2 HILOS (seg) 4 HILOS (seg) 8 HILOS (seg)
256x256 0,112 0,096 0,075 0,072
512x512 0,468 0,318 0,305 0,283
1024x1024 1,971 1,343 1,181 1,165
8
Tiempo de ejecución
2,5
Tiempo(segundos)
2
1,5
0,5
0
0 1 2 3 4 5 6 7 8 9
Número de procesos
Como vemos en la gráfica anterior para una imagen de 256x256 no se aprecia la diferencia de tiempo, pero si
usamos una resolución de 1024x1024 (línea gris) comprobamos como ha disminuido considerablemente de 2
segundos con 1 hilo a casi la mitad en el uso de 4 hilos y su rendimiento ya es constante.
Comprobaciones en el terminal
Si comprobamos el rendimiento que supone generar la imagen de 512x512px con distintos valores en los hilos
aplicando un núcleo de 23x23 obtenemos los resultados vistos anteriormente:
9
Y por último calculamos los tiempos obtenidos para la imagen con una resolución de 1024x1024:
Comprobamos como a medida que aumentamos el número de hilos, se puede apreciar como el tiempo disminuye,
con lo cual empleando más hilos hacemos que todo el proceso sea más eficiente.
VERSIÓN - MPI
Para realizar la versión MPI, hemos seguido el mismo patrón anterior, dividiendo el “ancho” de la imagen entre el
“número de procesadores en este caso” -1, ya que el procesador 0 “master” se encargará solamente de recibir los
datos de los de más procesadores y guardar la imagen, los parámetros que recogeremos para procesar la imagen
“Final” son el nombre de la imagen, el número de procesadores y el tamaño del núcleo o filtro.
En esta versión hemos creado dos métodos auxiliares los cuales nos permitan pasar una matriz a un vector y
viceversa, de esta manera nos resulta más fácil a la hora de trabajar con un vector para enviar y recibir los datos a
través de MPI, pero también necesitamos una matriz para aplicar el núcleo, es por eso el uso de estos dos métodos.
El resultado que obtenemos será una imagen con la misma resolución que la original, pero dependiendo del tamaño
del núcleo aplicado, esta imagen será más borrosa o menos como hemos visto anteriormente:
10
mpi.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pgm.h"
#include "mpi.h"
#define TAG 17
// gettimeofday
#include <sys/time.h>
/*Método para pasar la matriz a un vector, este método lo usaremos para poder enviar
* y recibir los datos que tenemos en MPI a través de un vector ya que el uso de una
matriz nos da fallos de memoria y de esta manera nos resulta más facil*/
void pasoMatriz (unsigned char** m, unsigned char *v, int Largo, int Alto)
{
int i,j;
int p = 0;
for (i=0; i<Largo; i++)
{
for (j=0; j<Alto; j++)
{
v[p] = m[i][j];
p++;
}
}
}
/*Método para pasar el vector a una matriz, necesitamos los datos en una matriz para
realizar la convolución y de esta manera aplicar el nucleo*/
void pasovector (unsigned char** m, unsigned char *v, int Largo, int Alto)
{
int i,j;
int p = 0;
for (i=0; i<Largo; i++)
{
for (j=0; j<Alto; j++)
{
m[i][j] = v[p];
p++;
}
}
}
11
/*Función hebrada donde generamos la convolución de la imagen donde le pasamos por
parametro
* la imagen Original, el nucleo, la imagen Salida, la posición donde comienza y
termina tanto las columnas
* como las filas, ademas de la dimensión del nucleo */
void convolucion(unsigned char** Original, int** nucleo, unsigned char** Salida, int
ini, int fin, int ini_rows, int fin_rows, int alto,int dim) {
int x, y;
int suma;
int k = 0;
int i, j;
12
int nucleo_size;
if(nucleo_size%2==0 || nucleo_size<3){
printf ("\nError, debe introducir un nommbre de imagen y un tamaño de nucleo
con valor impar y mayor o igual a 3:");
printf ("\nmpiexec -n número_procesadores mpi nombre_img nucleo_size\n");
exit(1);
}
int ini_rows = 0;
int fin_rows = Alto;
int aux=size-1; //número de procesadores
int resto=Largo%aux;
int cociente=Largo/aux;
13
int aux1=rank-1;
inicio=aux1*(Largo/aux)+(aux1<resto?aux1:resto);
fin=inicio+(Largo/aux)+(aux1<resto);
/*Creamos la convolución*/
convolucion (Original, nucleo, Salida, inicio, fin, ini_rows, fin_rows,
Alto,nucleo_size);
/*pasamos la matriz Salida calculada a un vector para poder enviarlo al proceso
maestro "0"*/
pasoMatriz (Salida, vector, Largo, Alto);
//printf("Proceso %d envia el vector: %d al proceso %d\n", rank, vector, 0);
MPI_Send (vector, Largo*Alto, MPI_CHAR, 0, TAG, MPI_COMM_WORLD);
//printf("Proceso %d envia el inicio: %d al proceso %d\n", rank, inicio, 0);
MPI_Send (&inicio, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD);
//printf("Proceso %d envia el fin: %d al proceso %d\n", rank, fin, 0);
MPI_Send (&fin, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD);
}
/*liberamos la memoria*/
Free2D((void**) nucleo, nucleo_size);
Free2D((void**) Original, Largo);
Free2D((void**) Salida, Largo);
Free2D((void**) Auxiliar, Largo);
free(vector);
/*Finalizamos MPI*/
MPI_Finalize();
return 0;
}
Más información
En este caso hacemos el mismo proceso para aplicar un núcleo de distintas dimensiones como hemos realizado para
los hilos, si, por ejemplo, tenemos una imagen de 12x12 y usamos 3 procesadores, el procesador0 se encarga de recibir
los datos y guardar la imagen, por lo tanto, esta imagen sería dividida en 2 partes, una parte para el procesador1 (color
naranja) y otra para el procesador2 (color blanco). En este caso se enviaría el inicio y fin de donde empezaría y
terminaría cada procesador y el resultado de aplicar la convolución sería enviado al procesador 0.
14
Pruebas de eficiencia
Ahora vamos a realizar una prueba de rendimiento con las resoluciones de 512x512 y 1024x1024 vistas anteriormente
con distinto número de procesadores y un núcleo fijo de 23x23, en la siguiente tabla mostramos el tiempo de ejecución
medio que ha tardado en procesarse dichas imágenes con 2,3 y 4 procesadores cada una:
Cuando hacemos uso de 2 procesadores, como el proceso 0 se encarga solo de recibir los datos y guardar la imagen
solo haría la función de aplicar la convolución un procesador, el 1, por lo tanto, aunque describamos 2 procesadores
solo trabajaría 1 la convolución y así para los otros.
Valores obtenidos
Tiempo de ejecución
2,5 2,272
Tiempo(segundos)
2 1,766
1,546
1,5
1 0,606
0,451 0,425
0,5
0
0 1 2 3 4 5
Número de procesadores
Img512x512 Img1024x1024
Comprobamos en la gráfica anterior la similitud con el uso de hilos, y es que cuanta más resolución tenga la imagen,
más claro se verá la diferencia en el uso de los procesadores. Si comparamos los tiempos obtenidos en el uso de
hilos y ahora con MPI comprobamos que los tiempos son muy parecidos, incluso un poco más lento en MPI debido a
que hacemos uso de más bucles anidados, pero aún así, los tiempos son muy similares.
A falta de tiempo podíamos haber hecho uso de MPI_Reduce, MPI_Scatter o MPI_Gather para mejorar su
eficiencia y comprobar como los tiempos de ejecución con MPI son menores respecto al uso de hilos.
Comprobaciones en el terminal
Si comprobamos el rendimiento que supone generar la imagen de 1024x1024px con distintos valores en los
procesadores empleados aplicando un núcleo de 23x23 obtenemos los resultados vistos anteriormente, también
podemos ver los tiempos que se han empleado en cada procesador, en la convolución y el total después de guardar
la imagen el procesador 0.
15
Probamos realizarlo con la imagen de 512x512 y los tiempos obtenidos son los siguientes:
16
VERSIÓN - HÍBRIDA
Para realizar la versión Híbrida, hemos aplicado las versiones anteriores, en este caso también dividimos el “ancho”
de la imagen entre el “número de procesadores -1, ya que el procesador 0 “master” se encargará solamente de
recibir los datos de los de más procesadores y guardar la imagen, una vez dividida la imagen por ejemplo haciendo
uso de 3 procesadores, ya sabemos que la imagen sería dividida en 2 partes y ahora cada una de estas partes será
dividida por un número de hilos que empleamos, los parámetros que recogeremos para procesar la imagen “Final”
son el nombre de la imagen, el número de procesadores, el tamaño del núcleo o filtro y por último el número de
hilos.
Como hemos mencionado anteriormente: También hemos creado dos métodos auxiliares los cuales nos permitan
pasar una matriz a un vector y viceversa, de esta manera nos resulta más fácil a la hora de trabajar con un vector
para enviar y recibir los datos a través de MPI, pero también necesitamos una matriz para aplicar el núcleo, es por
eso el uso de estos dos métodos.
El resultado que obtenemos será una imagen con la misma resolución que la original, pero dependiendo del tamaño
del núcleo aplicado, esta imagen será más borrosa o menos como hemos visto anteriormente:
hibrido.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pgm.h"
#include "mpi.h"
#define TAG 17
#include <pthread.h>
// gettimeofday
#include <sys/time.h>
17
* y recibir los datos que tenemos en MPI a través de un vector ya que el uso de una
matriz
* nos da fallos de memoria y de esta manera nos resulta más facil*/
void pasoMatriz (unsigned char** m, unsigned char *v, int Largo, int Alto)
{
int i,j;
int p = 0;
for (i=0; i<Largo; i++)
{
for (j=0; j<Alto; j++)
{
v[p] = m[i][j];
p++;
}
}
}
/*Método para pasar el vector a una matriz, necesitamos los datos en una matriz para
realizar
* la convolución y de esta manera aplicar el nucleo*/
void pasovector (unsigned char** m, unsigned char *v, int Largo, int Alto)
{
int i,j;
int p = 0;
for (i=0; i<Largo; i++)
{
for (j=0; j<Alto; j++)
{
m[i][j] = v[p];
p++;
}
}
}
/*Función principal donde generamos la convolución de la imagen aplicando el filtro
*a la estructura que le pasemos por parametro, en este caso la img original cuya
* estructura viene dada por ini_col (inicio de la columna), fin_col (final de la
columna)
* , ini_rows (inicio de la fila), fin_rows (final de la fila), el alto_ (altura de la
img),
* y por último dim_nucleo (dimensión del nucleo), estos 2 últimos no cambian*/
void *convolucion(void *estructura) {
datos *p = (datos*)estructura;
int ini = p->ini_col;
int fin = p->fin_col;
int dim = p->dim_nucleo;
int ini_rows=p->ini_rows;
int fin_rows=p->fin_rows;
int alto=p->alto;
int y;
int suma;
int k = 0;
int i, j;
int x;
18
/*Recorremos la estructura de la img según los valores calculados anteriormente */
for (x = ini; x<fin; x++){
for (y = ini_rows; y<fin_rows ; y++){
suma = 0;
/*Guardamos en suma el valor del resultado que obtenemos al aplicar el
nucleo*/
for (i = 0; i < dim; i++){
for (j = 0; j < dim; j++){
suma = suma + Original[(x-1)+i][(y-1)+j] * nucleo[i][j];
}
}
/*Guardamos en Salida el valor del resultado anterior/k,
* de esta manera hacemos que el pixel pierda nitidez y veamos
* una imagen más borrosa al final*/
if(k==0){
Salida[x][y] = suma;
}else{
Salida[x][y] = suma/k;
}
}
}
/*finalizamos el hilo*/
pthread_exit(NULL);
}
if(nucleo_size%2==0 || nucleo_size<3){
printf ("\nError, debe introducir un nommbre de imagen y un tamaño de nucleo
con valor impar y mayor o igual a 3:");
printf ("\nmpiexec -n número_procesadores mpi nombre_img nucleo_size\n");
exit(1);
}
19
nucleo = (int**) GetMem2D(nucleo_size, nucleo_size, sizeof(int));
int ini_rows = 0;
int fin_rows = Alto;
int aux=size-1; //número de procesadores
int resto=Largo%aux;
int cociente=Largo/aux;
pthread_t *threads;
/*Reservamos un espacio de memoria para los hilos generados*/
threads = (pthread_t *) malloc(numeroHilos*sizeof(pthread_t));
pthread_attr_t attr;
datos *datosArray;
/*Reservamos un espacio de memoria para el array de la estructura,
* en la cual pasaremos los datos a cada hilo*/
datosArray = (datos *)malloc(numeroHilos*sizeof(datos));
}else{
/*Determinamos el inicio y fin de cada columna depeniendo el número de procesadores*/
int aux1=rank-1;
inicio=aux1*(Largo/aux)+(aux1<resto?aux1:resto);
fin=inicio+(Largo/aux)+(aux1<resto);
/*También será necesario calcular los datos que abarcan cada hilo/s en cada procesador*/
int rc;
int cocienteHilo = (fin-inicio)/numeroHilos;
int restoHilo = (fin-inicio)%numeroHilos;
int t,final;
int inicioHilo=0,finHilo=0;
20
/*para cada uno de los hilos...*/
for(t = 0; t < numeroHilos; t++){
datosArray[t].ini_col = inicioHilo;
datosArray[t].fin_col = finHilo;
datosArray[t].dim_nucleo=nucleo_size;
datosArray[t].ini_rows=0;
datosArray[t].fin_rows=Alto;
datosArray[t].alto=Alto;
/*Para continuar será necesario esperar a que acaben todos los hilos*/
for(t = 0; t < numeroHilos; t++) {
rc = pthread_join(threads[t], NULL);
}
/*liberamos la memoria*/
Free2D((void**) nucleo, nucleo_size);
Free2D((void**) Original, Largo);
Free2D((void**) Salida, Largo);
Free2D((void**) Auxiliar, Largo);
free(vector);
/*Finalizamos MPI*/
MPI_Finalize();
return 0;
}
Más información
En este caso como hemos explicado anteriormente si tenemos una imagen con una resolución de 12x12 y queremos
ejecutarla con 3 procesadores (0 – “master”) , este ejecutaría la convolución en 2 de ellos, por lo tanto tenemos la
imagen dividida en 2 partes como vemos en la siguiente figura (procesador1 color naranja y procesador2 color
21
blanco) ahora si queremos ejecutar solamente 2 hilos, el procesador1 su parte la dividiría en otras 2 partes (siempre
dividimos en columnas ya que el acceso a memoria es más eficiente que realizar las divisiones por filas) como vemos
en el procesador1 el hilo0 subrayado de rojo y el hilo1 subrayado de azul, este mismo proceso haría el procesador2.
Pruebas de eficiencia
Ahora vamos a realizar una prueba de rendimiento con una resolución de 2048x2048 (lena_original4.pgm) con
distinto número de procesadores e hilos y un núcleo fijo de 23x23, las comprobaciones se han realizado en la parte
hibrida con 1,4 y 8 hilos. En la siguiente tabla mostramos el tiempo de ejecución que ha tardado en procesarse dichas
imágenes con 2,3 y 4 procesadores cada una:
Valores obtenidos
22
Tiempo de ejecución
12
Tiempo(segundos)
10
0
0 1 2 3 4 5
Número de procesadores
MPI HIBRIDO(1hilo) HIBRIDO(4hilos) HIBRIDO(8hilos)
Como vemos en la gráfica anterior tanto el proceso MPI e HIBRIDO con 1 hilo están en los mismos intervalos de
tiempo, pero si usamos la parte HIBRIDA con 4 y 8 hilos vemos como se hace más eficiente y en el caso de usar un
número mayor de procesadores llegaríamos a un intervalo de tiempo lineal como vemos en la gráfica.
Comprobaciones en el terminal
Si comprobamos el rendimiento que supone generar la imagen de 2048x2048px con distintos valores en los
procesadores empleados aplicando un núcleo de 23x23 obtenemos los resultados vistos anteriormente, también
podemos ver los tiempos que se han empleado en cada procesador, en la convolución y el total después de guardar
la imagen el procesador 0.
23
Si ejecutamos htop podemos comprobar el uso de procesadores empleados en cada proceso
24
Comprobaciones HÍBRIDO con 2,3 y 4 procesadores y con 8 hilos
Y por último vemos como los tiempos son algo más bajos que con 4 hilos pero la diferencia es mínima ya que nos
acercamos a un punto lineal como hemos mencionado anteriormente por el uso de los procesadores.
25