tutorial español ejemplos caracteristicas parallel-processing openmp

parallel processing - español - diferencia entre omp crítica y omp solo



openmp tutorial (2)

Estoy tratando de entender la diferencia exacta entre #pragma omp critical y #pragma omp single en OpenMP:

Las definiciones de Microsoft para estos son:

  • Único: le permite especificar que una sección de código debe ejecutarse en un solo hilo, no necesariamente el hilo maestro.
  • Crítico: especifica que el código solo se ejecuta en un hilo a la vez.

Por lo tanto, significa que en ambas, la sección exacta del código luego se ejecutará solo por un hilo y las otras no ingresarán a esa sección, por ejemplo, si imprimimos algo, veremos el resultado en la pantalla una vez, ¿verdad?

¿Qué hay de la diferencia? Parece que los críticos cuidan el tiempo de ejecución, pero no el single! ¡Pero no veo ninguna diferencia en la práctica! ¿Significa que un tipo de espera o sincronización para otros subprocesos (que no entran en esa sección) se considera crítico, pero no hay nada que mantenga a otros subprocesos en una sola? ¿Cómo puede cambiar el resultado en la práctica?

Aprecio si alguien me puede aclarar esto especialmente con un ejemplo. ¡Gracias!


single y critical pertenecen a dos clases completamente diferentes de construcciones OpenMP. single es una construcción de trabajo compartido, junto a for y sections . Las construcciones de trabajo compartido se utilizan para distribuir una cierta cantidad de trabajo entre los hilos. Tales construcciones son "colectivas" en el sentido de que en los programas OpenMP correctos, todos los subprocesos deben encontrarlos mientras se ejecutan y, además, en el mismo orden secuencial, también incluyen las construcciones de barrier . Las tres construcciones de trabajo compartido cubren tres casos generales diferentes:

  • for (también conocido como construcción de bucle) distribuye automáticamente las iteraciones de un bucle entre los hilos: en la mayoría de los casos, todos los hilos tienen trabajo que hacer;
  • sections distribuyen una secuencia de bloques de código independientes entre los subprocesos; algunos subprocesos hacen un trabajo que hacer. Esta es una generalización de la construcción for que un bucle con 100 iteraciones pueda expresarse como, por ejemplo, 10 secciones de bucles con 10 iteraciones cada una.
  • single escoge un bloque de código para ser ejecutado por un solo subproceso, a menudo el primero que lo encuentra (un detalle de implementación), solo un subproceso funciona. single es en gran medida equivalente a sections con una sola sección.

Un rasgo común de todas las construcciones de trabajo compartido es la presencia de una barrera implícita en su extremo, cuya barrera se puede desactivar agregando la cláusula nowait a la construcción OpenMP correspondiente, pero el estándar no requiere tal comportamiento y con algunos tiempos de ejecución OpenMP la barrera Podría seguir estando allí a pesar de la presencia de nowait . Por lo tanto, las construcciones de trabajo compartido incorrectamente ordenadas (es decir, fuera de secuencia en algunos de los subprocesos) podrían llevar a puntos muertos. Un programa correcto de OpenMP nunca se bloqueará cuando las barreras estén presentes.

critical es un constructo de sincronización, junto con el master , el atomic y otros. Las construcciones de sincronización se usan para prevenir condiciones de carrera y para poner orden en la ejecución de las cosas.

  • critical evita las condiciones de carrera al evitar la ejecución simultánea de código entre los subprocesos en el llamado grupo de contención . Esto significa que todos los subprocesos de todas las regiones paralelas que se encuentran con construcciones críticas con nombres similares se serializan;
  • atomic convierte ciertas operaciones simples de memoria en operaciones atómicas, generalmente utilizando instrucciones de ensamblaje especiales. La atómica se completa a la vez como una sola unidad no rompible. Por ejemplo, una lectura atómica desde alguna ubicación por un hilo, que ocurre simultáneamente con una escritura atómica en la misma ubicación por otro hilo, devolverá el valor antiguo o el valor actualizado, pero nunca algún tipo de mezcla intermedia de bits tanto del antiguo como del nuevo valor;
  • master selecciona un bloque de código para que lo ejecute solo el hilo maestro (hilo con ID de 0). A diferencia de single , no hay una barrera implícita al final de la construcción y tampoco hay ningún requisito de que todos los hilos deben encontrar la construcción master . Además, la falta de una barrera implícita significa que el master no vacía la vista de memoria compartida de los subprocesos (esta es una parte importante pero muy mal entendida de OpenMP). master es básicamente una forma abreviada de if (omp_get_thread_num() == 0) { ... } .

