Notas para contenedor de docker:

Comando de docker para ejecución de la nota de forma local:

nota: cambiar <ruta a mi directorio> por la ruta de directorio que se desea mapear a /datos dentro del contenedor de docker.

docker run --rm -v <ruta a mi directorio>:/datos --name jupyterlab_c_kernel_local -p 8888:8888 -d palmoreck/jupyterlab_c_kernel:1.1.0

password para jupyterlab: qwerty

Detener el contenedor de docker:

docker stop jupyterlab_c_kernel_local

Documentación de la imagen de docker palmoreck/jupyterlab_c_kernel:1.1.0 en liga.


Nota generada a partir de liga

A diferencia con los paquetes de parallel de R o de multiprocessing o dask de Python en los que se sugiere el lanzamiento de subprocesos a partir de un proceso principal para el cómputo en paralelo en tareas principalmente CPU bound (cada subproceso son instancias de Python y R), en el lenguaje C lanzamos threads desde un proceso principal conocido con el nombre de threading en los que los threads realizan fork's y join's vistos en 2.1.Un_poco_de_historia_y_generalidades.

Una herramienta que permite el fork y join de threads a partir de un proceso es OpenMP. Otra más es POSIX threads Pthreads. Revisaremos en esta nota algunos conceptos y usos básicos de OpenMP.

OpenMP

Documentación: openMP

Es una API para shared memory parallel programming. Las siglas MP se refieren a multiprocessing un sinónimo de shared memory parallel computing. Al programar con OpenMP consideramos que en nuestro sistema cada thread y core pueden potencialmente tener acceso a toda la memoria disponible.

Algunas características de OpenMP son:

  • Paralelización de ciclos for secuenciales en los que las iteraciones son independientes una de la otra de forma simple.

  • Paralelización de tareas y sincronización explícita de threads.

OpenMP es una API que provee directivas para shared_memory programming. Una directiva es una instrucción especial para indicar al preprocesador (vía la compilación) que ejecutaremos una instrucción que no se encuentra en la especificación básica del lenguaje C (ver C preprocessor). En OpenMP utilizamos el pragma: #pragma omp.

Comentario: los pragma's son añadidos al sistema para comportamientos que no son parte de la especificación básica de C. Cabe señalar que las versiones más recientes del compilador gcc sí soportan a los pragmas y todas las preprocessor directive son por default de longitud una línea. Si no cabe en una línea usamos un escaping (precedemos con un backslash "\" la "nueva línea").

Directiva parallel

1) Hello world


In [13]:
//%cflags:-fopenmp
//%cflags:-Wall
#include<stdio.h>
#include<stdlib.h>
#include<omp.h> //header file con prototipos y macro definitions para 
                //la librería de funciones y macros de openMP.

void Hello(void); //prototipo de función a ejecutar por los threads.
int main(){

    //Siempre iniciamos con un #pragma omp ... :

    #pragma omp parallel //directiva parallel
       //structured block que sólo consiste del llamado a la función Hello: 
        Hello();
    return 0;
}

//función que será ejecutada por los threads
void Hello(void){
    int mi_rango = omp_get_thread_num(); //obtenemos el rank dado por
                    //el run-time system a cada thread
    int conteo_threads = omp_get_num_threads(); //obtenemos el número de threads 
                    //que realizaron un fork
                    //del master thread
    printf("Hola del thread: %d de %d\n", mi_rango, conteo_threads);
}


Hola del thread: 1 de 2
Hola del thread: 0 de 2

Obs:

  • Obsérvese que hay que añadir la flag -fopenmp para soporte de OpenMP. De modo que la compilación se realiza:

gcc -Wall -fopenmp hello_world_omp.c -o hello_world_omp.out

