¿Por qué se necesita volátil en C?


Respuestas:


425

Volátil le dice al compilador que no optimice nada que tenga que ver con la variable volátil.

Hay al menos tres razones comunes para usarlo, todas involucrando situaciones en las que el valor de la variable puede cambiar sin acción del código visible: cuando interactúa con hardware que cambia el valor en sí mismo; cuando hay otro hilo en ejecución que también usa la variable; o cuando hay un controlador de señal que puede cambiar el valor de la variable.

Digamos que tiene una pequeña pieza de hardware que está asignada a la RAM en algún lugar y que tiene dos direcciones: un puerto de comando y un puerto de datos:

typedef struct
{
  int command;
  int data;
  int isbusy;
} MyHardwareGadget;

Ahora quieres enviar algún comando:

void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
  // wait while the gadget is busy:
  while (gadget->isbusy)
  {
    // do nothing here.
  }
  // set data first:
  gadget->data    = data;
  // writing the command starts the action:
  gadget->command = command;
}

Parece fácil, pero puede fallar porque el compilador es libre de cambiar el orden en que se escriben los datos y los comandos. Esto haría que nuestro pequeño gadget emita comandos con el valor de datos anterior. También eche un vistazo al ciclo de espera mientras está ocupado. Ese será optimizado. El compilador intentará ser inteligente, leerá el valor de isbusy solo una vez y luego entrará en un bucle infinito. Eso no es lo que quieres.

La forma de evitar esto es declarar el dispositivo puntero como volátil. De esta manera, el compilador se ve obligado a hacer lo que escribió. No puede eliminar las asignaciones de memoria, no puede almacenar en caché las variables en los registros y tampoco puede cambiar el orden de las asignaciones:

Esta es la versión correcta:

   void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

46
Personalmente, prefiero que el tamaño entero sea explícito, por ejemplo, int8 / int16 / int32 cuando hablo con el hardware. Buena respuesta, sin embargo;)
tonylo

22
Sí, debe declarar las cosas con un tamaño de registro fijo, pero bueno, es solo un ejemplo.
Nils Pipenbrinck

69
También se necesita volátil en el código de subprocesos cuando se juega con datos que no están protegidos por concurrencia. Y sí, hay momentos válidos para hacerlo, por ejemplo, puede escribir una cola de mensajes circular segura para subprocesos sin necesidad de protección de concurrencia explícita, pero necesitará volátiles.
Gordon Wrigley

14
Lea la especificación C más difícil. Volátil solo tiene un comportamiento definido en la E / S del dispositivo mapeado en memoria o en la memoria tocada por una función de interrupción asincrónica. No dice nada acerca de subprocesos, y un compilador que optimiza el acceso a la memoria tocado por múltiples subprocesos es conforme.
Ephemient

17
@tolomea: completamente equivocado. triste 17 personas no lo saben. volátil no es una valla de memoria. solo está relacionado con evitar la elisión de código durante la optimización basada en la suposición de efectos secundarios no visibles .
v.oddou

188

volatileen C realmente surgió con el propósito de no almacenar en caché los valores de la variable automáticamente. Le indicará al compilador que no almacene en caché el valor de esta variable. Por lo tanto, generará código para tomar el valor de la volatilevariable dada de la memoria principal cada vez que la encuentre. Este mecanismo se utiliza porque el sistema operativo o cualquier interrupción pueden modificar el valor en cualquier momento. Por lo tanto, usar volatilenos ayudará a acceder al valor nuevamente cada vez.


¿Llego a existir? ¿No fue 'volátil' originalmente tomado de C ++? Bueno, parece que recuerdo ...
syntaxerror

Esto no es volátil: también prohíbe algunos reordenamientos si se especifica como volátil ..
FaceBro

44
@FaceBro: El propósito de volatileera hacer posible que los compiladores optimizaran el código mientras permitían a los programadores lograr la semántica que se lograría sin tales optimizaciones. Los autores del Estándar esperaban que las implementaciones de calidad admitirían cualquier semántica que fuera útil, dadas sus plataformas de destino y campos de aplicación, y no esperaban que los escritores de compiladores buscaran ofrecer la semántica de menor calidad que se ajustara al Estándar y no fueran 100% estúpido (tenga en cuenta que los autores de la Norma reconocen explícitamente en la justificación ...
supercat

1
... que es posible que una implementación se ajuste sin ser de la calidad suficiente para ser adecuada para cualquier propósito, pero no pensaron que fuera necesario para evitarlo).
supercat

