Una barrera de memoria , también conocida como membar , 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 compilador imponga una restricción de orden en las operaciones de memoria emitidas antes y después de la instrucción de barrera. Por lo general, esto significa que se garantiza que las operaciones emitidas antes de la barrera se realizarán antes de 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 resultar en una ejecución desordenada . Este reordenamiento de las operaciones de memoria (cargas y almacenes) normalmente pasa desapercibido dentro de un solo 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 ordenamiento depende del hardware y está definida por el modelo de ordenamiento de memoria de la arquitectura . Algunas arquitecturas proporcionan múltiples barreras para hacer cumplir diferentes restricciones de ordenamiento.
Las barreras de memoria se utilizan normalmente al implementar código de máquina de bajo nivel que opera en memoria compartida por múltiples dispositivos. Dicho código incluye primitivas de sincronización y estructuras de datos sin bloqueo en sistemas multiprocesador y controladores de dispositivos que se comunican con el hardware de la computadora .
Ejemplo
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 hubieran realizado en el orden especificado por el programador (orden del programa), por lo que las barreras de memoria no son necesarias. Sin embargo, cuando la memoria se comparte con varios dispositivos, como otras CPU en un sistema multiprocesador o periféricos asignados en memoria , el acceso desordenado 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 ser de varios subprocesos (es decir, un subproceso de software como pthread 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 en el que varios subprocesos de software comparten un solo espacio de memoria). Varios subprocesos de software, dentro de un solo proceso, pueden ejecutarse simultáneamente en un procesador de múltiples núcleos .
El siguiente programa de varios subprocesos, que se ejecuta en un procesador de varios núcleos, ofrece un ejemplo de cómo dicha ejecución desordenada puede afectar el comportamiento del programa:
Inicialmente, las ubicaciones de la memoria x
y f
ambas mantienen el valor 0
. El subproceso de software que se ejecuta en el procesador n. ° 1 se repite 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 las instrucciones del procesador individual.
Hilo n. ° 1 Núcleo n. ° 1:
while ( f == 0 ); // Se requiere un límite de memoria aquí print x ;
Hilo n. ° 2 Núcleo n. ° 2:
x = 42 ; // Cerca de memoria requerida aquí f = 1 ;
Uno podría esperar que la declaración de impresión siempre imprima el número "42"; sin embargo, si las operaciones de almacenamiento del hilo # 2 se ejecutan fuera de orden, es posible f
que se actualicen antes x
y, por lo tanto, la declaración de impresión podría imprimir "0". De manera similar, las operaciones de carga del hilo # 1 pueden ejecutarse fuera de orden y es posible x
que se lean antes de que f
se verifique, y nuevamente la declaración de impresión podría, por lo tanto, 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 subproceso # 2 f
para garantizar que el nuevo valor de x
sea visible para otros procesadores en 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 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 // El límite de memoria requerido aquí activa 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 ver otro ejemplo ilustrativo (uno no trivial que surge en la práctica real), consulte el bloqueo de doble verificación .
Programación multiproceso y visibilidad de memoria
Los programas multiproceso suelen utilizar primitivas de sincronización proporcionadas por un entorno de programación de alto nivel, como Java y .NET Framework , o una interfaz de programación de aplicaciones (API) como POSIX Threads o API de Windows . Se proporcionan primitivas de sincronización, como mutex y semáforos , para sincronizar el acceso a los recursos desde subprocesos de ejecución paralelos. Estas primitivas generalmente se implementan con las barreras de memoria necesarias para proporcionar la semántica de visibilidad de memoria esperada . En tales entornos, generalmente no es necesario el uso explícito de barreras de memoria.
Cada API o entorno de programación, en principio, tiene su propio modelo de memoria de alto nivel que define su semántica de visibilidad de la memoria. Aunque los programadores no suelen necesitar utilizar barreras de memoria en entornos de tan alto nivel, es importante comprender la semántica de visibilidad de su memoria, en la medida de lo posible. Tal comprensión no es necesariamente fácil de lograr porque la semántica de visibilidad de la memoria no siempre se especifica o documenta de manera consistente.
Así como la semántica del lenguaje de programación se define en un nivel diferente de abstracción que los códigos de operación del lenguaje máquina , el modelo de memoria de un entorno de programación se define en un nivel diferente de abstracción que el de un modelo de memoria de hardware. Es importante comprender esta distinción y darse cuenta de que no siempre existe un mapeo simple entre la semántica de barrera de memoria de hardware de bajo nivel y la semántica de visibilidad de memoria de alto nivel de un entorno de programación particular. Como resultado, la implementación de subprocesos POSIX de una plataforma particular puede emplear barreras más fuertes que las requeridas por la especificación. Los programas que aprovechan la visibilidad de la memoria tal como se implementaron en lugar de lo especificado pueden no ser portátiles.
Ejecución fuera de orden versus optimizaciones de reordenamiento del compilador
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 separadas para inhibir las optimizaciones de reordenamiento del compilador para los datos que pueden ser compartidos por múltiples subprocesos de ejecución. Tenga en cuenta que estas medidas suelen ser necesarias solo para datos que no están protegidos por primitivas de sincronización como las que se comentaron en la sección anterior.
En C y C ++ , la palabra clave volátil estaba destinada a permitir que los programas C y C ++ accedan directamente a E / S mapeadas en memoria . La E / S mapeada en 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 se accede mediante E / S mapeadas en memoria. El compilador de AC o C ++ no puede omitir lecturas y escrituras en ubicaciones de memoria volátil, ni puede reordenar lecturas / escrituras en relación con otras acciones similares para la misma ubicación volátil (variable). La palabra clave volatile no garantiza una barrera de memoria para hacer cumplir la coherencia de la caché. Por tanto, el uso de volátil por sí solo no es suficiente para utilizar una variable para la comunicación entre subprocesos en todos los sistemas y procesadores. [1]
Los estándares C y C ++ anteriores a C11 y C ++ 11 no abordan múltiples subprocesos (o múltiples procesadores), [2] y, como tal, la utilidad de volátil depende del compilador y del hardware. Aunque volatile garantiza que las lecturas volátiles y 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 no- lecturas o escrituras volátiles, lo que limita su utilidad como indicador entre subprocesos o mutex. Evitarlo es específico del compilador, pero algunos compiladores, como gcc , no reordenarán las operaciones alrededor del código ensamblador en línea con volátil y etiquetas de "memoria" , como en: asm volatile ("" ::: "memoria"); (Vea más ejemplos en Orden de memoria # Orden de memoria en tiempo de compilación ). Además, no se garantiza que las lecturas y escrituras volátiles se vean en el mismo orden por otros procesadores o núcleos debido al almacenamiento en caché, el protocolo de coherencia de caché y el orden de memoria relajado, lo que significa que las variables volátiles por sí solas pueden ni siquiera funcionar como marcas entre subprocesos o mutex. .
Ver también
- Algoritmos sin bloqueo y sin espera
- Meltdown (vulnerabilidad de seguridad)
Referencias
- ^ Volátil considerado nocivo - Documentación del kernel de Linux
- ^ Boehm, Hans (junio de 2005). Los subprocesos no se pueden implementar como una biblioteca . Actas de la conferencia ACM SIGPLAN de 2005 sobre diseño e implementación de lenguajes de programación . Asociación de Maquinaria Informática . CiteSeerX 10.1.1.308.5939 . doi : 10.1145 / 1065010.1065042 .
enlaces externos
- Barreras de memoria: una visión de hardware para piratas informáticos
- Consideraciones de multiprocesador para controladores en modo kernel - Versión preliminar - 28 de octubre de 2004
- Informe técnico de HP HPL-2004-209: Los subprocesos no se pueden implementar como biblioteca
- Problemas de barrera de memoria del kernel de Linux en varios tipos de CPU
- Documentación sobre barreras de memoria en el kernel de Linux
- Manejo del ordenamiento de memoria en aplicaciones multiproceso con Oracle Solaris Studio 12 Update 2: Parte 1, Barreras del compilador
- Manejo del ordenamiento de la memoria en aplicaciones multiproceso con Oracle Solaris Studio 12 Update 2: Parte 2, Barreras de memoria y vallas de memoria
- RCU del espacio de usuario: colección de animales de la barrera de la memoria