critical es una construcción muy versátil ya que puede serializar diferentes partes de código en partes muy diferentes del código del programa, incluso en diferentes regiones paralelas (solo en el caso del paralelismo anidado). Cada construcción critical tiene un nombre opcional provisto entre paréntesis inmediatamente después. Las construcciones críticas anónimas comparten el mismo nombre específico de la implementación. Una vez que un subproceso entra en tal construcción, cualquier otro subproceso que se encuentre con otra construcción del mismo nombre se pone en espera hasta que el hilo original sale de su construcción. Luego el proceso de serialización continúa con el resto de los hilos.

Una ilustración de los conceptos anteriores sigue. El siguiente código:

#pragma omp parallel num_threads(3) { foo(); bar(); ... }

resulta en algo como:

thread 0: -----< foo() >< bar() >--------------> thread 1: ---< foo() >< bar() >----------------> thread 2: -------------< foo() >< bar() >------>

(el hilo 2 es intencionalmente un latecomer)

Teniendo el foo(); llamar dentro de una single construcción:

#pragma omp parallel num_threads(3) { #pragma omp single foo(); bar(); ... }

resulta en algo como:

thread 0: ------[-------|]< bar() >-----> thread 1: ---[< foo() >-|]< bar() >-----> thread 2: -------------[|]< bar() >----->

Aquí [ ... ] denota el alcance del constructo single y | Es la barrera implícita en su extremo. Tenga en cuenta cómo el subproceso 2 hace que todos los demás subprocesos esperen El subproceso 1 ejecuta la llamada foo() ya que el tiempo de ejecución de OpenMP de ejemplo elige asignar el trabajo al primer subproceso para encontrar la construcción.

Agregar una cláusula nowait podría eliminar la barrera implícita, dando como resultado algo como:

thread 0: ------[]< bar() >-----------> thread 1: ---[< foo() >]< bar() >-----> thread 2: -------------[]< bar() >---->

Teniendo el foo(); llamar dentro de una construcción critical anónima:

#pragma omp parallel num_threads(3) { #pragma omp critical foo(); bar(); ... }

resulta en algo como:

thread 0: ------xxxxxxxx[< foo() >]< bar() >--------------> thread 1: ---[< foo() >]< bar() >-------------------------> thread 2: -------------xxxxxxxxxxxx[< foo() >]< bar() >--->

Con xxxxx... se muestra el tiempo que un subproceso pasa esperando otros subprocesos ejecutando una construcción crítica del mismo nombre antes de que pueda ingresar su propia construcción.

Las construcciones críticas de nombres diferentes no se sincronizan entre sí. P.ej:

#pragma omp parallel num_threads(3) { if (omp_get_thread_num() > 1) { #pragma omp critical(foo2) foo(); } else { #pragma omp critical(foo01) foo(); } bar(); ... }

resulta en algo como:

thread 0: ------xxxxxxxx[< foo() >]< bar() >----> thread 1: ---[< foo() >]< bar() >---------------> thread 2: -------------[< foo() >]< bar() >----->

Ahora el subproceso 2 no se sincroniza con los otros subprocesos porque su construcción crítica tiene un nombre diferente y, por lo tanto, realiza una llamada simultánea potencialmente peligrosa a foo() .

Por otro lado, las construcciones críticas anónimas (y en general las construcciones con el mismo nombre) se sincronizan entre sí, sin importar en qué parte del código se encuentren:

#pragma omp parallel num_threads(3) { #pragma omp critical foo(); ... #pragma omp critical bar(); ... }

y la línea de tiempo de ejecución resultante:

thread 0: ------xxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]------------> thread 1: ---[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]-----------------------> thread 2: -------------xxxxxxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]->


single y critical son dos cosas muy diferentes . Como lo mencionaste:

  • single especifica que una sección de código debe ejecutarse por un solo hilo (no necesariamente el hilo maestro)
  • critical especifica que el código es ejecutado por un hilo a la vez

Por lo tanto, el primero se ejecutará solo una vez, mientras que el último se ejecutará tantas veces como haya de hilos .

Por ejemplo el siguiente código

int a=0, b=0; #pragma omp parallel num_threads(4) { #pragma omp single a++; #pragma omp critical b++; } printf("single: %d -- critical: %d/n", a, b);

imprimirá

single: 1 -- critical: 4

Espero que veas mejor la diferencia ahora.

En aras de la integridad, puedo añadir que:

  • master es muy similar al single con dos diferencias:
    1. master será ejecutado por el maestro solo mientras que single puede ejecutarse por cualquier hilo que llegue primero a la región; y
    2. single tiene una barrera implícita al completar la región, donde todos los subprocesos esperan la sincronización, mientras que el master no tiene ninguno.
  • atomic es muy similar a critical , pero está restringido para una selección de operaciones simples.

Agregué estas precisiones ya que estos dos pares de instrucciones son a menudo los que las personas tienden a confundir ...