1
@syntaxerror, ¿cómo se puede tomar prestado de C ++ cuando C era más de una década mayor que C ++ (tanto en las primeras versiones como en los primeros estándares)?
phuclv

178

Otro uso para volatilees manejadores de señal. Si tienes un código como este:

int quit = 0;
while (!quit)
{
    /* very small loop which is completely visible to the compiler */
}

El compilador puede notar que el cuerpo del bucle no toca la quitvariable y convierte el bucle en un while (true)bucle. Incluso si la quitvariable se establece en el controlador de señal para SIGINTy SIGTERM; el compilador no tiene forma de saber eso.

Sin embargo, si quitse declara la variable volatile, el compilador se ve obligado a cargarla cada vez, ya que puede modificarse en otro lugar. Esto es exactamente lo que quieres en esta situación.


cuando dice "el compilador se ve obligado a cargarlo cada vez, es como cuando el compilador decide optimizar una determinada variable y no declaramos que la variable sea volátil, en el tiempo de ejecución esa determinada variable se carga en los registros de la CPU que no están en la memoria ?
Amit Singh Tomar

1
@AmitSinghTomar Significa lo que dice: cada vez que el código verifica el valor, se vuelve a cargar. De lo contrario, el compilador puede asumir que las funciones que no toman una referencia a la variable no pueden modificarla, por lo que suponiendo que CesarB pretendiera que el bucle anterior no se establezca quit, el compilador puede optimizarlo en un bucle constante, suponiendo que no hay forma de quitcambiar entre iteraciones. NB: Esto no es necesariamente un buen sustituto para la programación real segura para subprocesos.
underscore_d

si salir es una variable global, entonces el compilador no optimizará el ciclo while, ¿correcto?
Pierre G.

2
@PierreG. No, el compilador siempre puede suponer que el código es de un solo subproceso, a menos que se le indique lo contrario. Es decir, en ausencia de volatileotros marcadores, se supondrá que nada fuera del ciclo modifica esa variable una vez que ingresa al ciclo, incluso si es una variable global.
CesarB

1
@PierreG. Si, por ejemplo, tratar de compilar extern int global; void fn(void) { while (global != 0) { } }con gcc -O3 -Sy mirar el archivo de conjunto resultante, en mi máquina lo hace movl global(%rip), %eax; testl %eax, %eax; je .L1; .L4: jmp .L4, es decir, un bucle infinito si el global no es cero. Luego intenta agregar volatiley ver la diferencia.
CesarB

60

volatilele dice al compilador que su variable puede cambiarse por otros medios, además del código que está accediendo a ella. por ejemplo, puede ser una ubicación de memoria asignada de E / S. Si esto no se especifica en tales casos, se pueden optimizar algunos accesos variables, por ejemplo, su contenido se puede mantener en un registro y la ubicación de la memoria no se puede volver a leer.


30

Vea este artículo de Andrei Alexandrescu, " volátil: el mejor amigo del programador multiproceso "

La palabra clave volátil se diseñó para evitar optimizaciones del compilador que podrían hacer que el código sea incorrecto en presencia de ciertos eventos asincrónicos. Por ejemplo, si declara una variable primitiva como volátil , el compilador no puede almacenarla en caché en un registro, una optimización común que sería desastrosa si esa variable se compartiera entre varios subprocesos. Entonces, la regla general es que si tiene variables de tipo primitivo que deben compartirse entre varios subprocesos, declare que esas variables son volátiles. Pero en realidad puede hacer mucho más con esta palabra clave: puede usarla para capturar código que no es seguro para subprocesos, y puede hacerlo en tiempo de compilación. Este artículo muestra cómo se hace; La solución implica un puntero inteligente simple que también facilita la serialización de secciones críticas de código.

El artículo se aplica a ambos Cy C++.

También vea el artículo " C ++ y los peligros del bloqueo doblemente revisado " por Scott Meyers y Andrei Alexandrescu:

Por lo tanto, cuando se trata de algunas ubicaciones de memoria (por ejemplo, puertos mapeados en memoria o memoria referenciada por ISR [Rutinas de servicio de interrupción]), se deben suspender algunas optimizaciones. existe volátil para especificar un tratamiento especial para tales ubicaciones, específicamente: (1) el contenido de una variable volátil es "inestable" (puede cambiar por medios desconocidos para el compilador), (2) todas las escrituras en datos volátiles son "observables" para que debe ejecutarse religiosamente y (3) todas las operaciones con datos volátiles se ejecutan en la secuencia en que aparecen en el código fuente. Las dos primeras reglas aseguran una lectura y escritura adecuadas. El último permite la implementación de protocolos de E / S que mezclan entrada y salida. Esto es informalmente lo que garantiza la volatilidad de C y C ++.


¿El estándar especifica si una lectura se considera 'comportamiento observable' si el valor nunca se usa? Mi impresión es que debería ser así, pero cuando afirmé que estaba en otra parte, alguien me retó por una cita. Me parece que en cualquier plataforma donde la lectura de una variable volátil podría tener algún efecto, se debería requerir un compilador que genere código que realice cada lectura indicada con precisión una vez; sin ese requisito, sería difícil escribir código que generara una secuencia predecible de lecturas.
supercat

@supercat: Según el primer artículo, "Si usa el modificador volátil en una variable, el compilador no almacenará en caché esa variable en los registros; cada acceso alcanzará la ubicación de memoria real de esa variable". Además, en la sección §6.7.3.6 del estándar c99 dice: "Un objeto que tiene un tipo volátil calificado puede modificarse de formas desconocidas para la implementación o tener otros efectos secundarios desconocidos". Además, implica que las variables volátiles pueden no almacenarse en caché en los registros y que todas las lecturas y escrituras deben ejecutarse en orden en relación con los puntos de secuencia, que de hecho son observables.
Robert S. Barnes

El último artículo afirma explícitamente que las lecturas son efectos secundarios. Lo primero indica que las lecturas no pueden realizarse fuera de secuencia, pero no parece descartar la posibilidad de que se eluyan por completo.
supercat

"el compilador no puede almacenarlo en caché en un registro" - La mayoría de las arquitecturas RISC son máquinas de registro, por lo que cualquier lectura-modificación-escritura tiene que almacenar en caché el objeto en un registro. volatileNo garantiza la atomicidad.
demasiado honesto para este sitio

1
@Olaf: cargar algo en un registro no es lo mismo que el almacenamiento en caché. El almacenamiento en caché afectaría el número de cargas o almacenes o su sincronización.
supercat

28

Mi explicación simple es:

En algunos escenarios, basados ​​en la lógica o el código, el compilador hará la optimización de las variables que cree que no cambian. La volatilepalabra clave evita que se optimice una variable.

Por ejemplo:

bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
    // execute logic for the scenario where the USB isn't connected 
}

Del código anterior, el compilador puede pensar que usb_interface_flagse define como 0, y que en el ciclo while será cero para siempre. Después de la optimización, el compilador lo tratará while(true)todo el tiempo, dando como resultado un bucle infinito.

Para evitar este tipo de escenarios, declaramos que el indicador es volátil, le estamos diciendo al compilador que este valor puede ser cambiado por una interfaz externa u otro módulo de programa, es decir, no lo optimice. Ese es el caso de uso para volátiles.


19

Un uso marginal para volátil es el siguiente. Supongamos que desea calcular la derivada numérica de una función f:

double der_f(double x)
{
    static const double h = 1e-3;
    return (f(x + h) - f(x)) / h;
}

El problema es que x+h-xgeneralmente no es igual a hdebido a errores de redondeo. Piénselo: cuando resta números muy cercanos, pierde muchos dígitos significativos que pueden arruinar el cálculo de la derivada (piense 1.00001 - 1). Una posible solución podría ser

double der_f2(double x)
{
    static const double h = 1e-3;
    double hh = x + h - x;
    return (f(x + hh) - f(x)) / hh;
}

