lunes, 9 de marzo de 2015

Programación paralela (II)

En el anterior post se introdujo un programa para el cálculo del número pi. Este programa es un intenso consumidor de instrucciones y en su bucle principal carece de llamadas las sistema por lo que no genera interrupciones.

Se puede hacer más o menos largo la duración del mismo (y la precisión del resultado) mediante la directiva:

static long num_steps = 100000000;

Su compilación, al no tener nada especial es muy sencilla:

gcc -o pi pi.c



La directiva pragma omp paralell


Modifiquemos nuestro programa para que pueda ser ejecutado paralelamente y quedará así:

#include <stdio.h>
#include <stdio.h>
#include <omp.h>
#include <sys/types.h>
static long num_steps = 100000000;
#define NUM_TRHEADS  2

int main()
{
double sum[NUM_TRHEADS];
double step;
double pi;
int j, hop;

omp_set_num_threads(NUM_TRHEADS);
step = 1.0/(double) num_steps ;
hop = num_steps / NUM_TRHEADS;


#pragma omp parallel
{
int i;
double x;
int ID=omp_get_thread_num();
int long stage;
sum[ID] =0.0;
stage = hop * ID;
for (i = stage ; i < stage + hop; i++) {
x = (i + 0.5) * step;
sum[ID] += 4.0/(1.0 +x*x);
}

}
pi = 0;
for (j = 0; j < NUM_TRHEADS; j++) pi += sum[j] * step;
printf("El numero Pi es: %2.10f\n", pi);

}


Analicemos las modificaciones:

Hemos creado una directiva del compilador con el número de threads, que como solo tengo dos procesadores será de dos. No tiene sentido poner más threads que procesadores disponibles.
Recalco que el número de THREADS debe ser una directiva  y no una variable.  Gran parte de las estructuras necesarias para la ejecución se construye en tiempo de compilación.

Hemos modificado la variable sum, que pasa a ser una matriz con tantos elementos como threads tengamos. Es la única variable que se necesita fuera del bucle para obtener el resultado final.

En la variable hop obtener el número de iteraciones que deberá realizar cada threads (en nuestro caso cada thread realizará la mitad de iteraciones.


La sentencia #pragma omp parallel señala la parte de código que se va a ejecutar en paralelo y la analizaremos posteriormente. Una vez finalizadas cada una de las threads que se ejecutan en paralelo hemos de sumar los resultados parciales para obtener el resultado final.

Lo compilaremos con esta instrucción:

gcc -fopenmp -o pi1 pi1.c


La parte de ejecución paralela.

Empieza la esta parte con la definición de las variables locales que necesitaremos.

Identificaremos cada thread usando la función omp_get_thread_num() y el valor obtenido será usado como índice en la matriz se resultados

      sum[ID] = 0;



El resto del algoritmo se puede traducir como:

Thread 0.
for(i = 0; i < 50000000; i ++) {
....
}

Thread 1.
for(i = 50000000; i < 100000000; i ++) {
....
}

Fraccionamos el total de iteraciones entre el número de threads que vamos a arrancar y asignamos a cada thread un segmento.

Ejecución.


Ejecutamos el programa sin paralelismo y vemos la gráfica de ocupación de CPU con el monitor del sistema.



La gráfica muestra tres ejecuciones la primera de ellas con una alternancia de ejecución en cada procesador. Probablemente debida a alguna interrupción generada en el sistema.
Las dos siguientes son más puras y podemos como cada ejecución del programa nos crea una meseta en cada ejecución.

Ejecutamos ahora el programa que hemos paralelizado y vemos su evolución en el monitor del sistema.  Se puede ver como ambos procesadores son ocupados por sendas threads.





Ejecutemos ahora el programa serializado y el programa que hemos paralelizado uno a continuación del otro y miremos la gráfica:




Podemos ver las mesetas creadas por cada ejecución. La meseta del programa serializado es de trazo simple (una línea) y es el doble de larga (más o menos) que la correspondiente al programa paralelizado. Siendo la meseta correspondiente al último programa de trazo doble.



No hay comentarios:

Publicar un comentario