En informática , una barrera de memoria , también conocida como barrera de memoria , valla de memoria o instrucción de valla , es un tipo de instrucción de barrera que hace que una unidad central de procesamiento (CPU) o un compilador apliquen una restricción de ordenación a las operaciones de memoria emitidas antes y después de la instrucción de barrera. Esto normalmente significa que se garantiza que las operaciones emitidas antes de la barrera se realicen antes que las operaciones emitidas después de la barrera.
Las barreras de memoria son necesarias porque la mayoría de las CPU modernas emplean optimizaciones de rendimiento que pueden dar como resultado una ejecución fuera de orden . Esta reordenación de las operaciones de memoria (cargas y almacenamientos) normalmente pasa desapercibida dentro de un único hilo de ejecución , pero puede causar un comportamiento impredecible en programas concurrentes y controladores de dispositivos a menos que se controle cuidadosamente. La naturaleza exacta de una restricción de ordenación depende del hardware y está definida por el modelo de ordenación de memoria de la arquitectura . Algunas arquitecturas proporcionan múltiples barreras para aplicar diferentes restricciones de ordenación.
Las barreras de memoria se utilizan normalmente al implementar código de máquina de bajo nivel que opera en memoria compartida por varios dispositivos. Dicho código incluye primitivas de sincronización y estructuras de datos sin bloqueos en sistemas multiprocesador , y controladores de dispositivos que se comunican con el hardware de la computadora .
Cuando un programa se ejecuta en una máquina con una sola CPU, el hardware realiza la contabilidad necesaria para garantizar que el programa se ejecute como si todas las operaciones de memoria se realizaran en el orden especificado por el programador (orden del programa), por lo que no son necesarias las barreras de memoria. Sin embargo, cuando la memoria se comparte con varios dispositivos, como otras CPU en un sistema multiprocesador o periféricos mapeados en memoria , el acceso fuera de orden puede afectar el comportamiento del programa. Por ejemplo, una segunda CPU puede ver los cambios de memoria realizados por la primera CPU en una secuencia que difiere del orden del programa.
Un programa se ejecuta a través de un proceso que puede tener múltiples subprocesos (es decir, un subproceso de software como pthreads en lugar de un subproceso de hardware). Los diferentes procesos no comparten un espacio de memoria, por lo que esta discusión no se aplica a dos programas, cada uno ejecutándose en un proceso diferente (por lo tanto, un espacio de memoria diferente). Se aplica a dos o más subprocesos (de software) que se ejecutan en un solo proceso (es decir, un solo espacio de memoria donde varios subprocesos de software comparten un solo espacio de memoria). Múltiples subprocesos de software, dentro de un solo proceso, pueden ejecutarse simultáneamente en un procesador de múltiples núcleos .
El siguiente programa multiproceso, que se ejecuta en un procesador multinúcleo, ofrece un ejemplo de cómo dicha ejecución fuera de orden puede afectar el comportamiento del programa:
Inicialmente, las ubicaciones de memoria x
y f
ambas contienen el valor 0
. El subproceso de software que se ejecuta en el procesador n.° 1 realiza un bucle mientras el valor de f
es cero, luego imprime el valor de x
. El subproceso de software que se ejecuta en el procesador n.° 2 almacena el valor 42
en x
y luego almacena el valor 1
en f
. A continuación se muestra el pseudocódigo para los dos fragmentos de programa.
Los pasos del programa corresponden a instrucciones individuales del procesador.
En el caso del procesador PowerPC, la eioio
instrucción asegura, como barrera de memoria, que cualquier operación de carga o almacenamiento iniciada previamente por el procesador se complete completamente con respecto a la memoria principal antes de que cualquier operación de carga o almacenamiento posterior iniciada por el procesador acceda a la memoria principal. [1] [2]
Hilo #1 Núcleo #1:
mientras ( f == 0 ); // Aquí se requiere una barrera de memoria print x ;
Hilo #2 Núcleo #2:
x = 42 ; // Aquí se requiere una barrera de memoria f = 1 ;
Se podría esperar que la sentencia print siempre imprima el número "42"; sin embargo, si las operaciones de almacenamiento del hilo n.° 2 se ejecutan fuera de orden, es posible que f
se actualice antes de x
, y por lo tanto, la sentencia print podría imprimir "0". De manera similar, las operaciones de carga del hilo n.° 1 pueden ejecutarse fuera de orden y es posible que x
se lea antes de f
que se verifique , y nuevamente, por lo tanto, la sentencia print podría imprimir un valor inesperado. Para la mayoría de los programas, ninguna de estas situaciones es aceptable. Se debe insertar una barrera de memoria antes de la asignación del hilo n.° 2 a para f
garantizar que el nuevo valor de x
sea visible para otros procesadores en el momento o antes del cambio en el valor de f
. Otro punto importante es que también se debe insertar una barrera de memoria antes del acceso del hilo n.° 1 a x
para garantizar que el valor de x
no se lea antes de ver el cambio en el valor de f
.
Otro ejemplo es cuando un conductor realiza la siguiente secuencia:
Preparar datos para un módulo de hardware // Aquí se requiere una barrera de memoria para activar el módulo de hardware para procesar los datos
Si las operaciones de almacenamiento del procesador se ejecutan fuera de orden, el módulo de hardware puede activarse antes de que los datos estén listos en la memoria.
Para otro ejemplo ilustrativo (no trivial, pero que surge en la práctica real), véase el bloqueo con doble verificación .
Los programas multiproceso suelen utilizar primitivas de sincronización proporcionadas por un entorno de programación de alto nivel (como Java o .NET ) o una interfaz de programación de aplicaciones (API) como POSIX Threads o Windows API . Las primitivas de sincronización, como los mutex y los semáforos, se proporcionan para sincronizar el acceso a los recursos desde subprocesos de ejecución paralelos. Estas primitivas suelen implementarse con las barreras de memoria necesarias para proporcionar la semántica de visibilidad de memoria esperada . En dichos entornos, el uso explícito de barreras de memoria no suele ser necesario.
Las instrucciones de barrera de memoria abordan los efectos de reordenamiento solo a nivel de hardware. Los compiladores también pueden reordenar las instrucciones como parte del proceso de optimización del programa . Aunque los efectos sobre el comportamiento del programa paralelo pueden ser similares en ambos casos, en general es necesario tomar medidas independientes para inhibir las optimizaciones de reordenamiento del compilador para los datos que pueden ser compartidos por varios subprocesos de ejecución.
En C y C++ , la palabra clave volátil fue pensada para permitir que los programas de C y C++ accedieran directamente a la E/S asignada a la memoria. La E/S asignada a la memoria generalmente requiere que las lecturas y escrituras especificadas en el código fuente ocurran en el orden exacto especificado sin omisiones. Las omisiones o reordenamientos de lecturas y escrituras por parte del compilador interrumpirían la comunicación entre el programa y el dispositivo al que accede la E/S asignada a la memoria. El compilador de AC o C++ no puede omitir lecturas desde y escrituras en ubicaciones de memoria volátiles, ni puede reordenar lecturas/escrituras en relación con otras acciones similares para la misma ubicación volátil (variable). La palabra clave volátil no garantiza una barrera de memoria para hacer cumplir la consistencia de la caché. Por lo tanto, el uso de volátil por sí solo no es suficiente para usar una variable para la comunicación entre subprocesos en todos los sistemas y procesadores. [3]
Los estándares C y C++ anteriores a C11 y C++11 no abordan múltiples subprocesos (o múltiples procesadores), [4] y, como tal, la utilidad de volátil depende del compilador y del hardware. Aunque volátil garantiza que las lecturas volátiles y las escrituras volátiles ocurrirán en el orden exacto especificado en el código fuente, el compilador puede generar código (o la CPU puede reordenar la ejecución) de modo que una lectura o escritura volátil se reordene con respecto a las lecturas o escrituras no volátiles, lo que limita su utilidad como indicador entre subprocesos o mutex.