pero dependiendo de la plataforma y los conmutadores del compilador, la segunda línea de esa función puede ser eliminada por un compilador de optimización agresiva. Entonces escribes en su lugar

    volatile double hh = x + h;
    hh -= x;

para forzar al compilador a leer la ubicación de la memoria que contiene hh, perdiendo una eventual oportunidad de optimización.


¿Cuál es la diferencia entre usar ho hhen una fórmula derivada? Cuando hhse calcula, la última fórmula la usa como la primera, sin diferencia. Tal vez debería ser (f(x+h) - f(x))/hh?
Sergey Zhukov

2
La diferencia entre hy hhes que hhla operación trunca a una potencia negativa de dos x + h - x. En este caso, x + hhy xdifieren exactamente por hh. También puede tomar su fórmula, se le dará el mismo resultado, ya que x + hy x + hhson iguales (es el denominador que es importante en este caso).
Alexandre C.

3
¿No sería una forma más legible de escribir esto x1=x+h; d = (f(x1)-f(x))/(x1-x)? sin usar el volátil.
Sergey Zhukov

¿Alguna referencia de que un compilador puede borrar esa segunda línea de la función?
CoffeeTableEspresso

@CoffeeTableEspresso: No, lo siento. Cuanto más sé sobre el punto flotante, más creo que el compilador solo puede optimizarlo si se le dice explícitamente, con -ffast-matho equivalente.
Alexandre C.

11

Hay dos usos. Estos se usan especialmente con mayor frecuencia en el desarrollo integrado.

  1. El compilador no optimizará las funciones que usan variables definidas con palabras clave volátiles

  2. Volátil se usa para acceder a ubicaciones de memoria exactas en RAM, ROM, etc. Esto se usa con más frecuencia para controlar dispositivos mapeados en memoria, acceder a registros de CPU y localizar ubicaciones de memoria específicas.

Ver ejemplos con listado de ensamblaje. Re: Uso de la palabra clave "volátil" C en desarrollo integrado


"El compilador no optimizará las funciones que utilizan variables que se definen con palabras clave volátiles", eso es completamente incorrecto.
demasiado honesto para este sitio

10

Volatile también es útil cuando desea forzar al compilador a no optimizar una secuencia de código específica (por ejemplo, para escribir un micro-benchmark).


10

Mencionaré otro escenario donde los volátiles son importantes.

Suponga que asigna un archivo de memoria a un archivo para una E / S más rápida y ese archivo puede cambiar detrás de escena (por ejemplo, el archivo no está en su disco duro local, sino que es servido a través de la red por otra computadora).

Si accede a los datos del archivo mapeado en memoria a través de punteros a objetos no volátiles (en el nivel del código fuente), entonces el código generado por el compilador puede obtener los mismos datos varias veces sin que usted lo sepa.

Si esos datos cambian, su programa puede usar dos o más versiones diferentes de los datos y entrar en un estado inconsistente. Esto puede conducir no solo a un comportamiento lógicamente incorrecto del programa, sino también a agujeros de seguridad explotables en él si procesa archivos no confiables o archivos desde ubicaciones no confiables.

Si le importa la seguridad, y debería hacerlo, este es un escenario importante a considerar.


7

volátil significa que es probable que el almacenamiento cambie en cualquier momento y se cambie pero algo fuera del control del programa de usuario. Esto significa que si hace referencia a la variable, el programa siempre debe verificar la dirección física (es decir, una entrada asignada Fifo), y no utilizarla en una memoria caché.


Ningún compilador toma volátil para significar "dirección física en RAM" o "omitir el caché".
curioso


5

En mi opinión, no debes esperar demasiado de volatile. Para ilustrar, mire el ejemplo en la respuesta altamente votada de Nils Pipenbrinck .

Yo diría que su ejemplo no es adecuado para volatile. volatilesolo se usa para: evitar que el compilador realice optimizaciones útiles y deseables . No tiene nada que ver con el hilo seguro, el acceso atómico o incluso el orden de la memoria.

En ese ejemplo:

    void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
    {
      // wait while the gadget is busy:
      while (gadget->isbusy)
      {
        // do nothing here.
      }
      // set data first:
      gadget->data    = data;
      // writing the command starts the action:
      gadget->command = command;
    }

