volatile (programmazione di computer) - volatile (computer programming)
In programmazione di computer , in particolare nel C , C ++ , C # e Java linguaggi di programmazione , la volatilità parola chiave indica che un valore può variare tra i diversi accessi, anche se non sembra essere modificata. Questa parola chiave impedisce a un compilatore di ottimizzazione di ottimizzare le letture o le scritture successive e quindi di riutilizzare in modo errato un valore non aggiornato o di omettere scritture. I valori volatili si verificano principalmente nell'accesso all'hardware ( I/O mappato in memoria ), in cui la lettura o la scrittura nella memoria viene utilizzata per comunicare con i dispositivi periferici e nel threading , in cui un thread diverso potrebbe aver modificato un valore.
Nonostante sia una parola chiave comune, il comportamento di volatiledifferisce in modo significativo tra i linguaggi di programmazione ed è facilmente frainteso. In C e C++, è un qualificatore di tipo , come const, ed è una proprietà del tipo . Inoltre, in C e C++ non funziona nella maggior parte degli scenari di threading e tale utilizzo è sconsigliato. In Java e C#, è una proprietà di una variabile e indica che l' oggetto a cui è associata la variabile può mutare ed è specificamente destinato al threading. Nel linguaggio di programmazione D esiste una parola chiave separata sharedper l'utilizzo del threading, ma non volatileesiste alcuna parola chiave.
In C e C++
In C, e di conseguenza C++, la volatileparola chiave era destinata a
- consentire l'accesso a dispositivi I/O mappati in memoria
- consentire usi di variabili tra
setjmpelongjmp - consentire l'uso di
sig_atomic_tvariabili nei gestori di segnale.
Sebbene sia inteso da C e C++, gli standard C non riescono a esprimere che la volatilesemantica si riferisce all'lvalue, non all'oggetto referenziato. Il relativo rapporto di difetto DR 476 (a C11) è ancora in fase di revisione con C17.
Le operazioni sulle volatilevariabili non sono atomiche , né stabiliscono una corretta relazione accade prima per il threading. Ciò è specificato negli standard pertinenti (C, C++, POSIX , WIN32) e le variabili volatili non sono thread-safe nella stragrande maggioranza delle attuali implementazioni. Pertanto, l'uso della volatileparola chiave come meccanismo di sincronizzazione portatile è scoraggiato da molti gruppi C/C++.
Esempio di I/O mappato in memoria in C
In questo esempio, il codice imposta il valore memorizzato in fooa 0. Quindi inizia a interrogare quel valore ripetutamente finché non cambia in 255:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255)
;
}
Un compilatore ottimizzatore noterà che nessun altro codice può modificare il valore memorizzato in fooe presumerà che rimarrà sempre uguale a 0. Il compilatore sostituirà quindi il corpo della funzione con un ciclo infinito simile a questo:
void bar_optimized(void) {
foo = 0;
while (true)
;
}
Tuttavia, foopotrebbe rappresentare una posizione che può essere modificata da altri elementi del sistema informatico in qualsiasi momento, come un registro hardware di un dispositivo collegato alla CPU . Il codice sopra non rileverebbe mai un tale cambiamento; senza la volatileparola chiave, il compilatore assume che il programma corrente sia l'unica parte del sistema che potrebbe modificare il valore (che è di gran lunga la situazione più comune).
Per impedire al compilatore di ottimizzare il codice come sopra, volatileviene utilizzata la parola chiave:
static volatile int foo;
void bar (void) {
foo = 0;
while (foo != 255)
;
}
Con questa modifica la condizione del loop non verrà ottimizzata e il sistema rileverà il cambiamento quando si verifica.
In genere, sono disponibili operazioni di barriera di memoria su piattaforme (che sono esposte in C++11) che dovrebbero essere preferite anziché volatili in quanto consentono al compilatore di eseguire una migliore ottimizzazione e, soprattutto, garantiscono un comportamento corretto in scenari multi-thread; né la specifica C (prima di C11) né la specifica C++ (prima di C++ 11) specificano un modello di memoria multi-thread, quindi volatile potrebbe non comportarsi in modo deterministico tra sistemi operativi/compilatori/CPU.
Confronto di ottimizzazione in C
I seguenti programmi C e gli assembly associati dimostrano come la volatileparola chiave influisca sull'output del compilatore. Il compilatore in questo caso era GCC .
Osservando il codice assembly, è chiaramente visibile che il codice generato con gli volatileoggetti è più dettagliato, rendendolo più lungo in modo che la natura degli volatileoggetti possa essere soddisfatta. La volatileparola chiave impedisce al compilatore di eseguire l'ottimizzazione sul codice che coinvolge oggetti volatili, garantendo così che ogni assegnazione e lettura di variabile volatile abbia un accesso alla memoria corrispondente. Senza la volatileparola chiave, il compilatore sa che una variabile non ha bisogno di essere riletta dalla memoria ad ogni utilizzo, perché non dovrebbero esserci scritture nella sua posizione di memoria da nessun altro thread o processo.
| Confronto di montaggio | |
|---|---|
Senza volatileparole chiave |
Con volatileparola chiave
|
# include <stdio.h>
int main() {
/* These variables will never be created on stack*/
int a = 10, b = 100, c = 0, d = 0;
/* "printf" will be called with arguments "%d" and
110 (the compiler computes the sum of a+b),
hence no overhead of performing addition at
run-time */
printf("%d", a + b);
/* This code will be removed via optimization, but
the impact of 'c' and 'd' becoming 100 can be
seen while calling "printf" */
a = b;
c = b;
d = b;
/* Compiler will generate code where printf is
called with arguments "%d" and 200 */
printf("%d", c + d);
return 0;
}
|
# include <stdio.h>
int main() {
volatile int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
| gcc -S -O3 -masm=intel noVolatileVar.c -o senza.s | gcc -S -O3 -masm=intel VolatileVar.c -o with.s |
.file "noVolatileVar.c"
.intel_syntax noprefix
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
sub rsp, 8
.cfi_def_cfa_offset 16
mov esi, 110
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
mov esi, 200
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.2"
.section .note.GNU-stack,"",@progbits
|
.file "VolatileVar.c"
.intel_syntax noprefix
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.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]
add esi, eax
xor eax, eax
call 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]
add esi, eax
xor eax, eax
call printf
xor eax, eax
add rsp, 24
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.2"
.section .note.GNU-stack,"",@progbits
|
C++ 11
Secondo lo standard ISO C++11 , la parola chiave volatile è pensata solo per l'uso per l'accesso all'hardware; non utilizzarlo per la comunicazione tra thread. Per la comunicazione tra thread, la libreria standard fornisce std::atomic<T>modelli.
In Giava
Anche il linguaggio di programmazione Java ha la volatileparola chiave, ma è usato per uno scopo un po' diverso. Quando applicato a un campo, il qualificatore Java volatilefornisce le seguenti garanzie:
- In tutte le versioni di Java, esiste un ordinamento globale in lettura e scrittura di tutte le variabili volatili (questo ordinamento globale sui volatili è un ordine parziale rispetto all'ordine di sincronizzazione più ampio (che è un ordine totale su tutte le azioni di sincronizzazione )). Ciò implica che ogni thread che accede a un campo volatile leggerà il suo valore corrente prima di continuare, invece di (potenzialmente) utilizzare un valore memorizzato nella cache. (Tuttavia, non vi è alcuna garanzia sull'ordine relativo di letture e scritture volatili con letture e scritture regolari, il che significa che generalmente non è un costrutto di threading utile.)
- In Java 5 o versioni successive, le letture e le scritture volatili stabiliscono una relazione accade prima , proprio come l'acquisizione e il rilascio di un mutex.
L'utilizzo volatilepuò essere più veloce di un lock , ma non funzionerà in alcune situazioni prima di Java 5. La gamma di situazioni in cui volatile è efficace è stata ampliata in Java 5; in particolare, il bloccaggio a doppio controllo ora funziona correttamente.
in do#
In C# , volatilegarantisce che il codice che accede al campo non sia soggetto ad alcune ottimizzazioni non sicure per i thread che possono essere eseguite dal compilatore, dal CLR o dall'hardware. Quando un campo è contrassegnato volatile, al compilatore viene richiesto di generare una "barriera di memoria" o "recinto" attorno ad esso, che impedisce il riordino delle istruzioni o la memorizzazione nella cache legata al campo. Durante la lettura di un volatilecampo, il compilatore genera un recinto di acquisizione , che impedisce ad altre letture e scritture nel campo, comprese quelle in altri thread, di essere spostate prima del recinto. Quando si scrive in un volatilecampo, il compilatore genera un release-fence ; questo recinto impedisce che altre letture e scritture sul campo vengano spostate dopo il recinto.
Solo i seguenti tipi possono essere marcate volatile: tutti i tipi di riferimento, Single, Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Char, e tutti i tipi enumerati con un tipo sottostante Byte, SByte, Int16, UInt16, Int32, o UInt32. (Ciò esclude valore struct , così come i tipi primitivi Double, Int64, UInt64ed Decimal.)
L'utilizzo della volatileparola chiave non supporta i campi passati per riferimento o le variabili locali acquisite ; in questi casi, Thread.VolatileReade Thread.VolatileWritedeve essere utilizzato al suo posto.
In effetti, questi metodi disabilitano alcune ottimizzazioni solitamente eseguite dal compilatore C#, dal compilatore JIT o dalla CPU stessa. Le garanzie fornite da Thread.VolatileReade Thread.VolatileWritesono un sovrainsieme delle garanzie fornite dalla volatileparola chiave: invece di generare un "mezzo recinto" (cioè un recinto di acquisizione impedisce solo il riordino e la memorizzazione nella cache delle istruzioni che lo precedono), VolatileReade VolatileWritegenera un "recinto completo" che impedire il riordino delle istruzioni e la memorizzazione nella cache di quel campo in entrambe le direzioni. Questi metodi funzionano come segue:
- Il
Thread.VolatileWritemetodo forza la scrittura del valore nel campo al momento della chiamata. Inoltre, qualsiasi caricamento e memorizzazione dell'ordine di programma precedente deve avvenire prima della chiamata aVolatileWritee qualsiasi caricamento e memorizzazione dell'ordine di programma successivo deve avvenire dopo la chiamata. - Il
Thread.VolatileReadmetodo forza la lettura del valore nel campo dal punto della chiamata. Inoltre, qualsiasi caricamento e memorizzazione dell'ordine di programma precedente deve avvenire prima della chiamata aVolatileReade qualsiasi caricamento e memorizzazione dell'ordine di programma successivo deve avvenire dopo la chiamata.
I metodi Thread.VolatileReade Thread.VolatileWritegenerano una recinzione completa chiamando il Thread.MemoryBarriermetodo, che costruisce una barriera di memoria che funziona in entrambe le direzioni. Oltre alle motivazioni per l'utilizzo di una recinzione completa fornite sopra, un potenziale problema con la volatileparola chiave che viene risolto utilizzando una recinzione completa generata da Thread.MemoryBarrierè il seguente: a causa della natura asimmetrica delle mezze barriere, un volatilecampo con un'istruzione di scrittura seguita da un'istruzione di lettura può ancora avere l'ordine di esecuzione scambiato dal compilatore. Poiché le recinzioni complete sono simmetriche, questo non è un problema quando si utilizzano Thread.MemoryBarrier.
In Fortran
VOLATILEfa parte dello standard Fortran 2003 , sebbene la versione precedente lo supportasse come estensione. Rendere tutte le variabili volatilein una funzione è utile anche per trovare bug relativi all'aliasing .
integer, volatile :: i ! When not defined volatile the following two lines of code are identical
write(*,*) i**2 ! Loads the variable i once from memory and multiplies that value times itself
write(*,*) i*i ! Loads the variable i twice from memory and multiplies those values
Eseguendo sempre il "drilling" nella memoria di un VOLATILE, al compilatore Fortran è precluso il riordino delle letture o delle scritture sui volatili. Ciò rende visibili ad altri thread le azioni eseguite in questo thread e viceversa.
L'uso di VOLATILE riduce e può anche impedire l'ottimizzazione.