Después de mirar un montón de otras preguntas y sus respuestas , tengo la impresión de que no hay un acuerdo generalizado sobre lo que significa exactamente la palabra clave "volátil" en C
Incluso el estándar en sí no parece ser lo suficientemente claro como para que todos estén de acuerdo en lo que significa .
Entre otros problemas:
- Parece proporcionar diferentes garantías dependiendo de su hardware y de su compilador.
- Afecta las optimizaciones del compilador pero no las optimizaciones de hardware, por lo que en un procesador avanzado que realiza sus propias optimizaciones de tiempo de ejecución, ni siquiera está claro si el compilador puede evitar cualquier optimización que desee evitar. (Algunos compiladores generan instrucciones para evitar algunas optimizaciones de hardware en algunos sistemas, pero esto no parece estar estandarizado de ninguna manera).
Para resumir el problema, parece (después de leer mucho) que "volátil" garantiza algo como: El valor se leerá / escribirá no solo desde / a un registro, sino al menos a la caché L1 del núcleo, en el mismo orden que las lecturas / escrituras aparecen en el código. Pero esto parece inútil, ya que leer / escribir desde / a un registro ya es suficiente dentro del mismo hilo, mientras que coordinar con la caché L1 no garantiza nada más con respecto a la coordinación con otros hilos. No puedo imaginar cuándo podría ser importante sincronizar solo con caché L1.
USO 1
El único uso ampliamente aceptado de volátil parece ser para sistemas antiguos o integrados donde ciertas ubicaciones de memoria están asignadas por hardware a funciones de E / S, como un bit en la memoria que controla (directamente, en el hardware) una luz , o un bit en la memoria que le indica si una tecla del teclado está presionada o no (porque está conectada directamente por el hardware a la tecla).
Parece que el "uso 1" no ocurre en el código portátil cuyos objetivos incluyen sistemas multi-core.
USE 2
No es muy diferente de "use 1" es la memoria que un manejador de interrupciones puede leer o escribir en cualquier momento (que puede controlar una luz o almacenar información de una tecla). Pero ya para esto tenemos el problema de que, dependiendo del sistema, el controlador de interrupciones podría ejecutarse en un núcleo diferente con su propia memoria caché , y "volátil" no garantiza la coherencia de la memoria caché en todos los sistemas.
Así que "usar 2" parece estar más allá de lo que puede ofrecer "volátil".
USO 3
El único otro uso indiscutible que veo es evitar la optimización incorrecta de los accesos a través de diferentes variables que apuntan a la misma memoria que el compilador no se da cuenta de que es la misma memoria. Pero esto probablemente solo sea indiscutible porque la gente no está hablando de eso, solo vi una mención al respecto. Y pensé que el estándar C ya reconocía que los punteros "diferentes" (como diferentes argumentos para una función) podrían apuntar al mismo elemento o elementos cercanos, y ya especifiqué que el compilador debe producir código que funcione incluso en tales casos. Sin embargo, no pude encontrar rápidamente este tema en el último estándar (¡500 páginas!).
¿Entonces "usar 3" tal vez no existe ?
De ahí mi pregunta:
¿"Volátil" garantiza algo en absoluto en el código C portátil para sistemas de múltiples núcleos?
EDITAR - actualizar
Después de examinar el último estándar , parece que la respuesta es al menos un sí muy limitado:
1. El estándar especifica repetidamente un tratamiento especial para el tipo específico "volatile sig_atomic_t". Sin embargo, el estándar también dice que el uso de la función de señal en un programa multiproceso da como resultado un comportamiento indefinido. Por lo tanto, este caso de uso parece limitado a la comunicación entre un programa de subproceso único y su controlador de señal.
2. La norma también especifica un significado claro para "volátil" en relación con setjmp / longjmp. (El código de ejemplo donde importa se da en otras preguntas y respuestas ).
Entonces, la pregunta más precisa es:
¿"volátil" garantiza algo en absoluto en el código C portátil para sistemas de múltiples núcleos, aparte de (1) permitir que un programa de un solo subproceso reciba información de su controlador de señal, o (2) permitir setjmp código para ver variables modificadas entre setjmp y longjmp?
Esta sigue siendo una pregunta de sí / no.
En caso afirmativo, sería genial si pudiera mostrar un ejemplo de código portátil sin errores que se vuelve defectuoso si se omite "volátil". Si "no", entonces supongo que un compilador es libre de ignorar "volátil" fuera de estos dos casos muy específicos, para objetivos de múltiples núcleos.
volatile
para informar al programa que puede cambiar de forma asincrónica.volatile
específicamente, lo cual creo que es necesario.Respuestas:
No, absolutamente no . Y eso hace que los volátiles sean casi inútiles para el propósito del código seguro de MT.
Si lo hiciera, entonces volátil sería bastante bueno para las variables compartidas por múltiples subprocesos, ya que ordenar los eventos en la caché L1 es todo lo que necesita hacer en una CPU típica (que es multi-core o multi-CPU en la placa base) capaz de cooperar de una manera que hace posible una implementación normal de subprocesos múltiples C / C ++ o Java con los costos esperados típicos (es decir, no es un costo enorme en la mayoría de las operaciones mutex atómicas o no satisfechas).
Pero volátil no proporciona ningún orden garantizado (o "visibilidad de memoria") en el caché, ya sea en teoría o en la práctica.
(Nota: lo siguiente se basa en una interpretación sólida de los documentos estándar, la intención del estándar, la práctica histórica y una comprensión profunda de las expectativas de los escritores compiladores. Este enfoque se basa en la historia, las prácticas reales y las expectativas y la comprensión de personas reales en el mundo real, que es mucho más fuerte y más confiable que analizar las palabras de un documento que no se conoce como escritura de especificaciones estelares y que ha sido revisado muchas veces).
En la práctica, volatile garantiza la capacidad de rastreo que es la capacidad de usar información de depuración para el programa en ejecución, en cualquier nivel de optimización , y el hecho de que la información de depuración tiene sentido para estos objetos volátiles:
ptrace
(un mecanismo similar a un trazado) para establecer puntos de ruptura significativos en los puntos de secuencia después de operaciones que involucran objetos volátiles: realmente puede romper exactamente en estos puntos (tenga en cuenta que esto solo funciona si está dispuesto a establecer muchos puntos de ruptura como cualquiera) La declaración C / C ++ puede compilarse en muchos puntos de inicio y finalización de ensamblaje diferentes, como en un bucle masivamente desenrollado);La garantía volátil en la práctica es un poco más que la interpretación estricta de ptrace: también garantiza que las variables automáticas volátiles tengan una dirección en la pila, ya que no están asignadas a un registro, una asignación de registro que haría las manipulaciones de ptrace más delicadas (el compilador puede muestra información de depuración para explicar cómo se asignan las variables a los registros, pero leer y cambiar el estado del registro es un poco más complicado que acceder a las direcciones de memoria).
Tenga en cuenta que la capacidad de depuración completa del programa, que está considerando todas las variables volátiles al menos en los puntos de secuencia, es proporcionada por el modo de "optimización cero" del compilador, un modo que todavía realiza optimizaciones triviales como simplificaciones aritméticas (generalmente no hay garantía de no optimización en todos los modos). Pero volátil es más fuerte que la no optimización:
x-x
se puede simplificar para un entero no volátilx
pero no para un objeto volátil.Por lo tanto, volátil significa que se garantiza que se compilará tal cual , como la traducción del origen al binario / ensamblado por parte del compilador de una llamada al sistema no es una reinterpretación, cambio u optimización de ninguna manera por parte de un compilador. Tenga en cuenta que las llamadas a la biblioteca pueden o no ser llamadas al sistema. Muchas funciones oficiales del sistema son en realidad funciones de biblioteca que ofrecen una capa delgada de interposición y generalmente difieren al núcleo al final. (En particular
getpid
, no necesita ir al kernel y podría leer una ubicación de memoria proporcionada por el sistema operativo que contiene la información).Las interacciones volátiles son interacciones con el mundo exterior de la máquina real , que debe seguir a la "máquina abstracta". No son interacciones internas de partes del programa con otras partes del programa. El compilador solo puede razonar sobre lo que sabe, es decir, las partes internas del programa.
La generación de código para un acceso volátil debería seguir la interacción más natural con esa ubicación de memoria: no debería ser sorprendente. Eso significa que se espera que algunos accesos volátiles sean atómicos : si la forma natural de leer o escribir la representación de a
long
en la arquitectura es atómica, entonces se espera que una lectura o escritura de avolatile long
sea atómica, ya que el compilador no debe generar código tonto e ineficiente para acceder a objetos volátiles byte por byte, por ejemplo .Debería poder determinar eso conociendo la arquitectura. No tiene que saber nada sobre el compilador, ya que volátil significa que el compilador debe ser transparente .
Pero lo volátil no hace más que forzar la emisión del ensamblaje esperado para los menos optimizados para casos particulares para realizar una operación de memoria: la semántica volátil significa semántica de caso general.
El caso general es lo que hace el compilador cuando no tiene ninguna información sobre una construcción: f.ex. llamar a una función virtual en un valor de l mediante un despacho dinámico es un caso general, hacer una llamada directa al anulador después de determinar en tiempo de compilación que el tipo de objeto designado por la expresión es un caso particular. El compilador siempre tiene un manejo de caso general de todas las construcciones, y sigue el ABI.
Volátil no hace nada especial para sincronizar subprocesos o proporcionar "visibilidad de memoria": volátil solo proporciona garantías en el nivel abstracto visto desde el interior de un subproceso en ejecución o detenido, es decir, dentro del núcleo de una CPU :
Solo el segundo punto significa que volátil no es útil en la mayoría de los problemas de comunicación entre subprocesos; El primer punto es esencialmente irrelevante en cualquier problema de programación que no implique comunicación con componentes de hardware fuera de la (s) CPU (s) pero aún en el bus de memoria.
La propiedad de volátil que proporciona un comportamiento garantizado desde el punto de vista del núcleo que ejecuta el subproceso significa que las señales asincrónicas entregadas a ese subproceso, que se ejecutan desde el punto de vista del orden de ejecución de ese subproceso, ver operaciones en orden de código fuente .
A menos que planee enviar señales a sus subprocesos (un enfoque extremadamente útil para la consolidación de la información sobre los subprocesos que se ejecutan actualmente sin un punto de detención previamente acordado), la volatilidad no es para usted.
fuente
No soy un experto, pero cppreference.com tiene lo que me parece una información
volatile
bastante buena . Aquí está la esencia de esto:También le da algunos usos:
Y, por supuesto, menciona que
volatile
no es útil para la sincronización de subprocesos:fuente
longjmp
en código C ++.En primer lugar, históricamente ha habido varios problemas con respecto a las diferentes interpretaciones del significado del
volatile
acceso y similares. Vea este estudio: Los volátiles están mal compilados y qué hacer al respecto .Además de los diversos problemas mencionados en ese estudio, el comportamiento de
volatile
es portátil, salvo por un aspecto de ellos: cuando actúan como barreras de memoria . Una barrera de memoria es un mecanismo que existe para evitar la ejecución simultánea no secuenciada de su código. Usarlovolatile
como una barrera de memoria ciertamente no es portátil.Si el lenguaje C garantiza o no el comportamiento de la memoria
volatile
es aparentemente discutible, aunque personalmente creo que el lenguaje es claro. Primero tenemos la definición formal de los efectos secundarios, C17 5.1.2.3:El estándar define el término secuenciación, como una forma de determinar el orden de evaluación (ejecución). La definición es formal y engorrosa:
El TL; DR de lo anterior es básicamente que en caso de que tengamos una expresión
A
que contenga efectos secundarios, debe ejecutarse antes de otra expresiónB
, en caso de queB
se secuencia despuésA
.Las optimizaciones del código C son posibles gracias a esta parte:
Esto significa que el programa puede evaluar (ejecutar) expresiones en el orden que el estándar exige en otro lugar (orden de evaluación, etc.). Pero no necesita evaluar (ejecutar) un valor si puede deducir que no se usa. Por ejemplo, la operación
0 * x
no necesita evaluarx
y simplemente reemplazar la expresión con0
.A menos que acceder a una variable sea un efecto secundario. Lo que significa que en el caso
x
esvolatile
, que debe evaluar (ejecutar)0 * x
no se permite a pesar de que el resultado siempre será 0. La optimización.Además, el estándar habla de comportamiento observable:
Dado todo lo anterior, una implementación conforme (compilador + sistema subyacente) puede no ejecutar el acceso de
volatile
objetos en un orden no secuenciado, en caso de que la semántica de la fuente C escrita indique lo contrario.Esto significa que en este ejemplo
Ambas expresiones de asignación deben evaluarse y
z = x;
deben evaluarse antesz = y;
. ¡Una implementación multiprocesador que externaliza estas dos operaciones a dos núcleos de secuencias diferentes no es conforme!El dilema es que los compiladores no pueden hacer mucho sobre cosas como el almacenamiento previo en caché y la canalización de instrucciones, etc., particularmente cuando no se ejecutan sobre un sistema operativo. Y entonces los compiladores entregan ese problema a los programadores, diciéndoles que las barreras de memoria ahora son responsabilidad del programador. Si bien el estándar C establece claramente que el compilador debe resolver el problema.
Sin embargo, al compilador no necesariamente le importa resolver el problema,
volatile
por lo que, en aras de actuar como una barrera de memoria, no es portátil. Se ha convertido en un problema de calidad de implementación.fuente
z
se ejecutan realmente? (comoz = x; z = y;
) El valor se borrará en la siguiente declaración.z
realmente se le asigna dos veces? ¿Cómo sabes que "las lecturas se ejecutan"?