¿Por qué se volatile
necesita en C? ¿Para qué se usa esto? Que va a hacer
¿Por qué se volatile
necesita en C? ¿Para qué se usa esto? Que va a hacer
Respuestas:
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;
}
volatile
en 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 volatile
variable 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 volatile
nos ayudará a acceder al valor nuevamente cada vez.
volatile
era 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 ...
Otro uso para volatile
es 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 quit
variable y convierte el bucle en un while (true)
bucle. Incluso si la quit
variable se establece en el controlador de señal para SIGINT
y SIGTERM
; el compilador no tiene forma de saber eso.
Sin embargo, si quit
se 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.
quit
, el compilador puede optimizarlo en un bucle constante, suponiendo que no hay forma de quit
cambiar entre iteraciones. NB: Esto no es necesariamente un buen sustituto para la programación real segura para subprocesos.
volatile
otros marcadores, se supondrá que nada fuera del ciclo modifica esa variable una vez que ingresa al ciclo, incluso si es una variable global.
extern int global; void fn(void) { while (global != 0) { } }
con gcc -O3 -S
y 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 volatile
y ver la diferencia.
volatile
le 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.
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 C
y 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 ++.
volatile
No garantiza la atomicidad.
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 volatile
palabra 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_flag
se 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.
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-x
generalmente no es igual a h
debido 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.
h
o hh
en una fórmula derivada? Cuando hh
se calcula, la última fórmula la usa como la primera, sin diferencia. Tal vez debería ser (f(x+h) - f(x))/hh
?
h
y hh
es que hh
la operación trunca a una potencia negativa de dos x + h - x
. En este caso, x + hh
y x
difieren exactamente por hh
. También puede tomar su fórmula, se le dará el mismo resultado, ya que x + h
y x + hh
son iguales (es el denominador que es importante en este caso).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? sin usar el volátil.
-ffast-math
o equivalente.
Hay dos usos. Estos se usan especialmente con mayor frecuencia en el desarrollo integrado.
El compilador no optimizará las funciones que usan variables definidas con palabras clave volátiles
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
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).
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.
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é.
La Wiki dice todo sobre volatile
:
Y el documento del kernel de Linux también hace una excelente notación sobre volatile
:
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
. volatile
solo 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 = data
antes gadget->command = command
solo 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.
volatile
está 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.
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 volatile
no 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 volatile
semá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 volatile
de 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.
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.
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.
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/longjmp
aquí, pero vale la pena leer al respecto; y cómo se puede usar la función de volatilidad para retener el último valor.