Perché è necessaria la volatilità in C?

Perché è necessaria la volatile in C? A cosa serve? Cosa farà?

Volatile dice al compilatore di non ottimizzare tutto ciò che ha a che fare con la variabile volatile.

C’è solo un motivo per usarlo: quando si interfaccia con l’hardware.

Diciamo che hai un piccolo pezzo di hardware che è mappato nella RAM da qualche parte e che ha due indirizzi: una porta di comando e una porta dati:

 typedef struct { int command; int data; int isbusy; } MyHardwareGadget; 

Ora vuoi inviare un 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 vengono scritti i dati e i comandi. Ciò farebbe sì che il nostro piccolo gadget emettesse comandi con il precedente valore di dati. Guarda anche l’attesa mentre il ciclo occupato. Quello sarà ottimizzato. Il compilatore proverà ad 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ò modificare 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; } 

Un altro uso per volatile sono i gestori di segnale. Se hai un codice come questo:

 quit = 0; while (!quit) { /* very small loop which is completely visible to the compiler */ } 

Il compilatore può notare che il corpo del loop non tocca la variabile quit e converte il loop in un loop while (true) . Anche se la variabile quit è impostata sul gestore di segnale per SIGINT e SIGTERM ; il compilatore non ha modo di saperlo.

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

volatile in C è venuto in realtà allo scopo di non memorizzare automaticamente i valori della variabile. Dirà alla macchina di non memorizzare nella cache il valore di questa variabile. Quindi prenderà il valore della variabile volatile data dalla memoria principale ogni volta che lo incontra. Questo meccanismo viene utilizzato perché in qualsiasi momento il valore può essere modificato dal sistema operativo o da qualsiasi interrupt. Quindi usare volatile ci aiuterà ad accedere ogni volta al valore di nuovo.

volatile dice al compilatore che la tua variabile può essere cambiata con altri mezzi, rispetto al codice che la sta accedendo. ad esempio, potrebbe essere una posizione di memoria mappata I / O. Se questo non è specificato in questi casi, alcuni accessi variabili possono essere ottimizzati, ad esempio, il contenuto può essere contenuto in un registro e la posizione di memoria non può essere nuovamente riletta.

Leggi questo articolo di Andrei Alexandrescu, ” volatile – Il migliore amico del programmatore multithread ”

La parola chiave volatile è stata concepita per impedire ottimizzazioni del compilatore che potrebbero rendere il codice errato in presenza di determinati eventi asincroni. Ad esempio, se dichiari una variabile primitiva come volatile , il compilatore non è autorizzato a memorizzarlo in un registro – una ottimizzazione comune che sarebbe disastrosa se quella 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 usarla per catturare il codice che non è thread-safe, e puoi farlo in fase di compilazione. Questo articolo mostra come è fatto; la soluzione include un semplice puntatore intelligente che semplifica anche la serializzazione delle sezioni critiche del codice.

L’articolo si applica a C e C++ .

Vedi anche l’articolo ” C ++ e i pericoli del blocco a doppio controllo ” di Scott Meyers e Andrei Alexandrescu:

Pertanto, quando si gestiscono alcune posizioni di memoria (ad esempio, porte mappate in memoria o memoria a cui fanno riferimento gli ISR ​​[Interrupt Service Routine]), alcune ottimizzazioni devono essere sospese. esiste volatile per specificare un trattamento speciale per tali luoghi, in particolare: (1) il contenuto di una variabile volatile è “instabile” (può cambiare per mezzo sconosciuto al compilatore), (2) tutte le scritture su dati volatili sono “osservabili” in modo che deve essere eseguito religiosamente, e (3) tutte le operazioni su dati volatili vengono eseguite nella sequenza in cui compaiono nel codice sorgente. Le prime due regole garantiscono una corretta lettura e scrittura. L’ultimo consente l’implementazione di protocolli I / O che combinano input e output. Questo è informalmente ciò che le garanzie volatili di C e C ++.

La mia semplice spiegazione è:

In alcuni scenari, basati sulla logica o sul codice, il compilatore eseguirà l’ottimizzazione delle variabili che pensa non cambino. La parola chiave volatile 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 precedente, il compilatore potrebbe 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) tutto il tempo, 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, ovvero, per favore, non ottimizzarlo. Questo è il caso d’uso per volatile.

Un utilizzo marginale per volatile è il seguente. Supponi di voler 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+hx generalmente non è uguale a h causa di errori di arrotondamento. Pensaci: quando si sumno numeri molto vicini, si perdono molte cifre significative che possono rovinare il calcolo della derivata (si pensi a 1.00001-1). Una soluzione ansible 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 potrebbe essere cancellata da un compilatore aggressivo. Quindi scrivi invece

  volatile double hh = x + h; hh -= x; 

