Volátil (programación informática)
En programación informática, volatile significa que un valor es propenso a cambiar con el tiempo, fuera del control de algún código. La volatilidad tiene implicaciones dentro de las convenciones de llamada de funciones y también afecta la forma en que se almacenan, acceden y almacenan en caché las variables.
En los lenguajes de programación C, C++, C# y Java, la palabra clave volatile indica que un valor puede cambiar entre diferentes accesos, incluso si no parece estar modificado. Esta palabra clave evita que un compilador de optimización optimice lecturas o escrituras posteriores y, por lo tanto, reutilice incorrectamente un valor obsoleto u omita escrituras. Los valores volátiles surgen principalmente en el acceso al hardware (E/S mapeadas en memoria), donde se utiliza la lectura o escritura en la memoria para comunicarse con dispositivos periféricos, y en el subproceso, donde un subproceso diferente puede haber modificado un valor.
A pesar de ser una palabra clave común, el comportamiento de volatile
difiere significativamente entre lenguajes de programación y se malinterpreta fácilmente. En C y C++, es un calificador de tipo, como const
, y es una propiedad del type. Además, en C y C++ no funciona en la mayoría de los escenarios de subprocesamiento y se desaconseja su uso. En Java y C#, es una propiedad de una variable e indica que el objeto al que está vinculada la variable puede mutar y está diseñado específicamente para subprocesos. En el lenguaje de programación D, existe una palabra clave independiente shared
para el uso de subprocesos, pero no existe ninguna palabra clave volatile
.
En C y C++
En C, y en consecuencia en C++, la palabra clave volatile
tenía como objetivo:
- permitir el acceso a dispositivos I/O con memoria
- permitir usos de variables entre
setjmp
ylongjmp
- permitir usos de
sig_atomic_t
variables en controladores de señal.
Dado que las variables marcadas como volátiles son propensas a cambiar fuera del flujo de código estándar, el compilador debe realizar cada lectura y escritura en la variable como lo indica el código. Cualquier acceso a variables volátiles no se puede optimizar, p. mediante el uso de registros para el almacenamiento de valores intermedios.
Aunque está previsto tanto en C como en C++, los estándares de C no expresan que la semántica volatile
se refiere al valor l, no al objeto referenciado. El respectivo informe de defectos DR 476 (a C11) aún está bajo revisión con C17.
Operaciones sobre volatile
variables no son atómicas, ni establecen una relación apropiada-antes para el roscado. Esto se especifica en las normas pertinentes (C, C++, POSIX, WIN32), y las variables volátiles no son inseguras en la gran mayoría de las implementaciones actuales. Así, el uso de volatile
palabra clave como mecanismo de sincronización portátil es desalentado por muchos grupos C/C++.
Ejemplo de E/S asignadas en memoria en C
En este ejemplo, el código establece el valor almacenado en foo
en 0
. Luego comienza a sondear ese valor repetidamente hasta que cambia a 255
:
estática int Foo;vacío bar()vacío) {} Foo = 0; mientras ()Foo ! 255) ;}
Un compilador optimizador notará que ningún otro código puede cambiar el valor almacenado en foo
y asumirá que permanecerá igual a 0
en todo momento. Por lo tanto, el compilador reemplazará el cuerpo de la función con un bucle infinito similar a este:
vacío bar_optimized()vacío) {} Foo = 0; mientras ()verdadero) ;}
Sin embargo, foo
puede representar una ubicación que otros elementos del sistema informático pueden cambiar en cualquier momento, como un registro de hardware de un dispositivo conectado a la CPU. El código anterior nunca detectaría tal cambio; sin la palabra clave volatile
, el compilador asume que el programa actual es la única parte del sistema que podría cambiar el valor (que es, con diferencia, la situación más común).
Para evitar que el compilador optimice el código como se indica arriba, se utiliza la palabra clave volatile
:
estática volátil int Foo;vacío bar ()vacío) {} Foo = 0; mientras ()Foo ! 255) ;}
Con esta modificación, la condición del bucle no se optimizará y el sistema detectará el cambio cuando ocurra.
Generalmente, hay operaciones de barrera de memoria disponibles en plataformas (que están expuestas en C++ 11) que deben preferirse en lugar de volátiles, ya que permiten que el compilador realice una mejor optimización y, lo que es más importante, garantizan un comportamiento correcto en subprocesos múltiples. escenarios; ni la especificación C (anterior a C11) ni la especificación C++ (anterior a C++11) especifican un modelo de memoria multiproceso, por lo que es posible que volátil no se comporte de manera determinista entre sistemas operativos, compiladores y CPU.
Comparación de optimización en C
Los siguientes programas en C y los extractos en lenguaje ensamblador que los acompañan demuestran cómo la palabra clave volatile
afecta la salida del compilador. El compilador en este caso fue GCC.
Al observar el código ensamblador, es claramente visible que el código generado con objetos volatile
es más detallado, lo que lo hace más largo para que se pueda cumplir la naturaleza de los objetos volatile
. . La palabra clave volatile
evita que el compilador realice optimización en el código que involucra objetos volátiles, asegurando así que cada asignación y lectura de variable volátil tenga un acceso a memoria correspondiente. Sin la palabra clave volatile
, el compilador sabe que no es necesario volver a leer una variable desde la memoria en cada uso, porque no debería haber ninguna escritura en su ubicación de memoria desde ningún otro subproceso o proceso.
Comparación de la Asamblea General | |
---|---|
Sin volatile palabra clave | Con volatile palabra clave
|
# incluir Identificado.hint principal() {} /* Estas variables nunca se crearán en la pila*/ int a = 10, b = 100, c = 0, d = 0; /* "printf" se llamará con argumentos "%d" y 110 (el compilador calcula la suma de a+b), por lo tanto, ninguna sobrecarga de realizar la adición tiempo de ejecución */ printf()"%d", a + b); /* Este código se eliminará mediante optimización, pero el impacto de 'c' y 'd' convirtiéndose en 100 puede ser visto al llamar "printf" */ a = b; c = b; d = b; /* Compiler generará código donde se imprime con argumentos "%d" y 200 */ printf()"%d", c + d); Regreso 0;} | # incluir Identificado.hint principal() {} volátil int a = 10, b = 100, c = 0, d = 0; printf()"%d", a + b); a = b; c = b; d = b; printf()"%d", c + d); Regreso 0;} |
gcc -S -O3 -masm=intel noVolatileVar.c -o without.s | gcc -S -O3 -masm=intel VolatileVar.c -o con.s |
.file "noVolátileVar.c" .intel_syntax noprefix .sección .rodata.str1.1,"AMS",@progbits,1. LC0: .string "%d" .sección .text.startup,"ax",@progbits .p2align 4,15 .globl principal .tipo principal, @funciónprincipal:.LFB11 .cfi_startproc sub rsp, 8 .cfi_def_cfa_offset 16 mov esi, 110 mov edi, OFFSET FLAT:.LC0 xor eax, eax llamada printf mov esi, 200 mov edi, OFFSET FLAT:.LC0 xor eax, eax llamada printf xor eax, eax añadir rsp, 8 .cfi_def_cfa_offset 8 Ret .cfi_endproc. LFE11: . tamaño principal, .-main .ident "GCC: (GNU) 4.8.2" .sección .note.GNU-stack,",@progbits | .file VolátilVar.c .intel_syntax noprefix .sección .rodata.str1.1,"AMS",@progbits,1. LC0: .string "%d" .sección .text.startup,"ax",@progbits .p2align 4,15 .globl principal .tipo principal, @funciónprincipal:.LFB11 .cfi_startproc sub rsp, 24 .cfi_def_cfa_offset 32 mov edi, OFFSET FLAT:.LC0 mov DWORD PTR [rsp] 10 mov DWORD PTR [rsp+4] 100 mov DWORD PTR [rsp+8] 0 mov DWORD PTR [rsp+12] 0 mov esi, DWORD PTR [rsp] mov eax, DWORD PTR [rsp+4] añadir esi, eax xor eax, eax llamada printf mov eax, DWORD PTR [rsp+4] mov edi, OFFSET FLAT:.LC0 mov DWORD PTR [rsp] eax mov eax, DWORD PTR [rsp+4] mov DWORD PTR [rsp+8] eax mov eax, DWORD PTR [rsp+4] mov DWORD PTR [rsp+12] eax mov esi, DWORD PTR [rsp+8] mov eax, DWORD PTR [rsp+12] añadir esi, eax xor eax, eax llamada printf xor eax, eax añadir rsp, 24 .cfi_def_cfa_offset 8 Ret .cfi_endproc. LFE11: . tamaño principal, .-main .ident "GCC: (GNU) 4.8.2" .sección .note.GNU-stack,",@progbits |
C++11
De acuerdo con el estándar ISO C++11, la palabra clave volátil solo debe usarse para acceso al hardware; no lo utilice para la comunicación entre subprocesos. Para la comunicación entre subprocesos, la biblioteca estándar proporciona plantillas std::atomic<T>
.
En Java
El lenguaje de programación Java también tiene la palabra clave volatile
, pero se utiliza para un propósito algo diferente. Cuando se aplica a un campo, el calificador Java volatile
proporciona las siguientes garantías:
- En todas las versiones de Java, hay un orden global sobre lecturas y escritos de todas las variables volátiles (este orden global sobre volatiles es un orden parcial sobre las mayores orden de sincronización (que es un orden total sobre todo acciones de sincronización)). Esto implica que cada hilo que accede a un campo volátil leerá su valor actual antes de continuar, en lugar de (potencialmente) utilizando un valor caché. (Sin embargo, no hay garantía sobre el ordenamiento relativo de lecturas volátiles y escribe con lecturas y escritos regulares, lo que significa que generalmente no es una construcción de rosca útil.)
- En Java 5 o posterior, las lecturas y escritos volátiles establecen una relación anterior a la aparición, como adquirir y liberar un mutex.
Usar volatile
puede ser más rápido que un bloqueo, pero no funcionará en algunas situaciones antes de Java 5. La variedad de situaciones en las que volatile es efectivo se amplió en Java 5; en particular, el bloqueo de doble verificación ahora funciona correctamente.
En C#
En C#, volatile
garantiza que el código que accede al campo no esté sujeto a algunas optimizaciones inseguras para subprocesos que puedan ser realizadas por el compilador, el CLR o el hardware. Cuando un campo está marcado como volatile
, el compilador genera una acquire-fence, que evita que otras lecturas y escrituras en el campo se muevan antes de la valla. . Al escribir en un campo volatile
, el compilador genera un release-fence; esta barrera evita que otras lecturas y escrituras en el campo se muevan después de la barrera.
Solo los siguientes tipos pueden marcarse como volatile
: todos los tipos de referencia, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char< /code> y todos los tipos enumerados con un tipo subyacente de
o Byte
, SByte
, Int16
, UInt16
, < código>Int32UInt32
. (Esto excluye las estructuras de valor, así como los tipos primitivos Double
, Int64
, UInt64
y Decimal
.)
El uso de la palabra clave volatile
no admite campos que se pasan por referencia o variables locales capturadas; en estos casos, se deben utilizar Thread.VolatileRead
y Thread.VolatileWrite
.
En efecto, estos métodos desactivan algunas optimizaciones que normalmente realizan el compilador de C#, el compilador JIT o la propia CPU. Las garantías proporcionadas por Thread.VolatileRead
y Thread.VolatileWrite
son un superconjunto de las garantías proporcionadas por la palabra clave volatile
: en lugar de generar un 34;media valla" (es decir, una barrera de adquisición solo evita el reordenamiento de las instrucciones y el almacenamiento en caché que le preceden), VolatileRead
y VolatileWrite
generan una barrera "completa" que impiden el reordenamiento de instrucciones y el almacenamiento en caché de ese campo en ambas direcciones. Estos métodos funcionan de la siguiente manera:
- El
Thread.VolatileWrite
método obliga al valor en el campo a ser escrito en el punto de la llamada. Además, cualquier cargas y tiendas anteriores del programa-orden debe ocurrir antes de la llamada aVolatileWrite
y cualquier cargas y tiendas posteriores del programa-orden debe ocurrir después de la llamada. - El
Thread.VolatileRead
método obliga al valor en el campo a ser leído desde el punto de la llamada. Además, cualquier cargas y tiendas anteriores del programa-orden debe ocurrir antes de la llamada aVolatileRead
y cualquier cargas y tiendas posteriores del programa-orden debe ocurrir después de la llamada.
El Thread.VolatileRead
y Thread.VolatileWrite
métodos generan una valla completa llamando a la Thread.MemoryBarrier
método, que construye una barrera de memoria que funciona en ambas direcciones. Además de las motivaciones para usar una valla completa dada arriba, un problema potencial con la volatile
palabra clave que se resuelve utilizando una valla completa generada por Thread.MemoryBarrier
es el siguiente: debido a la naturaleza asimétrica de medias cercas, a volatile
campo con una instrucción de escritura seguida de una instrucción de lectura puede todavía tener la orden de ejecución intercambiada por el compilador. Debido a que las cercas completas son simétricas, esto no es un problema al usar Thread.MemoryBarrier
.
En Fortran
VOLATILE
es parte del estándar Fortran 2003, aunque la versión anterior lo admitía como una extensión. Hacer que todas las variables sean volatile
en una función también es útil para encontrar errores relacionados con el alias.
entero, volátil :: i ! Cuando no se define volátil las siguientes dos líneas de código son idénticasescribir()*,*) i#2 ! Carga la variable una vez desde la memoria y multiplica que los tiempos de valor en síescribir()*,*) i*i ! Carga la variable dos veces de memoria y multiplica esos valores
Al "reducir" siempre a la memoria de un VOLATILE, el compilador de Fortran se excluye de reordenar lecturas o escribe a volatiles. Esto hace visible a otros hilos acciones hechas en este hilo, y viceversa.
El uso de VOLATILE reduce e incluso puede impedir la optimización.