La privatización es una técnica utilizada en la programación con memoria compartida para habilitar el paralelismo , eliminando las dependencias que se producen entre diferentes hilos en un programa paralelo . Las dependencias entre hilos surgen cuando dos o más hilos leen o escriben una variable al mismo tiempo. La privatización proporciona a cada hilo una copia privada, de modo que puede leerla y escribirla de forma independiente y, por lo tanto, simultánea. [ 1 ]
Cada algoritmo paralelo especifica si una variable es compartida o privada. Pueden surgir muchos errores de implementación si la variable se declara como compartida, pero el algoritmo requiere que sea privada, o viceversa. [ 2 ]
Tradicionalmente, los compiladores de paralelización solo podían aplicar la privatización a elementos escalares. Para aprovechar el paralelismo que se produce entre iteraciones dentro de un programa paralelo ( paralelismo a nivel de bucle ), surgió la necesidad de compiladores que también pudieran realizar la privatización de variables de matriz. [ 3 ] La mayoría de los compiladores actuales pueden realizar la privatización de matrices con más características y funciones para mejorar el rendimiento del programa paralelo en general. Un ejemplo es el compilador de paralelización Polaris. [ 4 ]
Descripción
Un multiprocesador de memoria compartida es un "sistema informático compuesto por múltiples procesadores independientes que ejecutan diferentes secuencias de instrucciones". [ 4 ] El modelo de programación de memoria compartida es el más utilizado para el diseño de procesadores paralelos. [ 1 ] Este modelo de programación comienza identificando las posibilidades de paralelismo dentro de un fragmento de código y luego asignando estas tareas paralelas a hilos.
El siguiente paso consiste en determinar el alcance de las variables utilizadas en un programa paralelo, lo cual es uno de los pasos clave y una de las principales preocupaciones dentro de este modelo.
Alcance variable
El siguiente paso del modelo agrupa las tareas en tareas más grandes, ya que normalmente hay más tareas que procesadores disponibles. Por lo general, el número de hilos de ejecución asignados a las tareas se elige de forma que sea menor o igual al número de procesadores, asignándose cada hilo a un procesador único. [ 1 ]
Inmediatamente después de este paso, es necesario analizar el uso de variables dentro de las tareas. Este paso determina si cada variable debe ser compartida por todos o privada para cada hilo. [ 1 ] Este paso es exclusivo de la programación con memoria compartida. (Una alternativa es el paso de mensajes , en el que todas las variables son privadas). [ 1 ]
Según su comportamiento, las variables se clasifican de la siguiente manera:
- Solo lectura : cuando una variable solo puede ser leída por todas las tareas paralelas.
- Lectura/escritura sin conflictos : cuando una variable es leída, escrita o ambas por una sola tarea. Si la variable no es escalar, diferentes tareas paralelas pueden leer/escribir distintos elementos.
- Conflicto de lectura/escritura : ocurre cuando una tarea escribe en una variable y otra puede leerla. Si la variable no es escalar, diferentes tareas paralelas leen/escriben elementos distintos.
Como se desprende de su definición, las variables de lectura/escritura conflictivas introducen dependencias entre diferentes hilos de ejecución y, por lo tanto, impiden la paralelización automática del programa. Las dos técnicas principales utilizadas para eliminar estas dependencias son la privatización y la reducción . En la reducción, a cada hilo se le proporciona una copia de la variable de lectura/escritura conflictiva para operar sobre ella y producir un resultado parcial, que luego se combina con las copias de otros hilos para producir un resultado global. [ 1 ] Otra técnica similar a la privatización se denomina expansión , en la que una variable escalar se expande en un array, lo que hace que cada hilo acceda a un elemento diferente del array. [ 5 ] Si la variable que se va a expandir es un array, la expansión añade otra dimensión al array. [ 6 ]
Privatización
Las dependencias —posibles conflictos entre diferentes hilos durante la ejecución— impiden la paralelización, y estos conflictos surgen cuando tenemos variables con permisos de lectura/escritura conflictivos. Una técnica para eliminar estos conflictos es la privatización. El principio básico consiste en crear una copia privada de una variable para cada hilo, en lugar de compartir una única instancia. Esto cambia la categoría de la variable de "Lectura/Escritura conflictiva" a "Lectura/Escritura no conflictiva".
Las instancias locales (privadas) reales de las variables con conflicto de lectura/escritura se crean en tiempo de compilación, asignando varias áreas de memoria para las variables almacenadas en diferentes ubicaciones de memoria. La arquitectura de los multiprocesadores de memoria compartida ayuda, ya que los hilos comparten un espacio de direcciones .
Existen dos situaciones en las que una variable puede describirse como privatizable :
- Cuando la variable se escribe antes de que la lea la misma tarea durante la secuencia de ejecución del programa original. En este caso, si la tarea escribiera en su copia privada en lugar de la compartida, se eliminaría el conflicto/dependencia. Esto se debe a que la secuencia de ejecución del programa garantiza que el valor será el escrito por la misma tarea, eliminando cualquier conflicto que pudiera surgir si otros subprocesos accedieran a la misma variable. Véase el ejemplo 1 .
- Cuando la variable se lee antes de que la misma tarea la escriba. La diferencia radica en que el valor que la tarea intenta leer proviene de un paso de cálculo previo en otra tarea. Pero si cada tarea escribiera en su propia copia privada, cualquier conflicto o dependencia se resolvería durante la ejecución, ya que todas leerían un valor conocido de antemano y luego escribirían sus valores correctos en sus propias copias. Véase el ejemplo 2 .
Dado que las variables con conflictos de lectura/escritura son la única categoría que impide la paralelización, no es necesario declarar explícitamente como privadas las variables de solo lectura y las variables sin conflictos de lectura/escritura. Si bien hacerlo no afectará la corrección del programa, podría consumir más memoria debido a copias innecesarias.
Limitaciones
En ocasiones, una variable no puede ni privatizarse ni reducirse para eliminar el conflicto de lectura/escritura. En estos casos, la variable con conflicto de lectura/escritura debe actualizarse mediante diferentes tareas en diferentes momentos. Véase el ejemplo 3 .
Este problema a veces se puede resolver cambiando el alcance del paralelismo para explorar una región paralela diferente. Esto podría producir buenos resultados, ya que a menudo, después de volver a analizar el código, algunas variables con conflictos de lectura/escritura pueden pasar a ser sin conflictos de lectura/escritura. [ 1 ] Si la variable sigue causando conflictos, el último recurso es declararla como compartida y proteger su acceso mediante alguna forma de exclusión mutua , y proporcionar sincronización si los accesos a la variable deben ocurrir en un orden específico para garantizar la corrección.
Matrices
Las variables que pueden generar conflictos de lectura/escritura pueden ser escalares o de tipos compuestos, como arreglos, matrices, tipos estructurados, etc. La privatización se puede aplicar a ambos tipos de variables.
Cuando se aplica a variables escalares, el espacio adicional y la sobrecarga introducidos al hacer copias privadas adicionales por hilo son relativamente pequeños, porque los escalares son pequeños. [ 1 ] Sin embargo, aplicar la privatización a arreglos, matrices u otros tipos compuestos es mucho más complejo.
Al trabajar con arreglos, el compilador intenta analizar el comportamiento de cada elemento por separado y verificar el orden en que se lee y se escribe. Si cada elemento se escribe antes de leerse en la misma iteración, este arreglo puede privatizarse. Para ello, el compilador necesita analizar el arreglo con mayor detalle para agrupar sus accesos en secciones. Además, el compilador debe contar con funciones adicionales para manipular y gestionar los elementos del arreglo. Por ejemplo, algunas expresiones de arreglo pueden contener términos simbólicos; por lo tanto, para poder privatizar dicho arreglo, el compilador necesita funciones avanzadas de manipulación simbólica . [ 5 ]
Ejemplos
Ejemplo 1
Una variable puede considerarse privada si cada tarea escribe en ella antes de leerla. En este caso, no importa si otros hilos lo hacen. En el código siguiente, la variable xse utiliza para intercambiar tres pares de variables diferentes. Dado que siempre se escribe en ella antes de leerla, puede considerarse privada.
//Código secuencial: x = a; a = b; b = x; x = c; c = d; d = x; x = e; e = f; b = x;
Este código no se puede paralelizar sin privatizarlo x. Con xprivatizado, puede ejecutarse en tres hilos diferentes, cada uno con su propio privado x:
//Código paralelo: //Hilo 1: x[1] = a; a = b; b = x[1]; // Hilo 2: x[2] = c; c = d; d = x[2]; // Hilo 3: x[3]= e; e = f; b = x[3];
Ejemplo 2
La privatización es posible cuando se conoce el valor de una variable antes de usarla, incluso si otra tarea escribe en ella. El siguiente código lo demuestra. La variable xse modifica en medio de cada tarea, pero su valor podría calcularse durante la compilación del programa. Al declararla xprivada y definirla al inicio de cada tarea, el código puede ejecutarse en paralelo.
//Código secuencial: x = 1; y = x * 3; x = 4; z = y/x; a = x * 9; x = 3; b = a/x; c = x * 1; x = 11; d = c/x;
Para que el código secuencial anterior sea paralelo, se deben agregar algunas líneas de código para que xpueda ser privatizado:
//Código paralelo //Hilo 0: x[0] = 1; y = x[0] * 3; x[0] = 4; z = y/x[0]; //Hilo 1: x[1] = 4; a = x[1] * 9; x[1] = 3; b = a/x[1]; //Hilo 2: x[2] = 3; c = x[2] * 1; x[2] = 11; d = c/x[2];
Debido al código adicional, este breve ejemplo podría no mostrar una mejora significativa en la velocidad. Sin embargo, en código más extenso y real, esta técnica puede mejorar considerablemente el rendimiento.
Ejemplo 3
Se produce un fallo de privatización cuando una variable se escribe en una tarea y se lee en otra, sin conocer su valor de antemano. Un ejemplo es la suma de los elementos de un array. La suma es una variable compartida que se lee y se escribe en cada iteración del bucle.
En código secuencial, esto funciona correctamente. Pero si cada iteración se ejecutara en un hilo diferente, se calcularía una suma incorrecta. En este caso, la privatización no funciona. sumNo se puede hacer privada porque depende de su valor de la iteración anterior.
//Código secuencial: suma = 0; para (i = 0; i < 100; i++) suma += a[i];
Este problema se puede resolver en parte mediante el desenrollado de bucles . Dado que no importa el orden en que se agreguen los elementos , el bucle se puede dividir en un número arbitrario de partes:
// Hilo 0: suma[0] = 0; para (i[0] = 0; i[0] < 100; i[0] += 3) suma[0] += a[i[0]]; // Hilo 1: suma[1] = 0; para (i[1] = 1; i[1] < 100; i[1] += 3) suma[1] += a[i[1]]; // Hilo 2: suma[2] = 0; para (i[2] = 2; i[2] < 100; i[2] += 3) suma[2] += a[i[2]]; // Hilo "Maestro": esperar_para_todos(hilo[0], hilo[1], hilo[2]); suma = suma[0] + suma[1] + suma[2];
OpenMP
OpenMP es un lenguaje de programación que admite la programación multiprocesador con memoria compartida. Por ello, inevitablemente se producirán conflictos de lectura/escritura de variables. En estos casos, a veces se puede utilizar la privatización para permitir la ejecución paralela del código.
Dado el código secuencial:
hacer i = 10, N - 1 x = (b(i) + c(i))/2 b(i) = a(i + 1) + x final
En cada iteración del bucle, xse escribe en y luego se lee. Dado que xes solo una variable escalar, el bucle no se puede ejecutar en paralelo porque se sobrescribiría en diferentes hilos y b(i)no siempre se le asignaría el valor correcto. [ 2 ]
El código paralelizado equivalente que utiliza privatización es:
!$omp parallel do shared(a, b) private(x) hacer i = 10, N - 1 x = (b(i) + c(i))/2 b(i) = a(i + 1) + x final
Como xse declara como privado, cada hilo obtiene su propia copia y se elimina la dependencia. [ 2 ]
Comparación con otras técnicas
Normalmente, cuando una variable presenta conflictos de lectura/escritura, la solución consiste en declararla como compartida y proteger su acceso mediante exclusión mutua , lo que proporciona sincronización cuando es necesario. Dado que la exclusión mutua ralentiza el proceso, esta técnica se evita en la medida de lo posible.
Por lo tanto, el compilador o programador primero verifica si la variable se puede reducir. Si no es posible, la siguiente verificación es para privatizarla. La privatización sacrifica espacio por tiempo, por lo que la exclusión mutua puede ser una mejor opción cuando la memoria es limitada.
En comparación con la reducción, la privatización requiere un solo paso de modelado: simplemente analizar el código para identificar las variables privatizables. Por otro lado, la reducción requiere dos pasos: identificar la variable de reducción y luego paralelizar el operador de reducción. [ 7 ] Al observar cada una de las dos técnicas, es fácil determinar qué tipo de sobrecarga agrega cada una al programa paralelo; la reducción aumenta la sobrecarga computacional, mientras que la privatización aumenta la memoria consumida por el programa. [ 5 ]
En comparación con la expansión, la privatización tiene un menor consumo de memoria. El espacio de memoria necesario para la privatización es proporcional al número de procesadores, mientras que en la expansión es proporcional al número de iteraciones. [ 5 ] Dado que el número de tareas suele ser mayor que el número de procesadores, la memoria requerida por la expansión es mucho mayor que la requerida por la privatización.
Se puede cambiar el alcance del paralelismo para explorar una región paralela diferente. Esto a veces puede modificar significativamente el comportamiento de las variables. Por lo tanto, volver a analizar el código y aplicar esta técnica a menudo puede convertir las variables con conflictos de lectura/escritura en variables sin conflictos. [ 1 ]
Véase también
Referencias
- 1 2 3 4 5 6 7 8 9 Solihin, Yan (2015). Fundamentos de la arquitectura multinúcleo paralela . Chapman and Hall/CRC. ISBN 978-1-4822-1118-4.
- 1 2 3 Chandra, Rohit (2001). Penrose, Denise (ed.). Programación paralela en OpenMP (PDF) . Morgan Kaufmann . págs. 48, 74, 143. ISBN 978-1-55860-671-5.
- ↑ Gupta, M. (1997-04-01). "Sobre la privatización de variables para la ejecución paralela de datos". Actas del 11.º Simposio Internacional de Procesamiento Paralelo . págs. 533–541 . CiteSeerX 10.1.1.50.2508 . doi : 10.1109/IPPS.1997.580952 . ISBN 978-0-8186-7793-9. S2CID 17389658 .
- 1 2 Ceze, Luis H. (2011-01-01). "Multiprocesadores de memoria compartida". En Padua, David (ed.). Enciclopedia de computación paralela . Springer US. pp. 1810–1812 . doi : 10.1007/978-0-387-09766-4_142 . ISBN 978-0-387-09765-7.
- 1 2 3 4 Padua, David (2011-01-01). "Paralelización automática". En Padua, David (ed.). Enciclopedia de computación paralela . Springer US. pp. 1442–1450 – Parafraseado por Bruce Leasure. doi : 10.1007/978-0-387-09766-4_197 . ISBN 978-0-387-09765-7.
- ↑ Tu, Peng; Padua, David (1993-08-12). "Automatic Array Privatization". En Banerjee, Utpal; Gelernter, David ; Nicolau, Alex; Padua, David (eds.). Languages and Compilers for Parallel Computing . Lecture Notes in Computer Science. Springer Berlin Heidelberg. pp. 500–521 . CiteSeerX 10.1.1.3.5746 . doi : 10.1007/3-540-57659-2_29 . ISBN 978-3-540-57659-4.
- ↑ Yu, Hao; Rauchwerger, Lawrence (1 de enero de 2014). «Técnicas de paralelización de reducción adaptativa». Volumen del 25.º aniversario de la Conferencia Internacional de Supercomputación de la ACM . Nueva York, NY, EE. UU.: ACM. págs. 311–322 . doi : 10.1145/2591635.2667180 . ISBN 978-1-4503-2840-1. S2CID 52865514 .
- Programación informática