Perché è necessario volatile in C?


Risposte:


425

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;
    }

46
Personalmente, preferirei che la dimensione intera sia esplicita, ad esempio int8 / int16 / int32 quando parlo con l'hardware. Bella risposta però;)
tonylo

22
sì, dovresti dichiarare le cose con una dimensione del registro fissa, ma ehi - è solo un esempio.
Nils Pipenbrinck,

69
Volatile è necessario anche nel codice thread quando si gioca con dati non protetti da concorrenza. E sì, ci sono periodi validi per farlo, ad esempio puoi scrivere una coda di messaggi circolari sicuri senza thread senza la protezione esplicita della concorrenza, ma avrà bisogno di volatili.
Gordon Wrigley,

14
Leggi la specifica C più difficile. Volatile ha definito il comportamento solo sull'I / O del dispositivo mappato in memoria o sulla memoria toccata da una funzione di interruzione asincrona. Non dice nulla sul threading e un compilatore che ottimizza l'accesso alla memoria toccato da più thread è conforme.
effimero

17
@tolomea: completamente sbagliato. 17 persone tristi non lo sanno. volatile non è una barriera di memoria. è legato solo all'evitare l' eliminazione del codice durante l'ottimizzazione basata sul presupposto di effetti collaterali non visibili .
v.oddou,

188

volatilein 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 volatilevariabile 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 volatileci aiuterà ad accedere nuovamente al valore ogni volta.


È venuto all'esistenza? "Volatile" non era stato inizialmente preso in prestito dal C ++? Beh, mi sembra di ricordare ...
syntaxerror

Questo non è volatile, ma proibisce anche un riordino se specificato come volatile.
FaceBro

4
@FaceBro: lo scopo volatileera 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 ...
supercat

1
... che è possibile che un'implementazione sia conforme senza essere di buona qualità per essere effettivamente adatta a qualsiasi scopo, ma non hanno ritenuto necessario prevenirlo).
supercat

1
@syntaxerror come può essere preso in prestito dal C ++ quando il C aveva più di un decennio più vecchio del C ++ (sia nelle prime versioni che nei primi standard)?
phuclv,

178

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 quitvariabile e converte il ciclo in un while (true)ciclo. Anche se la quitvariabile è impostata sul gestore del segnale per SIGINTeSIGTERM ; il compilatore non ha modo di saperlo.

Tuttavia, se la quitvariabile viene dichiarata volatile, il compilatore è costretto a caricarla ogni volta, perché può essere modificata altrove. Questo è esattamente quello che vuoi in questa situazione.


quando dici "il compilatore è costretto a caricarlo ogni volta, è come quando il compilatore decide di ottimizzare una determinata variabile e non dichiariamo la variabile come volatile, in fase di esecuzione quella determinata variabile viene caricata nei registri della CPU non in memoria ?
Amit Singh Tomar,

1
@AmitSinghTomar Significa cosa dice: ogni volta che il codice controlla il valore, viene ricaricato. Altrimenti, al compilatore è consentito assumere che funzioni che non prendono un riferimento alla variabile non possano modificarlo, quindi supponendo che CesarB intendesse che il ciclo sopra non si imposta quit, il compilatore può ottimizzarlo in un ciclo costante, assumendo che non è quitpossibile cambiare tra le iterazioni. NB: Questo non è necessariamente un buon sostituto per l'effettiva programmazione thread-safe.
underscore_d

se quit è una variabile globale, il compilatore non deve ottimizzare il ciclo while, giusto?
Pierre G.

2
@PierreG. No, il compilatore può sempre presumere che il codice sia a thread singolo, se non diversamente specificato. Cioè, in assenza di volatileo altri marcatori, supporrà che nulla al di fuori del ciclo modifichi quella variabile una volta che entra nel ciclo, anche se è una variabile globale.
CesarB,

1
@PierreG. Sì, provare per esempio la compilazione extern int global; void fn(void) { while (global != 0) { } }con gcc -O3 -Se 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 volatilee vedi la differenza.
CesarB,

60

volatiledice 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.


30

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 Ce 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 ++.


Lo standard specifica se una lettura è considerata "comportamento osservabile" se il valore non viene mai utilizzato? La mia impressione è che dovrebbe essere, ma quando ho affermato che era altrove qualcuno mi ha sfidato per una citazione. Mi sembra che su qualsiasi piattaforma in cui una lettura di una variabile volatile possa avere in qualche modo effetti possibili, dovrebbe essere richiesto un compilatore per generare codice che esegua ogni lettura indicata con precisione una sola volta; senza tale requisito, sarebbe difficile scrivere codice che generasse una sequenza prevedibile di letture.
supercat,

