Perché è volatile
necessario in C? A cosa serve? Che cosa farà?
Perché è volatile
necessario in C? A cosa serve? Che cosa farà?
Risposte:
Volatile dice al compilatore di non ottimizzare nulla che abbia a che fare con la variabile volatile.
Ci sono almeno tre ragioni comuni per usarlo, tutte implicanti situazioni in cui il valore della variabile può cambiare senza azione dal codice visibile: Quando si interfaccia con l'hardware che cambia il valore stesso; quando è in esecuzione un altro thread che utilizza anche la variabile; o quando c'è un gestore di segnale che potrebbe cambiare il valore della variabile.
Supponiamo che tu abbia un piccolo componente hardware mappato nella RAM da qualche parte e che abbia due indirizzi: una porta di comando e una porta dati:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Ora vuoi inviare qualche 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;
}
Sembra facile, ma può fallire perché il compilatore è libero di cambiare l'ordine in cui sono scritti dati e comandi. Ciò farebbe in modo che il nostro piccolo gadget emettesse comandi con il valore di dati precedente. Dai anche un'occhiata all'attesa mentre il circuito è occupato. Quello sarà ottimizzato. Il compilatore cercherà di essere intelligente, leggere il valore di isbusy solo una volta e poi andare in un ciclo infinito. Non è quello che vuoi.
Il modo per aggirare questo è dichiarare il gadget puntatore come volatile. In questo modo il compilatore è costretto a fare ciò che hai scritto. Non può rimuovere le assegnazioni di memoria, non può memorizzare nella cache le variabili nei registri e non può nemmeno cambiare l'ordine delle assegnazioni:
Questa è la versione corretta:
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
in C è effettivamente esistito allo scopo di non memorizzare automaticamente nella cache i valori della variabile. Dirà al compilatore di non memorizzare nella cache il valore di questa variabile. Quindi genererà codice per prendere il valore della volatile
variabile data dalla memoria principale ogni volta che la incontra. Questo meccanismo viene utilizzato perché in qualsiasi momento il valore può essere modificato dal sistema operativo o da qualsiasi interruzione. Quindi l'utilizzo volatile
ci aiuterà ad accedere nuovamente al valore ogni volta.
volatile
era quello di consentire ai compilatori di ottimizzare il codice, consentendo allo stesso tempo ai programmatori di ottenere la semantica che sarebbe stata raggiunta senza tali ottimizzazioni. Gli autori dello Standard si aspettavano che le implementazioni di qualità avrebbero supportato qualunque semantica fosse utile date le loro piattaforme target e campi di applicazione, e non si aspettavano che gli autori di compilatori avrebbero cercato di offrire la semantica di qualità più bassa conforme allo Standard e non erano al 100% stupido (nota che gli autori dello Standard riconoscono esplicitamente nella logica ...
Un altro uso volatile
è per i gestori di segnale. Se hai un codice come questo:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Al compilatore è consentito notare che il corpo del ciclo non tocca la quit
variabile e converte il ciclo in un while (true)
ciclo. Anche se la quit
variabile è impostata sul gestore del segnale per SIGINT
eSIGTERM
; il compilatore non ha modo di saperlo.
Tuttavia, se la quit
variabile viene dichiarata volatile
, il compilatore è costretto a caricarla ogni volta, perché può essere modificata altrove. Questo è esattamente quello che vuoi in questa situazione.
quit
, il compilatore può ottimizzarlo in un ciclo costante, assumendo che non è quit
possibile cambiare tra le iterazioni. NB: Questo non è necessariamente un buon sostituto per l'effettiva programmazione thread-safe.
volatile
o altri marcatori, supporrà che nulla al di fuori del ciclo modifichi quella variabile una volta che entra nel ciclo, anche se è una variabile globale.
extern int global; void fn(void) { while (global != 0) { } }
con gcc -O3 -S
e un'occhiata al file assembly risultante, sulla mia macchina lo fa movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, cioè un ciclo infinito se il globale non è zero. Quindi prova ad aggiungere volatile
e vedi la differenza.
volatile
dice al compilatore che la tua variabile può essere cambiata con altri mezzi, oltre al codice a cui accede. ad esempio, potrebbe essere una posizione di memoria mappata I / O. Se ciò non viene specificato in questi casi, alcuni accessi variabili possono essere ottimizzati, ad esempio, il suo contenuto può essere conservato in un registro e la posizione della memoria non può essere riletta nuovamente.
Vedi questo articolo di Andrei Alexandrescu, " volatile - Il migliore amico del programmatore multithread "
La parola chiave volatile è stata ideata per impedire l'ottimizzazione del compilatore che potrebbe rendere il codice errato in presenza di determinati eventi asincroni. Ad esempio, se si dichiara una variabile primitiva come volatile , al compilatore non è consentito memorizzarla nella cache in un registro, un'ottimizzazione comune che sarebbe disastrosa se tale variabile fosse condivisa tra più thread. Quindi la regola generale è che se si hanno variabili di tipo primitivo che devono essere condivise tra più thread, dichiarare tali variabili volatili. Ma in realtà puoi fare molto di più con questa parola chiave: puoi usarlo per catturare il codice che non è thread-safe e puoi farlo in fase di compilazione. Questo articolo mostra come è fatto; la soluzione prevede un semplice puntatore intelligente che semplifica anche la serializzazione di sezioni critiche di codice.
L'articolo si applica a entrambi C
e C++
.
Vedi anche l'articolo " C ++ and the Perils of Double-Checked Locking " di Scott Meyers e Andrei Alexandrescu:
Pertanto, quando si ha a che fare con alcune posizioni di memoria (ad es. Porte mappate in memoria o memoria a cui fanno riferimento gli ISR [routine di servizio di interruzione]), alcune ottimizzazioni devono essere sospese. esiste volatile per specificare un trattamento speciale per tali posizioni, in particolare: (1) il contenuto di una variabile volatile è "instabile" (può cambiare in modo sconosciuto al compilatore), (2) tutte le scritture su dati volatili sono "osservabili", quindi deve essere eseguito religiosamente e (3) tutte le operazioni sui dati volatili vengono eseguite nella sequenza in cui compaiono nel codice sorgente. Le prime due regole garantiscono una lettura e una scrittura adeguate. L'ultimo consente l'implementazione di protocolli I / O che mescolano input e output. Questo è informalmente ciò che le garanzie volatili di C e C ++.
volatile
non garantisce l'atomicità.
La mia semplice spiegazione è:
In alcuni scenari, in base alla logica o al codice, il compilatore eseguirà l'ottimizzazione delle variabili che ritiene non cambino. La volatile
parola chiave impedisce l'ottimizzazione di una variabile.
Per esempio:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Dal codice sopra, il compilatore può pensare che usb_interface_flag
sia definito come 0 e che nel ciclo while sarà zero per sempre. Dopo l'ottimizzazione, il compilatore lo tratterà come while(true)
sempre, risultando in un ciclo infinito.
Per evitare questo tipo di scenari, dichiariamo il flag come volatile, stiamo dicendo al compilatore che questo valore può essere modificato da un'interfaccia esterna o da un altro modulo di programma, cioè, per favore, non ottimizzarlo. Questo è il caso d'uso per volatile.
Un uso marginale per volatile è il seguente. Supponiamo che tu voglia calcolare la derivata numerica di una funzione f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Il problema è che x+h-x
generalmente non è uguale a h
causa di errori di arrotondamento. Pensaci: quando sottrai numeri molto vicini, perdi molte cifre significative che possono rovinare il calcolo della derivata (pensa 1.00001 - 1). Una possibile soluzione potrebbe essere
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
ma a seconda della piattaforma e degli switch del compilatore, la seconda riga di quella funzione può essere cancellata da un compilatore che ottimizza in modo aggressivo. Quindi scrivi invece
volatile double hh = x + h;
hh -= x;
forzare il compilatore a leggere la posizione della memoria contenente hh, perdendo un'eventuale opportunità di ottimizzazione.
h
o hh
nella formula derivata? Quando hh
viene calcolata, l'ultima formula la utilizza come la prima, senza alcuna differenza. Forse dovrebbe essere (f(x+h) - f(x))/hh
?
h
e hh
è che hh
viene troncata a una potenza negativa di due dall'operazione x + h - x
. In questo caso,x + hh
e x
differiscono esattamente per hh
. Puoi anche prendere la tua formula, darà lo stesso risultato, poiché x + h
e x + hh
sono uguali (è il denominatore che è importante qui).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? senza usare la volatile.
-ffast-math
o equivalente.
Ci sono due usi. Questi sono usati specialmente più spesso nello sviluppo integrato.
Il compilatore non ottimizzerà le funzioni che utilizzano variabili definite con parole chiave volatili
Volatile è usato per accedere a posizioni di memoria esatte in RAM, ROM, ecc ... Questo è usato più spesso per controllare dispositivi mappati in memoria, accedere ai registri della CPU e individuare posizioni di memoria specifiche.
Vedi esempi con elenco di assiemi. Ri: Utilizzo della parola chiave "volatile" C nello sviluppo integrato
Volatile è utile anche quando si desidera forzare il compilatore a non ottimizzare una specifica sequenza di codice (ad es. Per scrivere un micro-benchmark).
Citerò un altro scenario in cui i volatili sono importanti.
Supponiamo di mappare la memoria di un file per un I / O più veloce e che il file possa cambiare dietro le quinte (ad esempio il file non si trova sul disco rigido locale, ma viene invece servito sulla rete da un altro computer).
Se accedi ai dati del file mappato in memoria tramite puntatori a oggetti non volatili (a livello di codice sorgente), il codice generato dal compilatore può recuperare gli stessi dati più volte senza che tu ne sia consapevole.
Se i dati cambiano, il tuo programma potrebbe utilizzare due o più versioni diverse dei dati e entrare in uno stato incoerente. Ciò può portare non solo a comportamenti logicamente errati del programma, ma anche a falle di sicurezza sfruttabili se elabora file o file non attendibili da percorsi non attendibili.
Se ti preoccupi della sicurezza e dovresti, questo è uno scenario importante da considerare.
volatile significa che l'archiviazione può cambiare in qualsiasi momento e può essere modificata, ma qualcosa al di fuori del controllo del programma utente. Ciò significa che se si fa riferimento alla variabile, il programma dovrebbe sempre verificare l'indirizzo fisico (ovvero un input mappato fifo) e non utilizzarlo in modo cache.
Il Wiki dice tutto su volatile
:
E anche il documento del kernel Linux fa un'ottima annotazione su volatile
:
Secondo me, non dovresti aspettarti troppo da volatile
. Per illustrare, guarda l'esempio nella risposta molto votata di Nils Pipenbrinck .
Direi che il suo esempio non è adatto volatile
. volatile
serve solo a:
impedire al compilatore di apportare ottimizzazioni utili e desiderabili . Non ha nulla a che fare con il thread sicuro, l'accesso atomico o persino l'ordine della memoria.
In quell'esempio:
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;
}
il solo gadget->data = data
prima gadget->command = command
è garantito solo nel codice compilato dal compilatore. In fase di esecuzione, il processore può ancora riordinare i dati e l'assegnazione dei comandi, per quanto riguarda l'architettura del processore. L'hardware potrebbe ottenere dati errati (supponiamo che il gadget sia mappato sull'I / O hardware). La barriera di memoria è necessaria tra l'assegnazione dei dati e dei comandi.
volatile
sta degradando le prestazioni senza motivo. Se è sufficiente, ciò dipenderà da altri aspetti del sistema che il programmatore può conoscere più del compilatore. D'altra parte, se un processore garantisce che un'istruzione per scrivere a un determinato indirizzo svuota la cache della CPU ma un compilatore non fornisce alcun modo per svuotare le variabili memorizzate nella cache del registro, la CPU non sa nulla di ciò, svuotare la cache sarebbe inutile.
Nel linguaggio progettato da Dennis Ritchie, ogni accesso a qualsiasi oggetto, ad eccezione degli oggetti automatici il cui indirizzo non era stato preso, si sarebbe comportato come se calcolasse l'indirizzo dell'oggetto e quindi leggesse o scrivesse la memoria a quell'indirizzo. Ciò ha reso il linguaggio molto potente, ma opportunità di ottimizzazione molto limitate.
Mentre sarebbe stato possibile aggiungere un qualificatore che inviterebbe un compilatore ad assumere che un determinato oggetto non venga modificato in modi strani, tale ipotesi sarebbe appropriata per la stragrande maggioranza degli oggetti nei programmi C, e avrebbe è stato poco pratico aggiungere un qualificatore a tutti gli oggetti per i quali tale ipotesi sarebbe appropriata. D'altra parte, alcuni programmi devono utilizzare alcuni oggetti per i quali tale ipotesi non reggerebbe. Per risolvere questo problema, lo standard afferma che i compilatori possono assumere quegli oggetti che non sono stati dichiarativolatile
osservati o modificati in modo che siano al di fuori del controllo del compilatore o che siano al di fuori di una ragionevole comprensione del compilatore.
Poiché varie piattaforme possono avere diversi modi in cui gli oggetti possono essere osservati o modificati al di fuori del controllo di un compilatore, è opportuno che i compilatori di qualità per tali piattaforme differiscano nella loro esatta gestione della volatile
semantica. Sfortunatamente, poiché lo Standard non ha suggerito che i compilatori di qualità destinati alla programmazione di basso livello su una piattaforma dovrebbero gestire volatile
in modo tale da riconoscere tutti gli effetti rilevanti di una particolare operazione di lettura / scrittura su quella piattaforma, molti compilatori non riescono a farlo quindi in modi che rendono più difficile elaborare cose come l'I / O in background in un modo che è efficiente ma non può essere interrotto da "ottimizzazioni" del compilatore.
In termini semplici, indica al compilatore di non eseguire alcuna ottimizzazione su una particolare variabile. Le variabili associate al registro del dispositivo vengono modificate indirettamente dal dispositivo. In questo caso, è necessario utilizzare volatile.
Un volatile può essere modificato dall'esterno del codice compilato (ad esempio, un programma può mappare una variabile volatile su un registro mappato in memoria.) Il compilatore non applicherà determinate ottimizzazioni al codice che gestisce una variabile volatile, ad esempio, ha vinto " caricarlo in un registro senza scriverlo in memoria. Questo è importante quando si tratta di registri hardware.
Come giustamente suggerito da molti qui, l'uso popolare della parola chiave volatile è saltare l'ottimizzazione della variabile volatile.
Il miglior vantaggio che mi viene in mente e che vale la pena menzionare dopo aver letto della volatile è: impedire il rollback della variabile in caso di a longjmp
. Un salto non locale.
Cosa significa questo?
Significa semplicemente che l'ultimo valore verrà conservato dopo aver svolto lo svolgimento dello stack , per tornare ad un precedente frame dello stack; in genere in caso di scenari errati.
Dal momento che sarebbe fuori dalla portata di questa domanda, non entrerò nei dettagli di setjmp/longjmp
qui, ma vale la pena leggerne; e come la funzionalità di volatilità può essere utilizzata per conservare l'ultimo valore.