el gadget->data = dataantes gadget->command = commandsolo solo está garantizado en el código compilado por el compilador. En tiempo de ejecución, el procesador aún puede reordenar los datos y la asignación de comandos, en relación con la arquitectura del procesador. El hardware podría obtener los datos incorrectos (supongamos que el gadget está asignado a E / S de hardware). La barrera de memoria es necesaria entre los datos y la asignación de comandos.


2
Diría que volátil se usa para evitar que el compilador realice optimizaciones que normalmente serían útiles y deseables. Tal como volatileestá escrito, parece que está degradando el rendimiento sin ninguna razón. En cuanto a si es suficiente, eso dependerá de otros aspectos del sistema que el programador pueda conocer más que el compilador. Por otro lado, si un procesador garantiza que una instrucción para escribir en una determinada dirección vaciará el caché de la CPU pero un compilador no proporcionó ninguna forma de vaciar las variables almacenadas en caché de registro de las que la CPU no sabe nada, vaciar el caché sería inútil.
supercat

5

En el lenguaje diseñado por Dennis Ritchie, cada acceso a cualquier objeto, que no sean objetos automáticos cuya dirección no se haya tomado, se comportaría como si calculara la dirección del objeto y luego leyera o escribiera el almacenamiento en esa dirección. Esto hizo que el lenguaje fuera muy poderoso, pero limitó severamente las oportunidades de optimización.

Si bien podría haber sido posible agregar un calificador que invitaría a un compilador a suponer que un objeto en particular no se cambiaría de manera extraña, tal suposición sería apropiada para la gran mayoría de los objetos en los programas en C, y habría tenido No ha sido práctico agregar un calificador a todos los objetos para los cuales tal suposición sería apropiada. Por otro lado, algunos programas necesitan usar algunos objetos para los cuales tal suposición no sería válida. Para resolver este problema, el Estándar dice que los compiladores pueden suponer que los objetos que no se declaran volatileno tendrán su valor observado o cambiado de formas que están fuera del control del compilador, o estarían fuera del entendimiento de un compilador razonable.

Debido a que varias plataformas pueden tener diferentes formas en que los objetos podrían observarse o modificarse fuera del control de un compilador, es apropiado que los compiladores de calidad para esas plataformas difieran en su manejo exacto de la volatilesemántica. Desafortunadamente, debido a que el Estándar no sugirió que los compiladores de calidad destinados a la programación de bajo nivel en una plataforma deben manejar volatilede una manera que reconozca todos los efectos relevantes de una operación de lectura / escritura en particular en esa plataforma, muchos compiladores no cumplen de modo que sea más difícil procesar cosas como E / S en segundo plano de una manera que sea eficiente pero que no se pueda romper con las "optimizaciones" del compilador.


5

En términos simples, le dice al compilador que no haga ninguna optimización en una variable en particular. Las variables que se asignan al registro del dispositivo son modificadas indirectamente por el dispositivo. En este caso, se debe usar volátil.


1
¿Hay algo nuevo en esta respuesta que no se haya mencionado antes?
slfan

3

Se puede cambiar un volátil desde fuera del código compilado (por ejemplo, un programa puede asignar una variable volátil a un registro mapeado en memoria). El compilador no aplicará ciertas optimizaciones al código que maneja una variable volátil, por ejemplo, ganó ' t cárguelo en un registro sin escribirlo en la memoria. Esto es importante cuando se trata de registros de hardware.


0

Como muchos sugieren aquí, el uso popular de la palabra clave volátil es omitir la optimización de la variable volátil.

La mejor ventaja que viene a la mente, y vale la pena mencionar después de leer sobre volátiles es: evitar el retroceso de la variable en caso de a longjmp. Un salto no local.

¿Qué significa esto?

Simplemente significa que el último valor se retendrá después de desenrollar la pila , para volver a algún marco de pila anterior; típicamente en caso de algún escenario erróneo.

Dado que estaría fuera del alcance de esta pregunta, no voy a entrar en detalles setjmp/longjmpaquí, pero vale la pena leer al respecto; y cómo se puede usar la función de volatilidad para retener el último valor.


-2

no permite que el compilador cambie automáticamente los valores de las variables. Una variable volátil es para uso dinámico.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.