suponiendo que el programa anterior se guarda en un archivo con nombre hello_world_omp.c.

  • Dependiendo del número de cores de nuestro sistema tendremos diferentes número de printf's.

  • Lo que continúa a la línea de #pragma omp parallel es un structured block, esto es, un statement o conjunto de statements que tienen un punto de entrada y un punto de salida, no se permiten statements como el siguiente:

#pragma omp parallel
if(...) break;

ni tampoco:

#pragma omp parallel
    {
        if(variable == valor) return 1;
        return -1;
    }

2) Hello world con clause num_threads

A continuación del nombre parallel podemos usar diferentes tipos de clauses, una clause en OpenMP es un texto que modifica una directiva. Por ejemplo, podemos usar la clause num_threads para especificar el número de threads que ejecutarán el structured block:


In [16]:
//%cflags:-fopenmp
//%cflags:-Wall
#include<stdio.h>
#include<stdlib.h>
#include<omp.h> //header file con prototipos y macro definitions para 
                //la librería de funciones y macros de openMP.

void Hello(void); //prototipo de función a ejecutar por los threads.
int main(){
    int numero_threads=5;
    //Siempre iniciamos con un #pragma omp ... :

    #pragma omp parallel num_threads(numero_threads) //directive parallel con clause num_threads
       //structured block que sólo consiste del llamado a la función Hello: 
        Hello();
    return 0;
}

//función que será ejecutada por los threads
void Hello(void){
    int mi_rango = omp_get_thread_num(); //obtenemos el rank dado por
                    //el run-time system a cada thread
    int conteo_threads = omp_get_num_threads(); //obtenemos el número de threads 
                    //que realizaron un fork
                    //del master thread
    printf("Hola del thread: %d de %d\n", mi_rango, conteo_threads);
}


Hola del thread: 0 de 5
Hola del thread: 4 de 5
Hola del thread: 3 de 5
Hola del thread: 2 de 5
Hola del thread: 1 de 5

Obs: obsérvese el no determinismo en el printf.

3) Regla compuesta del Rectángulo

Para la medición de tiempos se utilizaron las ligas: liga y liga2

Forma secuencial


In [1]:
//%cflags:-lm
#include<stdio.h>
#include<stdlib.h>
#include<math.h> //header para funciones de mate
#include<time.h>
#include <sys/time.h>
void Rcf(double a, double b, int n,\
    double *suma_global_p);
double f(double nodo);
int main(int argc, char *argv[]){
    double suma_global = 0.0;
    double a=0.0, b=1.0;
    int n=1e6; //número de subintervalos
    double objetivo=0.7468241328124271;
    struct timeval start;
    struct timeval end;
    long seconds;
    long long mili;
    

    gettimeofday(&start, NULL);
    Rcf(a,b,n,&suma_global);
    gettimeofday(&end, NULL);
    seconds = (end.tv_sec - start.tv_sec);
    mili = 1000*(seconds) + (end.tv_usec - start.tv_usec)/1000;
    printf("Integral de %f a %f = %1.15e\n", a,b,suma_global);
    printf("Error relativo de la solución: %1.15e\n", fabs(suma_global-objetivo)/fabs(objetivo));
    printf("Tiempo de ejecución: %lld miliseconds", mili);
    return 0;
}
void Rcf(double a, double b, int n, double *sum){
    double h_hat=(b-a)/n;
    double x=0.0;
    int i=0;
    *sum = 0.0;
    for(i=0;i<=n-1;i++){
        x = a+(i+1/2.0)*h_hat;
        *sum+=f(x);
    }
    *sum =h_hat*(*sum);
}
double f(double nodo){
    double valor_f;
    valor_f = exp(-pow(nodo,2));
    return valor_f;
}


Integral de 0.000000 a 1.000000 = 7.468241328124773e-01
Error relativo de la solución: 6.719397313003120e-14
Tiempo de ejecución: 32 miliseconds

Forma en paralelo:

Nota: los siguientes resultados se obtuvieron con una máquina con 8 cores, así que pueden no coincidir con los resultados previos de esta sección.