@supercat: Secondo il primo articolo, "Se si utilizza il modificatore volatile su una variabile, il compilatore non memorizzerà nella cache quella variabile nei registri - ogni accesso colpirà l'effettiva posizione di memoria di quella variabile." Inoltre, nella sezione §6.7.3.6 dello standard c99 si dice: "Un oggetto che ha un tipo qualificato volatile può essere modificato in modi sconosciuti all'implementazione o avere altri effetti collaterali sconosciuti". Ciò implica inoltre che le variabili volatili non possono essere memorizzate nella cache nei registri e che tutte le letture e le scritture devono essere eseguite in ordine relativo ai punti di sequenza, che in effetti sono osservabili.
Robert S. Barnes,

Quest'ultimo articolo afferma infatti esplicitamente che le letture sono effetti collaterali. Il primo indica che le letture non possono essere eseguite in sequenza, ma non sembrano precludere la possibilità che vengano eluse del tutto.
supercat

"il compilatore non è autorizzato a memorizzarlo nella cache in un registro" - La maggior parte delle architetture RISC sono macchine del registro, quindi qualsiasi lettura-modifica-scrittura deve memorizzare nella cache l'oggetto in un registro. volatilenon garantisce l'atomicità.
troppo onesto per questo sito

1
@Olaf: caricare qualcosa in un registro non è la stessa cosa della memorizzazione nella cache. La memorizzazione nella cache influirebbe sul numero di carichi o negozi o sulla loro tempistica.
supercat

28

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 volatileparola 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_flagsia 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.


19

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-xgeneralmente non è uguale a hcausa 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.


Qual è la differenza tra l'utilizzo ho hhnella formula derivata? Quando hhviene calcolata, l'ultima formula la utilizza come la prima, senza alcuna differenza. Forse dovrebbe essere (f(x+h) - f(x))/hh?
Sergey Zhukov,

2
La differenza tra he hhè che hhviene troncata a una potenza negativa di due dall'operazione x + h - x. In questo caso,x + hh e xdifferiscono esattamente per hh. Puoi anche prendere la tua formula, darà lo stesso risultato, poiché x + he x + hhsono uguali (è il denominatore che è importante qui).
Alexandre C.,

3
Non sarebbe un modo più leggibile per scrivere questo x1=x+h; d = (f(x1)-f(x))/(x1-x)? senza usare la volatile.
Sergey Zhukov,

Qualche riferimento che un compilatore può cancellare quella seconda riga della funzione?
CoffeeTableEspresso

@CoffeeTableEspresso: No, scusa. Più so di virgola mobile, più credo che il compilatore sia autorizzato a ottimizzarlo solo se esplicitamente detto, con -ffast-matho equivalente.
Alexandre C.,

11

Ci sono due usi. Questi sono usati specialmente più spesso nello sviluppo integrato.

  1. Il compilatore non ottimizzerà le funzioni che utilizzano variabili definite con parole chiave volatili

  2. 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


"Il compilatore non ottimizzerà le funzioni che utilizzano variabili definite con una parola chiave volatile", questo è chiaramente sbagliato.
troppo onesto per questo sito

10

Volatile è utile anche quando si desidera forzare il compilatore a non ottimizzare una specifica sequenza di codice (ad es. Per scrivere un micro-benchmark).


10

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.


7

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.


Nessun compilatore diventa volatile per indicare "indirizzo fisico nella RAM" o "bypassare la cache".
curiousguy,


5

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. volatileserve 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 = dataprima 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.


2
Direi che volatile è usato per impedire al compilatore di fare ottimizzazioni che sarebbero normalmente utili e desiderabili. Come scritto, sembra che volatilesta 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.
supercat,

5

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 volatilesemantica. Sfortunatamente, poiché lo Standard non ha suggerito che i compilatori di qualità destinati alla programmazione di basso livello su una piattaforma dovrebbero gestire volatilein 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.


5

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.


1
C'è qualcosa di nuovo in questa risposta che non è stato menzionato prima?
slfan,

3

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.


0

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/longjmpqui, ma vale la pena leggerne; e come la funzionalità di volatilità può essere utilizzata per conservare l'ultimo valore.


-2

non consente al compilatore di modificare automaticamente i valori delle variabili. una variabile volatile è per uso dinamico.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.