martes, 19 de mayo de 2015

Programación paralela (y IV) - La directiva for

En todas las versiones del programa que nos calcula el número PI siempre hemos hecho algo en común: Hemos dividido las N iteraciones entre el número de threads y mediante la expresión

 for (i = stage ; i < stage + hop; i++)

 hemos fraccionado el trabajo de forma uniforme entre todas las threads recordando que el valor de la variable stage depende del identificador de thread en el que se está ejecutando y que obtenemos usando la función omp_get_thread_num()

 De forma gráfica se podría ver esto así:



La directiva for


 En OMP tenemos una directiva que nos permite hacer esto de forma más simple. Esta directiva tiene es homónima de la instrucción en C a la que pretende complementar, es decir la directiva for


 #pragma omp parallel
  {
  #pragma omp for
 .........
 ..........

  }

}

El programa para el cálculo pi si usamos esta directiva queda así:

#include <stdio.h>
#include <omp.h>
#include <time.h>
#include <sys/types.h>
static long num_steps = 100000000;
#define NUM_TRHEADS  2
double step, pi,x;
double sum =0.0; //[NUM_TRHEADS];
int main()
{

int i;
step = 1.0/(double) num_steps ;
omp_set_num_threads(NUM_TRHEADS);
#pragma omp parallel private(i,x) reduction(+:sum)                                                                
{


#pragma omp for schedule(static)
for (i = 0 ; i < num_steps; i++) {
x = (i + 0.5) * step;
sum += 4.0/(1.0 +x*x);
}
}
pi = step * sum;
printf("El numero Pi es: %2.10f\n", pi);
}

Como hay bastantes diferencias las vamos a ir viendo, no en secuencia de arriba abajo, como hemos hecho otras veces, si no de de la parte más interna del código, la que tiene más llaves a su alrededor.

La parte más interna corresponde al bucle 


 for (i = 0 ; i < num_steps; i++)                                                                                               


Cuyas condiciones de condiciones son las misma que teníamos en la primera versión del programa, la que no era multiproceso. 

Nos hemos "liberado" de tener que poner el código necesario para realizar el fraccionamiento del bucle que teníamos  para las versiones con proceso paralelo: 


#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);
}

    }                       

Núcleo del programa en programación paralela


La directiva #pragma omp for schedule(static)  es quien nos gestiona este trabajo. Pero esta directiva es mucho más potente. En este ejemplo hemos tratado con la forma más sencilla. La directiva schedule tiene la siguente sintaxis:

schedule (tipo [,fraccionamiento])

  • tipo admite los valores: static, dynamic, guided.
  • fraccionamiento: es el rango de iteraciones que se va a asignar a cada proceso paralelo.
Si se especifica static y no se pone ningún valor para fraccionamiento, éste toma el valor del número de iteraciones divido entre el número de tareas, que en nuestro ejemplo es el algoritmo que usamos para calcular 

i = stage ; i < stage + hop

A partir de esta base podemos jugar con todas las demás posibilidades que tiene la directiva schedule y que será muy útil se cada iteración tiene dentro cálculos más complejos y menos uniformes que nuestro ejemplo. 

Saltando un escalón más hacia arriba nos encontramos con la ya conocida directiva  #pragma omp parallel  a la que hemos añadido algunos parámetros más:  

 private(i,x) reduction(+:sum)  

El primero  de ellos [private(v1, v2, ... vn)] da instrucciones para que en tiempo de ejecución se cree un juego de  esas variables en cada proceso paralelo y sólo accesible dentro de cada uno de ellos. Nos vale para sustituir las declaraciones de variables:


int i;
double x;

La sentencia [reduction(+:sum)] nos evita tener que difinir expresamente una matriz para para almacenar el resultado de cada proceso. Recordemos que debíamos de definir una matriz para los resultados y cada proceso almacenaba su resultado en un elemento de esa matriz. 

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);

Y posteriormente debíamos "ensamblar" los resultados parciales: 


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

Resumiendo.

Gracias a la directiva for logramos poder realizar una programación paralela pero dejando los núcleos del algoritmo independiente de si el proceso de va a ejecutar en uno o varios threads. 
Me dejo bastantes más cosas pendientes pero que haría esta serie demasiado larga. 

En el ámbito de visibilidad de datos hemos visto private y reduction, pero tenemos otras como shared, firstprivate, lastprivate.

En la sincronización hemos visto la directiva critical, pero está: barrier, taskwait, atomic.

Y para finalizar la directiva for es una de las estructuras de comparticion de tareas, pero están también las directivas section, single.

Información sobre todas estas estructura se pueden encontrar en esta dirección:  https://computing.llnl.gov/tutorials/openMP/




No hay comentarios:

Publicar un comentario