per forzare il compilatore a leggere la posizione di memoria contenente hh, perdendo un’eventuale opportunità di ottimizzazione.

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

  1. Il compilatore non ottimizzerà le funzioni che utilizzano variabili definite con una parola chiave volatile

  2. Volatile viene utilizzato per accedere a posizioni di memoria esatte in RAM, ROM, ecc. Viene utilizzato più spesso per controllare i dispositivi mappati in memoria, accedere ai registri della CPU e individuare posizioni di memoria specifiche.

Vedi esempi con la lista di assemblaggio. Ri: Uso della parola chiave “volatile” C nello sviluppo integrato

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

Menzionerò un altro scenario in cui i volatili sono importanti.

Supponiamo che tu ricordi la mapping 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 tuo disco rigido locale, ma viene invece servito sulla rete da un altro computer).

Se si accede 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 se ne accorga.

Se questi dati cambiano, il tuo programma potrebbe utilizzare due o più versioni diverse dei dati e entrare in uno stato incoerente. Ciò può comportare non solo un comportamento logico scorretto del programma ma anche lacune di sicurezza sfruttabili in esso se esso elabora file o file non attendibili da posizioni non attendibili.

Se ti interessa la sicurezza, e dovresti, questo è uno scenario importante da considerare.

volatile significa che è probabile che lo spazio di archiviazione cambi in qualsiasi momento e venga modificato, ma al di fuori del controllo del programma utente. Ciò significa che se si fa riferimento alla variabile, il programma dovrebbe sempre controllare l’indirizzo fisico (cioè un ingresso mappato fifo) e non usarlo in modo cache.

Il Wiki dice tutto sulla volatile :

  • volatile (programmazione per computer)

E il documento del kernel di Linux fa anche un’ottima notazione su volatile :

  • Perché la class di tipo “volatile” non dovrebbe essere utilizzata

Un volatile può essere modificato dall’esterno del codice compilato (ad esempio, un programma può mappare una variabile volatile in un registro mappato in memoria.) Il compilatore non applicherà alcune ottimizzazioni al codice che gestisce una variabile volatile – ad esempio, ha vinto ‘ t caricarlo in un registro senza scriverlo in memoria. Questo è importante quando si ha a che fare con i registri hardware.

A mio parere, non dovresti aspettarti troppo da volatile . Per illustrare, guarda l’esempio nella risposta altamente votata di Nils Pipenbrinck .

Direi che il suo esempio non è adatto alla volatile . volatile è usato solo per: impedire al compilatore di realizzare ottimizzazioni utili e desiderabili . Non si tratta di thread safe, accesso atomico o ordine di memoria.

In questo 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 gadget->data = data prima di gadget->command = command solo il gadget->command = command è garantito solo dal codice compilato dal compilatore. Al momento dell’esecuzione, il processore può ancora riordinare i dati e l’assegnazione dei comandi, in relazione all’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 i dati e l’assegnazione del comando.

Nel linguaggio progettato da Dennis Ritchie, ogni accesso a qualsiasi object, diverso da oggetti automatici il cui indirizzo non era stato preso, si comporterebbe come se calcolasse l’indirizzo dell’object e quindi leggesse o scrivesse la memoria a quell’indirizzo. Ciò ha reso il linguaggio molto potente, ma ha limitato le opportunità di ottimizzazione.

Mentre sarebbe stato ansible aggiungere un qualificatore che inviterebbe un compilatore ad assumere che un particolare object non sarebbe stato 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 sarebbe valida. Per risolvere questo problema, lo standard dice che i compilatori possono presumere che gli oggetti che non sono dichiarati volatile non avranno il loro valore osservato o modificato in modi che sono al di fuori del controllo del compilatore, o sarebbero al di fuori della ragionevole comprensione del compilatore.

Poiché varie piattaforms 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 piattaforms differiscano nella loro esatta gestione della semantica volatile . Sfortunatamente, poiché lo standard non è riuscito a suggerire che i compilatori di qualità destinati alla programmazione di basso livello su una piattaforma dovrebbero gestire volatile in modo da riconoscere tutti gli effetti rilevanti di una particolare operazione di lettura / scrittura su quella piattaforma, molti compilatori non sono in grado di farlo in modi che rendono più difficile elaborare cose come I / O in background in un modo che sia efficiente ma che non possa essere interrotto dalle “ottimizzazioni” del compilatore.

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