In [2]:
//%cflags:-fopenmp
//%cflags:-lm
//%cflags:-Wall
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#include<math.h> //header para funciones de mate
#include<time.h>
#include <sys/time.h>
//prototipo de funciones
double Rcf_parallel(double a, double h_hat, int ns_p);
double f(double nodo);

//programa principal
int main(){
    double suma_global = 0.0; //variable que es shared, en esta se sumarán los resultados
                //de las aproximaciones de cada uno de los threads.
    double a=0.0, b=1.0;
    int n=1e6; //número de subintervalos
    double h_hat=(b-a)/n;
    int ns_p;
    int numero_threads[5]={0};//conteo_threads debe dividir de forma exacta a n
    int long_numero_threads = 0;
    double objetivo=0.7468241328124271;
    struct timeval start;
    struct timeval end;
    long seconds;
    long long mili;
    int i;
    numero_threads[0]=1;
    numero_threads[1]=2;
    numero_threads[2]=4;
    numero_threads[3]=5;
    numero_threads[4]=8;
    long_numero_threads = sizeof(numero_threads)/sizeof(numero_threads[0]);
    for(i=0;i<long_numero_threads;i++){
        ns_p=n/numero_threads[i];
        gettimeofday(&start, NULL);
        #pragma omp parallel num_threads(numero_threads[i]) reduction(+: suma_global)
            suma_global+=Rcf_parallel(a,h_hat,ns_p);
        suma_global = h_hat*suma_global;
        gettimeofday(&end, NULL);
        seconds = (end.tv_sec - start.tv_sec);
        mili = 1000*(seconds) + (end.tv_usec - start.tv_usec)/1000;
        printf("Integral de %f a %f = %1.15e\n", a,b,suma_global);
        printf("Error relativo de la solución: %1.15e\n", fabs(suma_global-objetivo)/fabs(objetivo));
        printf("Tiempo de ejecución con %d threads: %lld miliseconds\n", numero_threads[i],mili);
        printf("----------------------\n");
        suma_global=0.0;
    }
    return 0;
}

//definición de funciones:
double Rcf_parallel(double a, double h_hat, int ns_p){
    int begin, end;
    int mi_rango = omp_get_thread_num();
    double local_int=0;
    int i;
    double x;
    begin = mi_rango*ns_p;
    end = begin + ns_p; 
    for(i=begin;i<=end-1;i++){
        x = a+(i+1/2.0)*h_hat;
        local_int+=f(x);
    }   
    return local_int;
}
        
double f(double nodo){
    double valor_f;
    valor_f = exp(-pow(nodo,2));
    return valor_f;
}


Integral de 0.000000 a 1.000000 = 7.468241328124773e-01
Error relativo de la solución: 6.719397313003120e-14
Tiempo de ejecución con 1 threads: 32 miliseconds
----------------------
Integral de 0.000000 a 1.000000 = 7.468241328124707e-01
Error relativo de la solución: 5.842307840730588e-14
Tiempo de ejecución con 2 threads: 17 miliseconds
----------------------
Integral de 0.000000 a 1.000000 = 7.468241328124631e-01
Error relativo de la solución: 4.816559135869493e-14
Tiempo de ejecución con 4 threads: 12 miliseconds
----------------------
Integral de 0.000000 a 1.000000 = 7.468241328124646e-01
Error relativo de la solución: 5.024682061493483e-14
Tiempo de ejecución con 5 threads: 11 miliseconds
----------------------
Integral de 0.000000 a 1.000000 = 7.468241328124514e-01
Error relativo de la solución: 3.255637193689565e-14
Tiempo de ejecución con 8 threads: 19 miliseconds
----------------------

Ejercicio: Calcular tiempo de ejecución para la regla de Simpson. No olviden medir errores relativos. Tal regla está en 1.5.Integracion_numerica,

Referencias:

Para más sobre OpenMP: