¿"Volátil" garantiza algo en absoluto en el código C portátil para sistemas de múltiples núcleos?

12

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:

  1. Parece proporcionar diferentes garantías dependiendo de su hardware y de su compilador.
  2. 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.

Mate
fuente
3
Las señales existen en C portátil; ¿Qué pasa con una variable global que es actualizada por un controlador de señal? Esto debería ser volatilepara informar al programa que puede cambiar de forma asincrónica.
Nate Eldredge el
2
@NateEldredge Global, aunque solo es volátil, no es lo suficientemente bueno. También necesita ser atómico.
Eugene Sh.
@ EugeneSh .: Sí, por supuesto. Pero la pregunta en cuestión es volatileespecíficamente, lo cual creo que es necesario.
Nate Eldredge el
" mientras que coordinar con caché L1 no garantiza nada más con respecto a la coordinación con otros subprocesos " ¿Dónde "coordinar con caché L1" no es suficiente para comunicarse con otros subprocesos?
curioso
1
Tal vez sea relevante, propuesta de C ++ para desaprobar la volatilidad , la propuesta aborda muchas de las inquietudes que plantea aquí, y tal vez su resultado será influyente para el comité C
MM

Respuestas:

1

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 .

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:

  • puede usar 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);
  • mientras se detiene un subproceso de ejecución, puede leer el valor de todos los objetos volátiles, ya que tienen su representación canónica (siguiendo la ABI para su tipo respectivo); una variable local no volátil podría tener una representación atípica, f.ex. una representación desplazada: una variable utilizada para indexar una matriz podría multiplicarse por el tamaño de los objetos individuales, para facilitar la indexación; o puede ser reemplazado por un puntero a un elemento de matriz (siempre que todos los usos de la variable se conviertan de manera similar) (piense en cambiar dx a du en una integral);
  • también puede modificar esos objetos (siempre que las asignaciones de memoria lo permitan, ya que un objeto volátil con una vida útil estática que está calificado puede estar en un rango de memoria asignado de solo lectura).

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-xse puede simplificar para un entero no volátil xpero 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 longen la arquitectura es atómica, entonces se espera que una lectura o escritura de a volatile longsea ​​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 :

  • volatile no dice nada acerca de qué operaciones de memoria alcanzan la RAM principal (puede configurar tipos de memoria caché específicos con instrucciones de ensamblaje o llamadas al sistema para obtener estas garantías);
  • volatile no proporciona ninguna garantía sobre cuándo las operaciones de memoria se comprometerán con cualquier nivel de caché (ni siquiera L1) .

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.

curioso
fuente
6

No soy un experto, pero cppreference.com tiene lo que me parece una informaciónvolatile bastante buena . Aquí está la esencia de esto:

Cada acceso (tanto de lectura como de escritura) realizado a través de una expresión de valor de tipo calificado volátil se considera un efecto secundario observable para fines de optimización y se evalúa estrictamente de acuerdo con las reglas de la máquina abstracta (es decir, todas las escrituras se completan en algún tiempo antes del siguiente punto de secuencia). Esto significa que dentro de un solo hilo de ejecución, un acceso volátil no puede optimizarse ni reordenarse en relación con otro efecto secundario visible que está separado por un punto de secuencia del acceso volátil.

También le da algunos usos:

Usos de volátiles

1) los objetos volátiles estáticos modelan puertos de E / S mapeados en memoria, y los objetos volátiles constantes estáticos modelan puertos de entrada mapeados en memoria, como un reloj en tiempo real

2) los objetos volátiles estáticos de tipo sig_atomic_t se utilizan para la comunicación con los manejadores de señales.

3) las variables volátiles que son locales a una función que contiene una invocación de la macro setjmp son las únicas variables locales garantizadas para retener sus valores después de que devuelve longjmp.

4) Además, las variables volátiles se pueden usar para deshabilitar ciertas formas de optimización, por ejemplo, para deshabilitar la eliminación del almacén muerto o el plegado constante para microbenchmarks.

Y, por supuesto, menciona que volatileno es útil para la sincronización de subprocesos:

Tenga en cuenta que las variables volátiles no son adecuadas para la comunicación entre hilos; no ofrecen atomicidad, sincronización u ordenamiento de memoria. Una lectura de una variable volátil que es modificada por otro hilo sin sincronización o modificación concurrente de dos hilos no sincronizados es un comportamiento indefinido debido a una carrera de datos.

Fred Larson
fuente
2
En particular, (2) y (3) son relevantes para el código portátil.
Nate Eldredge el
2
@TED ​​A pesar del nombre de dominio, el enlace es a información sobre C, no C ++
David Brown
@NateEldredge Raramente se puede usar longjmpen código C ++.
curioso
@DavidBrown C y C ++ tienen la misma definición de un SE observable, y esencialmente las mismas primitivas de subproceso.
curioso
4

En primer lugar, históricamente ha habido varios problemas con respecto a las diferentes interpretaciones del significado del volatileacceso 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 volatilees 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. Usarlo volatilecomo una barrera de memoria ciertamente no es portátil.

Si el lenguaje C garantiza o no el comportamiento de la memoria volatilees 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:

Acceder a un volatileobjeto, modificar un objeto, modificar un archivo o llamar a una función que realiza cualquiera de esas operaciones son todos efectos secundarios , que son cambios en el estado del entorno de ejecución.

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:

Secuenciado anteriormente es una relación asimétrica, transitiva, en pares, entre evaluaciones ejecutadas por un solo hilo, lo que induce un orden parcial entre esas evaluaciones. Dadas cualesquiera dos evaluaciones A y B, si A se secuencia antes de B, entonces la ejecución de A precederá a la ejecución de B. (Por el contrario, si A se secuencia antes de B, entonces B se secuencia después de A.) Si A no se secuencia antes o después de B, entonces A y B no están secuenciados . Las evaluaciones A y B se secuencian de forma indeterminada cuando A se secuencia antes o después de B, pero no se especifica cuál. 13) La presencia de un punto de secuencia entre la evaluación de las expresiones A y B implica que cada cálculo de valor y efecto secundario asociado con A se secuencia antes de cada cálculo de valor y efecto secundario asociado con B. (En el anexo C se proporciona un resumen de los puntos de secuencia).

El TL; DR de lo anterior es básicamente que en caso de que tengamos una expresión Aque contenga efectos secundarios, debe ejecutarse antes de otra expresión B, en caso de que Bse secuencia después A.

Las optimizaciones del código C son posibles gracias a esta parte:

En la máquina abstracta, todas las expresiones se evalúan según lo especificado por la semántica. Una implementación real no necesita evaluar parte de una expresión si puede deducir que su valor no se usa y que no se producen los efectos secundarios necesarios (incluidos los causados ​​por llamar a una función o acceder a un objeto volátil).

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 * xno necesita evaluar xy simplemente reemplazar la expresión con 0.

A menos que acceder a una variable sea un efecto secundario. Lo que significa que en el caso xes volatile, que debe evaluar (ejecutar) 0 * xno se permite a pesar de que el resultado siempre será 0. La optimización.

Además, el estándar habla de comportamiento observable:

Los requisitos mínimos en una implementación conforme son:

  • Los accesos a objetos volátiles se evalúan estrictamente de acuerdo con las reglas de la máquina abstracta.
    / - / Este es el comportamiento observable del programa.

Dado todo lo anterior, una implementación conforme (compilador + sistema subyacente) puede no ejecutar el acceso de volatileobjetos 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

volatile int x;
volatile int y;
z = x;
z = y;

Ambas expresiones de asignación deben evaluarse y z = x; deben evaluarse antes z = 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, volatilepor 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.

Lundin
fuente
@curiousguy no importa.
Lundin
@curiousguy No importa, siempre y cuando sea algún tipo de tipo entero con o sin calificadores.
Lundin el
Si se trata de un entero simple no volátil, ¿por qué las escrituras redundantes zse ejecutan realmente? (como z = x; z = y;) El valor se borrará en la siguiente declaración.
curioso
@curiousguy Debido a que las lecturas de las variables volátiles deben ejecutarse sin importar, en la secuencia especificada.
Lundin el
Entonces, ¿ zrealmente se le asigna dos veces? ¿Cómo sabes que "las lecturas se ejecutan"?
curioso