El paralelismo a nivel de bucle es una forma de paralelismo en la programación de software que se centra en extraer tareas paralelas de los bucles . La oportunidad para el paralelismo a nivel de bucle surge a menudo en programas informáticos donde los datos se almacenan en estructuras de datos de acceso aleatorio . Mientras que un programa secuencial itera sobre la estructura de datos y opera sobre los índices uno a uno, un programa que aprovecha el paralelismo a nivel de bucle utiliza múltiples hilos o procesos que operan sobre algunos o todos los índices simultáneamente. Este paralelismo proporciona una aceleración en el tiempo de ejecución total del programa, generalmente en consonancia con la ley de Amdahl .
Descripción
Para bucles simples, donde cada iteración es independiente de las demás, el paralelismo a nivel de bucle puede ser fácilmente paralelizable , ya que la paralelización solo requiere asignar un proceso para manejar cada iteración. Sin embargo, muchos algoritmos están diseñados para ejecutarse secuencialmente y fallan cuando los procesos paralelos compiten debido a la dependencia dentro del código. Los algoritmos secuenciales a veces se pueden aplicar a contextos paralelos con ligeras modificaciones. Sin embargo, por lo general, requieren sincronización de procesos . La sincronización puede ser implícita, mediante el paso de mensajes , o explícita, mediante primitivas de sincronización como los semáforos .
Ejemplo
Considere el siguiente código que opera sobre una lista l, donde l.size() == n.
for ( int i = 0 ; i < n ; ++ i ) { l [ i ] += 10 ; // s1 }Cada iteración del bucle toma el valor del índice actual de ly lo incrementa en 10. Si la instrucción s1tarda Ttiempo en ejecutarse, entonces el bucle tarda tiempo n * Ten ejecutarse secuencialmente, ignorando el tiempo que tardan las construcciones del bucle. Ahora, consideremos un sistema con pprocesadores donde p > n. Si nlos hilos se ejecutan en paralelo, el tiempo para ejecutar todos nlos pasos se reduce a T.
Los casos menos simples producen resultados inconsistentes, es decir, no serializables . Considere el siguiente bucle que opera sobre la misma lista l.
for ( int i = 0 ; i < n ; ++ i ) { l [ i ] = l [ i - 1 ] + 10 ; // s1 }En cada iteración, el índice actual se establece como el valor del índice anterior más diez. Al ejecutarse secuencialmente, se garantiza que cada iteración ya tendrá el valor correcto. Con múltiples hilos, la planificación de procesos y otras consideraciones impiden que el orden de ejecución garantice que una iteración se ejecute solo después de que se cumpla su dependencia. Es muy posible que esto ocurra antes, lo que puede generar resultados inesperados. La serializabilidad se puede restablecer agregando sincronización para preservar la dependencia de las iteraciones anteriores.
Dependencias en el código
Existen varios tipos de dependencias que se pueden encontrar dentro del código. [ 1 ] [ 2 ]
Para preservar el comportamiento secuencial de un bucle cuando se ejecuta en paralelo, debe conservarse la dependencia verdadera. La antidependencia y la dependencia de salida pueden abordarse asignando a cada proceso su propia copia de las variables (lo que se conoce como privatización). [ 1 ]
Ejemplo de dependencia verdadera
ent a , b ; // s1 a = 2 ; // s2 b = a + 40 ; // s3s2 ->T s3, lo que significa que s2 tiene una verdadera dependencia de s3 porque s2 escribe en la variable a, de la cual s3 lee.
Ejemplo de antidependencia
int a , b = 40 ; // s1 a = b - 38 ; // s2 b = -1 ; // s3s2 ->A s3, lo que significa que s2 tiene una antidependencia de s3 porque s2 lee de la variable bantes de que s3 escriba en ella.
Ejemplo de dependencia de la salida
int a , b = 40 ; // s1 a = b - 38 ; // s2 a = 2 ; // s3s2 ->O s3, lo que significa que s2 tiene una dependencia de salida de s3 porque ambos escriben en la variable a.
Ejemplo de dependencia de entrada
int a , b , c = 2 ; // s1 a = c - 1 ; // s2 b = c + 1 ; // s3s2 ->I s3, lo que significa que s2 tiene una dependencia de entrada de s3 porque tanto s2 como s3 leen de la variable c.
Dependencia en bucles
Dependencia mediada por bucles frente a dependencia independiente de bucles
Los bucles pueden tener dos tipos de dependencia:
- Dependencia transmitida por bucles
- Dependencia independiente del bucle
En la dependencia independiente de bucles, los bucles presentan dependencia entre iteraciones, pero no entre iteraciones. Cada iteración puede tratarse como un bloque y ejecutarse en paralelo sin necesidad de sincronización adicional.
En el siguiente código de ejemplo utilizado para intercambiar los valores de dos matrices de longitud n, existe una dependencia independiente del bucle de s1 ->T s3.
for ( int i = 1 ; i < n ; ++ i ) { tmp = a [ i ]; // s1 a [ i ] = b [ i ]; // s2 b [ i ] = tmp ; // s3 }En la dependencia de bucle, las instrucciones de una iteración dependen de las instrucciones de otra iteración del mismo bucle. La dependencia de bucle utiliza una versión modificada de la notación de dependencia vista anteriormente.
Ejemplo de dependencia llevada por bucle donde s1[i] ->T s1[i + 1], donde iindica la iteración actual, e i + 1indica la siguiente iteración.
for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + 1 ; // s1 }Grafo de dependencia transportada por bucle
Un grafo de dependencias de bucle muestra gráficamente las dependencias entre iteraciones. Cada iteración se representa como un nodo en el grafo, y las aristas dirigidas muestran las dependencias verdaderas, inversas y de salida entre cada iteración.
Tipos
Existen diversas metodologías para paralelizar bucles.
- Bucle DISTRIBUIDO
- Paralelismo de DOALL
- Paralelismo DOACROSS
- HÉLICE [ 3 ]
- Paralelismo DOPIPE
Cada implementación varía ligeramente en la forma en que se sincronizan los hilos, si es que se sincronizan. Además, las tareas paralelas deben asignarse de alguna manera a un proceso. Estas tareas pueden asignarse de forma estática o dinámica. Las investigaciones han demostrado que el equilibrio de carga se logra mejor mediante algunos algoritmos de asignación dinámica que cuando se realiza de forma estática. [ 4 ]
El proceso de paralelización de un programa secuencial se puede dividir en los siguientes pasos discretos. [ 1 ] Cada paralelización de bucle concreta que se muestra a continuación los realiza implícitamente.
Bucle DISTRIBUIDO
Cuando un bucle tiene una dependencia entre bucles, una forma de paralelizarlo es distribuirlo en varios bucles diferentes. Las instrucciones que no dependen unas de otras se separan para que estos bucles distribuidos puedan ejecutarse en paralelo. Por ejemplo, considere el siguiente código.
for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 c [ i ] += d [ i ]; // s2 }El bucle tiene una dependencia de bucle, s1[i] ->T s1[i + 1]pero s2 y s1 no tienen una dependencia independiente del bucle, por lo que podemos reescribir el código de la siguiente manera.
// bucle1 para ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 }// bucle2 para ( int i = 1 ; i < n ; ++ i ) { c [ i ] += d [ i ]; // s2 }Tenga en cuenta que ahora loop1 y loop2 pueden ejecutarse en paralelo. En lugar de que una sola instrucción se ejecute en paralelo sobre diferentes datos, como en el paralelismo a nivel de datos, aquí diferentes bucles realizan diferentes tareas sobre diferentes datos. Digamos que el tiempo de ejecución de s1 y s2 esyEntonces, el tiempo de ejecución para la forma secuencial del código anterior esAhora, debido a que dividimos las dos instrucciones y las colocamos en dos bucles diferentes, obtenemos un tiempo de ejecución deA este tipo de paralelismo lo denominamos paralelismo de funciones o paralelismo de tareas.
paralelismo de MUÑECA
El paralelismo DOALL existe cuando las instrucciones dentro de un bucle pueden ejecutarse de forma independiente (situaciones en las que no hay dependencia entre bucles). [ 1 ] Por ejemplo, el siguiente código no lee del array ay no actualiza los arrays b, c. Ninguna iteración depende de ninguna otra.
for ( int i = 0 ; i < n ; ++ i ) { a [ i ] = b [ i ] + c [ i ]; // s1 }Digamos que el tiempo de una ejecución de s1 esEntonces, el tiempo de ejecución para la forma secuencial del código anterior esAhora bien, debido a que el paralelismo DOALL existe cuando todas las iteraciones son independientes, se puede lograr una aceleración ejecutando todas las iteraciones en paralelo, lo que nos da un tiempo de ejecución de, que es el tiempo que tarda una iteración en la ejecución secuencial.
El siguiente ejemplo, utilizando un pseudocódigo simplificado, muestra cómo se podría paralelizar un bucle para ejecutar cada iteración de forma independiente.
beginParallelism ();for ( int i = 0 ; i < n ; ++ i ) { a [ i ] = b [ i ] + c [ i ]; // s1 endParallelism (); }bloquear ();Paralelismo DOACROSS
El paralelismo DOACROSS existe cuando las iteraciones de un bucle se paralelizan extrayendo cálculos que pueden realizarse de forma independiente y ejecutándolos simultáneamente. [ 5 ]
La sincronización existe para imponer la dependencia transmitida por el bucle.
Considere el siguiente bucle síncrono con dependencia s1[i] ->T s1[i + 1].
para ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ] + 1 ; }Cada iteración del bucle realiza dos acciones.
- Calcular
a[i - 1] + b[i] + 1 - Asigne el valor a
a[i]
El cálculo del valor a[i - 1] + b[i] + 1y la posterior realización de la asignación se pueden descomponer en dos líneas (instrucciones s1 y s2):
int tmp = b [ i ] + 1 ; // s1 a [ i ] = a [ i - 1 ] + tmp ; // s2La primera línea, int tmp = b[i] + 1;, no tiene dependencia de bucle. El bucle se puede paralelizar calculando el valor temporal en paralelo y luego sincronizando la asignación a a[i].
post ( 0 ); for ( int i = 1 ; i < n ; ++ i ) { int tmp = b [ i ] + 1 ; // s1 wait ( i - 1 );a [ i ] = a [ i - 1 ] + tmp ; // s2 post ( i ); }Digamos que el tiempo de ejecución de s1 y s2 esyEntonces, el tiempo de ejecución para la forma secuencial del código anterior esAhora bien, debido a que existe el paralelismo DOACROSS, se puede lograr una aceleración ejecutando iteraciones de forma segmentada, lo que nos da un tiempo de ejecución de.
paralelismo DOPIPE
DOPIPE Parallelism implementa paralelismo segmentado para dependencias entre bucles, donde una iteración de bucle se distribuye entre múltiples bucles sincronizados. [ 1 ] El objetivo de DOPIPE es funcionar como una cadena de montaje, donde una etapa se inicia tan pronto como haya suficientes datos disponibles de la etapa anterior. [ 6 ]
Considere el siguiente código síncrono con dependencia s1[i] ->T s1[i + 1].
for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 c [ i ] += a [ i ]; // s2 }s1 debe ejecutarse secuencialmente, pero s2 no tiene dependencia de bucle. s2 podría ejecutarse en paralelo usando el paralelismo DOALL después de realizar todos los cálculos necesarios para s1 en serie. Sin embargo, la mejora de velocidad es limitada si se hace de esta manera. Un enfoque mejor es paralelizar de forma que el s2 correspondiente a cada s1 se ejecute cuando dicho s1 haya finalizado.
La implementación del paralelismo segmentado da como resultado el siguiente conjunto de bucles, donde el segundo bucle puede ejecutarse para un índice tan pronto como el primer bucle haya terminado con su índice correspondiente.
for ( int i = 1 ; i < n ; ++ i ) { a [ i ] = a [ i - 1 ] + b [ i ]; // s1 post ( i ); }for ( int i = 1 ; i < n ; i ++ ) { wait ( i ); c [ i ] += a [ i ]; // s2 }Digamos que el tiempo de ejecución de s1 y s2 esyEntonces, el tiempo de ejecución para la forma secuencial del código anterior esAhora bien, debido a que existe el paralelismo DOPIPE, se puede lograr una aceleración ejecutando iteraciones de forma segmentada, lo que nos da un tiempo de ejecución dedonde p es el número de procesadores en paralelo.
Véase también
- paralelismo de datos
- Paralelismo DOACROSS
- Paralelismo de tareas
- Paralelismo mediante diferentes tipos de modelos de memoria, como memoria compartida , memoria distribuida y paso de mensajes.
Referencias
- ^ Solihin , Yan ( 2016 ) . Fundamentos de la Arquitectura Paralela . Boca Ratón, FL: CRC Press. ISBN 978-1-4822-1118-4.
- ↑ Goff, Gina (1991). «Pruebas prácticas de dependencia». Actas de la conferencia ACM SIGPLAN 1991 sobre diseño e implementación de lenguajes de programación - PLDI '91 . págs. 15–29 . doi : 10.1145/113445.113448 . ISBN 0897914287. S2CID 2357293 .
- ↑ Murphy, Niall. "Descubriendo y explotando el paralelismo en bucles DOACROSS" (PDF) . Universidad de Cambridge . Consultado el 10 de septiembre de 2016 .
- ↑ Kavi, Krishna. "Paralelización de bucles DOALL y DOACROSS: una revisión" .
{{cite journal}}: Para citar una revista se requiere|journal=( ayuda ) - ↑ Unnikrishnan, Priya (2012), "Un enfoque práctico para la paralelización de DOACROSS", Procesamiento paralelo Euro-Par 2012 , Lecture Notes in Computer Science, vol. 7484, pp. 219–231 , doi : 10.1007/978-3-642-32820-6_23 , ISBN 978-3-642-32819-0, S2CID 18571258
- ↑ "DoPipe: Un enfoque eficaz para paralelizar la simulación" (PDF) . Intel . Consultado el 13 de septiembre de 2016 .
- Computación paralela