Entiendo que volatile
le informa al compilador que el valor puede cambiar, pero para lograr esta funcionalidad, ¿el compilador necesita introducir una barrera de memoria para que funcione?
Según tengo entendido, la secuencia de operaciones en objetos volátiles no se puede reordenar y debe conservarse. Esto parece implicar que son necesarias algunas barreras de memoria y que realmente no hay una forma de evitar esto. ¿Estoy en lo correcto al decir esto?
Hay una discusión interesante en esta pregunta relacionada
... Los accesos a distintas variables volátiles no pueden ser reordenados por el compilador siempre que ocurran en expresiones completas separadas ... cierto que volátil es inútil para la seguridad de los subprocesos, pero no por las razones que él da. No es porque el compilador pueda reordenar los accesos a objetos volátiles, sino porque la CPU puede reordenarlos. Las operaciones atómicas y las barreras de memoria evitan que el compilador y la CPU reordenen
A lo que David Schwartz responde en los comentarios :
... No hay diferencia, desde el punto de vista del estándar C ++, entre el compilador haciendo algo y el compilador emitiendo instrucciones que hacen que el hardware haga algo. Si la CPU puede reordenar los accesos a los volátiles, entonces el estándar no requiere que se mantenga su orden. ...
... El estándar C ++ no hace ninguna distinción sobre lo que hace el reordenamiento. Y no se puede argumentar que la CPU puede reordenarlos sin ningún efecto observable, por lo que está bien: el estándar C ++ define su orden como observable. Un compilador cumple con el estándar C ++ en una plataforma si genera código que hace que la plataforma haga lo que requiere el estándar. Si el estándar requiere que los accesos a los volátiles no se reordenen, entonces una plataforma que los reordena no los cumple. ...
Mi punto es que si el estándar C ++ prohíbe al compilador reordenar los accesos a distintos volátiles, basándose en la teoría de que el orden de dichos accesos es parte del comportamiento observable del programa, entonces también requiere que el compilador emita código que prohíba a la CPU hacer entonces. El estándar no diferencia entre lo que hace el compilador y lo que el código de generación del compilador hace que haga la CPU.
Lo que da lugar a dos preguntas: ¿Alguno de ellos es "correcto"? ¿Qué hacen realmente las implementaciones reales?
fuente
volatile
las memorias caché de la CPU optimicen las lecturas de variables. O todos estos compiladores no son conformes o el estándar no significa lo que usted cree que significa. (El estándar no distingue entre lo que hace el compilador y lo que el compilador hace que haga la CPU. El trabajo del compilador es emitir código que, cuando se ejecuta, cumple con el estándar.)Respuestas:
En lugar de explicar qué
volatile
hace, permítame explicarle cuándo debe usarvolatile
.volatile
variable es prácticamente lo único que el estándar le permite hacer desde un manejador de señales. Desde C ++ 11 puede usarlostd::atomic
para ese propósito, pero solo si el atómico no tiene bloqueos.setjmp
acuerdo con Intel .Por ejemplo:
volatile int *foo = some_memory_mapped_device; while (*foo) ; // wait until *foo turns false
Sin el
volatile
especificador, el compilador puede optimizar completamente el bucle. Elvolatile
especificador le dice al compilador que no puede asumir que 2 lecturas posteriores devuelven el mismo valor.Tenga en cuenta que
volatile
no tiene nada que ver con los hilos. El ejemplo anterior no funciona si se ha escrito un hilo diferente*foo
porque no hay ninguna operación de adquisición involucrada.En todos los demás casos, el uso de
volatile
debe considerarse no portátil y ya no debe pasar la revisión del código, excepto cuando se trata de compiladores anteriores a C ++ 11 y extensiones de compilador (como el/volatile:ms
conmutador de msvc , que está habilitado de forma predeterminada en X86 / I64).fuente
setjmp
son las dos garantías que hace el estándar. Por otro lado, la intención , al menos al principio, era admitir IO mapeadas en memoria. Lo que en algunos procesadores puede requerir una valla o un membar.volatile
accesos.No se requiere un compilador de C ++ que cumpla con la especificación para introducir una barrera de memoria. Su compilador particular podría; dirija su pregunta a los autores de su compilador.
La función de "volátil" en C ++ no tiene nada que ver con el subproceso. Recuerde, el propósito de "volatile" es deshabilitar las optimizaciones del compilador para que la lectura de un registro que está cambiando debido a condiciones exógenas no se optimice. ¿Es una dirección de memoria en la que está escribiendo un hilo diferente en una CPU diferente un registro que está cambiando debido a condiciones exógenas? No. Nuevamente, si algunos autores de compiladores han optado por tratar las direcciones de memoria en las que se escriben diferentes subprocesos en diferentes CPU como si fueran registros que cambian debido a condiciones exógenas, ese es su problema; no están obligados a hacerlo. Tampoco son necesarios, incluso si introduce una barrera de memoria, para, por ejemplo, garantizar que cada hilo ve una constante ordenación de lecturas y escrituras volátiles.
De hecho, volátil es bastante inútil para subprocesos en C / C ++. La mejor práctica es evitarlo.
Además: las barreras de memoria son un detalle de implementación de arquitecturas de procesador particulares. En C #, donde volatile está diseñado explícitamente para múltiples subprocesos, la especificación no dice que se introducirán medias vallas, porque el programa podría estar ejecutándose en una arquitectura que no tiene vallas en primer lugar. Más bien, nuevamente, la especificación ofrece ciertas garantías (extremadamente débiles) sobre qué optimizaciones evitará el compilador, el tiempo de ejecución y la CPU para poner ciertas restricciones (extremadamente débiles) sobre cómo se ordenarán algunos efectos secundarios. En la práctica, estas optimizaciones se eliminan mediante el uso de medias vallas, pero ese es un detalle de implementación sujeto a cambios en el futuro.
El hecho de que le importe la semántica de volatile en cualquier idioma en lo que respecta al subproceso múltiple indica que está pensando en compartir memoria entre subprocesos. Considere simplemente no hacer eso. Hace que su programa sea mucho más difícil de comprender y mucho más probable que contenga errores sutiles imposibles de reproducir.
fuente
volatile
se introdujo en el estándar C. Aún así, debido a que el estándar no puede especificar cosas como lo que realmente sucede en un "acceso", dice que "Lo que constituye un acceso a un objeto que tiene un tipo calificado de volátil está definido por la implementación". Demasiadas implementaciones en la actualidad no proporcionan una definición útil de un acceso, lo que en mi humilde opinión viola el espíritu del estándar, incluso si se ajusta a la letra.volatile
La semántica es más fuerte que eso, el compilador tiene que generar cada acceso solicitado (1.9 / 8, 1.9 / 12), no simplemente garantizar que los cambios exógenos sean eventualmente detectados (1.10 / 27). En el mundo de las E / S mapeadas en memoria, una lectura de memoria puede tener una lógica asociada arbitraria, como un captador de propiedades. No optimizaría las llamadas a los captadores de propiedades de acuerdo con las reglas que ha establecidovolatile
, ni el Estándar lo permite.Lo que David está pasando por alto es el hecho de que el estándar C ++ especifica el comportamiento de varios subprocesos que interactúan solo en situaciones específicas y todo lo demás da como resultado un comportamiento indefinido. Una condición de carrera que involucre al menos una escritura no está definida si no usa variables atómicas.
En consecuencia, el compilador está perfectamente en su derecho a renunciar a cualquier instrucción de sincronización ya que su CPU solo notará la diferencia en un programa que exhibe un comportamiento indefinido debido a la falta de sincronización.
fuente
volatile
no tiene nada que ver con hilos; su propósito original era soportar IO mapeado en memoria. Y al menos en algunos procesadores, admitir E / S mapeadas en memoria requeriría vallas. (Los compiladores no hacen esto, pero ese es un problema diferente).volatile
tiene mucho que ver con los subprocesos: sevolatile
ocupa de la memoria a la que se puede acceder sin que el compilador sepa que se puede acceder a ella, y eso cubre muchos usos del mundo real de datos compartidos entre subprocesos en una CPU específica.En primer lugar, los estándares C ++ no garantizan las barreras de memoria necesarias para ordenar correctamente las lecturas / escrituras que no son atómicas. Se recomiendan variables volátiles para usar con MMIO, manejo de señales, etc. En la mayoría de las implementaciones, volatile no es útil para multi-threading y generalmente no se recomienda.
En cuanto a la implementación de accesos volátiles, esta es la elección del compilador.
Este artículo , que describe el comportamiento de gcc , muestra que no puede usar un objeto volátil como barrera de memoria para ordenar una secuencia de escrituras en la memoria volátil.
Con respecto al comportamiento de icc , encontré que esta fuente también dice que volatile no garantiza ordenar accesos a la memoria.
El compilador de Microsoft VS2013 tiene un comportamiento diferente. Esta documentación explica cómo volatile aplica la semántica Release / Acquire y permite que los objetos volátiles se utilicen en bloqueos / liberaciones en aplicaciones multiproceso.
Otro aspecto que debe tenerse en cuenta es que el mismo compilador puede tener un comportamiento diferente wrt. volátil dependiendo de la arquitectura de hardware de destino . Esta publicación sobre el compilador de MSVS 2013 establece claramente los detalles de la compilación con volátiles para plataformas ARM.
Entonces mi respuesta a:
sería: No garantizado, probablemente no, pero algunos compiladores podrían hacerlo. No debe confiar en el hecho de que lo hace.
fuente
volatile
evita que el compilador reordene cargas / almacenes? ¿O está diciendo que el estándar C ++ requiere que lo haga? Y si es lo último, ¿puede responder a mi argumento en sentido contrario citado en la pregunta original?volatile
lvalue. Dado que deja la definición de "acceso" a la implementación, sin embargo, esto no nos compra mucho si a la implementación no le importa.volatile
, pero no hay vallas en el código generado por el compilador en Visual Studios 2012.volatile
es el específicamente enumerado por el estándar. (setjmp
, señales, etc.)El compilador solo inserta una barrera de memoria en la arquitectura Itanium, hasta donde yo sé.
La
volatile
palabra clave se usa mejor para cambios asincrónicos, por ejemplo, manejadores de señales y registros mapeados en memoria; Por lo general, es la herramienta incorrecta para utilizar en la programación multiproceso.fuente
volatile
es extremadamente útil para muchos usos que nunca tratan con hardware. Siempre que desee que la implementación genere un código de CPU que siga de cerca el código C / C ++, utilicevolatile
.Depende de qué compilador sea "el compilador". Visual C ++ lo hace, desde 2005. Pero el estándar no lo requiere, por lo que algunos otros compiladores no lo hacen.
fuente
int volatile i; int main() { return i; }
genera una principal con exactamente dos instrucciones:mov eax, i; ret 0;
.cl /help
dice la versión 18.00.21005.1. El directorio en el que se encuentra esC:\Program Files (x86)\Microsoft Visual Studio 12.0\VC
. El encabezado de la ventana de comandos dice VS 2013. Entonces, con respecto a la versión ... Las únicas opciones que usé fueron/c /O2 /Fa
. (Sin el/O2
, también configura el marco de pila local. Pero todavía no hay instrucciones de valla.)main
, para que el compilador pudiera ver todo el programa y saber que no había otros hilos, o al menos ningún otro acceso a la variable antes que la mía (por lo que no podría haber problemas de caché) podría afectar esto. también, pero de alguna manera, lo dudo.Esto se debe principalmente a la memoria y se basa en versiones anteriores a C ++ 11, sin subprocesos. Pero habiendo participado en las discusiones sobre el hilo en el comité, puedo decir que el comité nunca tuvo la intención de
volatile
pudiera usarse para la sincronización entre subprocesos. Microsoft lo propuso, pero la propuesta no se llevó a cabo.La especificación clave de
volatile
es que el acceso a un volátil representa un "comportamiento observable", al igual que IO. De la misma manera, el compilador no puede reordenar o eliminar E / S específicas, no puede reordenar o eliminar accesos a un objeto volátil (o más correctamente, accesos a través de una expresión lvalue con tipo calificado volátil). La intención original de volatile era, de hecho, soportar IO mapeado en memoria. El "problema" con esto, sin embargo, es que es la implementación definida lo que constituye un "acceso volátil". Y muchos compiladores lo implementan como si la definición fuera "se ha ejecutado una instrucción que lee o escribe en la memoria". Cuál es una definición legal, aunque inútil, si la implementación lo especifica. (Todavía tengo que encontrar la especificación real de ningún compilador.Podría decirse (y es un argumento que acepto), esto viola la intención del estándar, ya que a menos que el hardware reconozca las direcciones como E / S mapeadas en memoria e inhiba cualquier reordenación, etc., ni siquiera puede usar volatile para E / S mapeadas en memoria, al menos en arquitecturas Sparc o Intel. Sin embargo, ninguno de los compiladores que he visto (Sun CC, g ++ y MSC) genera instrucciones de cerca o membar. (Para el momento en que Microsoft propuso extender las reglas
volatile
, creo que algunos de sus compiladores implementaron su propuesta y emitieron instrucciones de cerca para accesos volátiles. No he verificado qué hacen los compiladores recientes, pero no me sorprendería si dependiera en alguna opción del compilador. Sin embargo, la versión que verifiqué (creo que fue VS6.0) no emitió vallas).fuente
volatile
semántica es perfectamente adecuada. Generalmente, estos periféricos informan que sus áreas de memoria no se pueden almacenar en caché, lo que ayuda a reordenar a nivel de hardware.ASI_REAL_IO
parte del espacio de direcciones, creo que debería estar bien. (Altera NIOS usa una técnica similar, con altos bits de la dirección que controlan el bypass de MMU; estoy seguro de que también hay otros)No es necesario. Volátil no es una primitiva de sincronización. Simplemente deshabilita las optimizaciones, es decir, obtiene una secuencia predecible de lecturas y escrituras dentro de un hilo en el mismo orden que prescribe la máquina abstracta. Pero las lecturas y escrituras en diferentes hilos no tienen orden en primer lugar, no tiene sentido hablar de preservar o no preservar su orden. El orden entre los cabezales se puede establecer mediante primitivas de sincronización, se obtiene UB sin ellas.
Un poco de explicación sobre las barreras de la memoria. Una CPU típica tiene varios niveles de acceso a la memoria. Hay una canalización de memoria, varios niveles de caché, luego RAM, etc.
Las instrucciones de Membar descargan la tubería. No cambian el orden en el que se ejecutan las lecturas y escrituras, solo obliga a que las pendientes se ejecuten en un momento dado. Es útil para programas multiproceso, pero no mucho más.
La (s) caché (s) normalmente son coherentes automáticamente entre las CPU. Si uno quiere asegurarse de que la caché esté sincronizada con la RAM, se necesita vaciar la caché. Es muy diferente a un membar.
fuente
volatile
solo deshabilita las optimizaciones del compilador? Eso no tiene ningún sentido. Cualquier optimización que pueda hacer el compilador puede, al menos en principio, ser igualmente bien realizada por la CPU. Entonces, si el estándar dice que solo deshabilita las optimizaciones del compilador, eso significaría que no proporcionaría ningún comportamiento en el que uno pueda confiar en el código portátil. Pero obviamente eso no es cierto porque el código portátil puede depender de su comportamiento con respecto a lassetjmp
señales.El compilador necesita introducir una barrera de memoria alrededor de los
volatile
accesos si, y solo si, es necesario para que los usosvolatile
especificados en el estándar funcionen (setjmp
, manejadores de señales, etc.) en esa plataforma en particular.Tenga en cuenta que algunos compiladores van mucho más allá de lo que requiere el estándar C ++ para ser
volatile
más potentes o útiles en esas plataformas. El código portátil no debería dependervolatile
de hacer nada más allá de lo que se especifica en el estándar C ++.fuente
Siempre uso volátil en rutinas de servicio de interrupción, por ejemplo, el ISR (a menudo código ensamblador) modifica alguna ubicación de memoria y el código de nivel superior que se ejecuta fuera del contexto de interrupción accede a la ubicación de memoria a través de un puntero a volátil.
Hago esto para RAM así como para IO mapeado en memoria.
Según la discusión aquí, parece que este sigue siendo un uso válido de volatile pero no tiene nada que ver con múltiples subprocesos o CPU. Si el compilador de un microcontrolador "sabe" que no puede haber ningún otro acceso (por ejemplo, todo está en el chip, no hay caché y solo hay un núcleo), pensaría que una barrera de memoria no está implícita en absoluto, el compilador solo necesita evitar ciertas optimizaciones.
A medida que acumulamos más cosas en el "sistema" que ejecuta el código objeto, casi todas las apuestas se cancelan, al menos así es como leo esta discusión. ¿Cómo podría un compilador cubrir todas las bases?
fuente
Creo que la confusión en torno al reordenamiento de instrucciones y volátiles proviene de las 2 nociones de reordenamiento que hacen las CPU:
Volátil afecta la forma en que un compilador genera el código asumiendo una ejecución de un solo subproceso (esto incluye interrupciones). No implica nada sobre las instrucciones de la barrera de la memoria, sino que impide que un compilador realice ciertos tipos de optimizaciones relacionadas con los accesos a la memoria.
Un ejemplo típico es recuperar un valor de la memoria, en lugar de usar uno almacenado en caché en un registro.
Ejecución fuera de orden
Las CPU pueden ejecutar instrucciones fuera de orden / especulativamente siempre que el resultado final pudiera haber ocurrido en el código original. Las CPU pueden realizar transformaciones que no están permitidas en los compiladores porque los compiladores solo pueden realizar transformaciones que sean correctas en todas las circunstancias. Por el contrario, las CPU pueden comprobar la validez de estas optimizaciones y retirarse de ellas si resultan ser incorrectas.
Secuencia de lectura / escritura de memoria vista por otras CPU
El resultado final de una secuencia de instrucción, el orden efectivo, debe coincidir con la semántica del código generado por un compilador. Sin embargo, el orden de ejecución real elegido por la CPU puede ser diferente. El orden efectivo como se ve en otras CPU (cada CPU puede tener una vista diferente) puede verse limitado por barreras de memoria.
No estoy seguro de cuánto puede diferir el orden efectivo y el real porque no sé hasta qué punto las barreras de memoria pueden impedir que las CPU realicen una ejecución fuera de orden.
Fuentes:
fuente
Mientras trabajaba en un video tutorial descargable en línea para el desarrollo de gráficos 3D y motores de juegos trabajando con OpenGL moderno. Usamos
volatile
dentro de una de nuestras clases. El sitio web del tutorial se puede encontrar aquí y el video que trabaja con lavolatile
palabra clave se encuentra en elShader Engine
video de la serie 98. Estos trabajos no son míos, pero están acreditadosMarek A. Krzeminski, MASc
y este es un extracto de la página de descarga del video.Y si está suscrito a su sitio web y tiene acceso a sus videos dentro de este video, hace referencia a este artículo sobre el uso de
Volatile
conmultithreading
programación.Aquí está el artículo del enlace anterior: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766
Este artículo puede estar un poco anticuado, pero da una buena idea de un uso excelente del uso del modificador volátil en el uso de la programación multiproceso para ayudar a mantener los eventos asíncronos mientras el compilador verifica las condiciones de carrera por nosotros. Es posible que esto no responda directamente a la pregunta original de los OP sobre la creación de una barrera de memoria, pero elijo publicar esto como una respuesta para otros como una excelente referencia para un buen uso de volátiles cuando se trabaja con aplicaciones multiproceso.
fuente
volatile
Básicamente, la palabra clave significa que la lectura y escritura de un objeto debe realizarse exactamente como lo escribe el programa y no debe optimizarse de ninguna manera . El código binario debe seguir el código C o C ++: una carga donde se lee, una tienda donde hay una escritura.También significa que no se debe esperar que ninguna lectura dé como resultado un valor predecible: el compilador no debe asumir nada sobre una lectura, incluso inmediatamente después de una escritura en el mismo objeto volátil:
volatile int i; i = 1; int j = i; if (j == 1) // not assumed to be true
volatile
puede ser la herramienta más importante en la caja de herramientas "C es un lenguaje ensamblador de alto nivel" .Si declarar un objeto volátil es suficiente para garantizar el comportamiento del código que se ocupa de los cambios asincrónicos depende de la plataforma: diferentes CPU dan diferentes niveles de sincronización garantizada para lecturas y escrituras de memoria normales. Probablemente no debería intentar escribir código de subprocesos múltiples de tan bajo nivel a menos que sea un experto en el área.
Las primitivas atómicas proporcionan una buena vista de los objetos de alto nivel para el subproceso múltiple que facilita el razonamiento sobre el código. Casi todos los programadores deberían usar primitivas atómicas o primitivas que proporcionen exclusiones mutuas como mutex, bloqueos de lectura-escritura, semáforos u otras primitivas de bloqueo.
fuente