Una vez obtenidos los conocimientos sobre la creación de hebras y reparto de las iteraciones entre estas, vamos a resolver el calculo de PI mediante OpenMP. Pero principalmente en este tutorial prestaremos atención a la memoria compartida y sus problemas.
#include <math.h> #include <iostream> using namespace std; int main(int argc, char *argv[]) { int n; cout<<"introduce la precision del calculo (n > 0): "; cin>>n; double PI25DT = 3.141592653589793238462643; double h = 1.0 / (double) n; double sum = 0.0; #pragma omp parallel for shared( sum ) for (int i = 1; i <= n; i++) { double x = h * ((double)i - 0.5); sum += (4.0 / (1.0 + x*x)); } double pi = sum * h; cout << "El valor aproximado de PI es: " << pi << ", con un error de " << fabs(pi - PI25DT) << endl; return 0; }
La clausula private que acompaña a la directiva #pragma omp parallel indica que las hebras creadas tendrán copias locales de estas.
Vemos tres ejecuciones del programa inicial, el primer resultado con precisión 10 (es decir 10 iteraciones) el resultado es muy bueno. En las dos siguientes ejecuciones con una precisión mucho mas alta, da resultados totalmente erróneos, y dispares. Estos resultados erróneos se deben a que la variable sum es una variable compartida pudiéndose dar el caso de entrelazamiento de instrucciones que den lugar a valores erróneos.
Existen tres soluciones para este caso.
Ahora toca experimentar:
Añadiendo la clausula #pragma omp critical antes de la instrucción que escribe la variable sum conseguimos que ese conjunto de instrucciones no se ejecute a la vez por varias hebras (exclusión mutua). Esta clausula permite un nombre como parámetro que permite coexistir con otras regiones criticas, pudiendo entre dos regiones con distinto nombre ejecutarse simultaneamente.
#pragma omp parallel for shared( sum ) for (int i = 1; i <= n; i++) { double x = h * ((double)i - 0.5); #pragma omp critical sum += (4.0 / (1.0 + x*x)); }
La directiva #pragma omp critical se queda grande ya que esta permite la exclusión mutua de bloques de código. Para nuestro caso al tratarse de una instrucción simple podemos decir que sea una instrucción atómica, siento esto mas eficiente que una región critica.
#pragma omp parallel for shared( sum ) for (int i = 1; i <= n; i++) { double x = h * ((double)i - 0.5); #pragma omp atomic sum += (4.0 / (1.0 + x*x)); }
Otra solución mas eficiente que las anteriores seria el uso de la clausula reduction. Cuando la variable compartida puede ser tratada por separado y luego mezclar los resultado de cada hebra la aplicación de esta clausula es lo mas eficiente.
recution crea una copia local a cada hebra, cuando terminan todas las hebras terminan las iteraciones se aplica la operación de reducción en nuestro caso se suman todas.
#pragma omp parallel for reduction(+:sum) for (int i = 1; i <= n; i++) { double x = h * ((double)i - 0.5); sum += (4.0 / (1.0 + x*x)); }