Warum wird volatile
in C benötigt? Was wird es verwendet? Was wird es tun?
Warum wird volatile
in C benötigt? Was wird es verwendet? Was wird es tun?
Antworten:
Volatile weist den Compiler an, nichts zu optimieren, was mit der flüchtigen Variablen zu tun hat.
Es gibt mindestens drei häufige Gründe für die Verwendung: In allen Situationen kann sich der Wert der Variablen ändern, ohne dass der sichtbare Code eine Aktion ausführt: Wenn Sie eine Schnittstelle zu Hardware herstellen, die den Wert selbst ändert; wenn ein anderer Thread ausgeführt wird, der ebenfalls die Variable verwendet; oder wenn es einen Signalhandler gibt, der den Wert der Variablen ändern kann.
Angenommen, Sie haben ein kleines Stück Hardware, das irgendwo im RAM abgebildet ist und zwei Adressen hat: einen Befehlsport und einen Datenport:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Jetzt möchten Sie einen Befehl senden:
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;
}
Sieht einfach aus, kann aber fehlschlagen, da der Compiler die Reihenfolge ändern kann, in der Daten und Befehle geschrieben werden. Dies würde dazu führen, dass unser kleines Gadget Befehle mit dem vorherigen Datenwert ausgibt. Schauen Sie sich auch die Wartezeit während der Besetztschleife an. Dieser wird optimiert. Der Compiler wird versuchen, klug zu sein, den Wert von isbusy nur einmal zu lesen und dann in eine Endlosschleife zu gehen. Das willst du nicht.
Um dies zu umgehen, müssen Sie das Zeiger-Gadget als flüchtig deklarieren. Auf diese Weise ist der Compiler gezwungen, das zu tun, was Sie geschrieben haben. Es kann die Speicherzuweisungen nicht entfernen, es kann keine Variablen in Registern zwischenspeichern und es kann auch die Reihenfolge der Zuweisungen nicht ändern:
Dies ist die richtige Version:
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 entstand tatsächlich, um die Werte der Variablen nicht automatisch zwischenzuspeichern. Der Compiler wird angewiesen, den Wert dieser Variablen nicht zwischenzuspeichern. Es wird also Code generiert, der bei volatile
jeder Begegnung den Wert der angegebenen Variablen aus dem Hauptspeicher entnimmt. Dieser Mechanismus wird verwendet, da der Wert jederzeit vom Betriebssystem oder einem Interrupt geändert werden kann. Die Verwendung volatile
hilft uns also, jedes Mal neu auf den Wert zuzugreifen.
volatile
war es, Compilern die Optimierung von Code zu ermöglichen und gleichzeitig Programmierern die Möglichkeit zu geben, die Semantik zu erreichen, die ohne solche Optimierungen erreicht werden würde. Die Autoren des Standards erwarteten, dass Qualitätsimplementierungen jede Semantik unterstützen würden, die angesichts ihrer Zielplattformen und Anwendungsfelder nützlich ist, und erwarteten nicht, dass Compiler-Autoren versuchen würden, die Semantik mit der niedrigsten Qualität anzubieten, die dem Standard entspricht und nicht 100% ist dumm (beachten Sie, dass die Autoren des Standards ausdrücklich in der Begründung anerkennen ...
Eine andere Verwendung für volatile
ist Signalhandler. Wenn Sie folgenden Code haben:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Der Compiler darf feststellen, dass der Schleifenkörper die quit
Variable nicht berührt , und die Schleife in eine while (true)
Schleife konvertieren . Auch wenn die quit
Variable im Signalhandler für SIGINT
und gesetzt ist SIGTERM
; Der Compiler kann das nicht wissen.
Wenn die quit
Variable jedoch deklariert ist volatile
, muss der Compiler sie jedes Mal laden, da sie an anderer Stelle geändert werden kann. Dies ist genau das, was Sie in dieser Situation wollen.
quit
, kann der Compiler sie unter der Annahme in eine konstante Schleife optimieren dass es keine Möglichkeit gibt, quit
zwischen Iterationen geändert zu werden. NB: Dies ist nicht unbedingt ein guter Ersatz für die eigentliche threadsichere Programmierung.
volatile
wenn keine oder andere Markierungen vorhanden sind, wird davon ausgegangen, dass nichts außerhalb der Schleife diese Variable ändert, sobald sie in die Schleife eintritt, selbst wenn es sich um eine globale Variable handelt.
extern int global; void fn(void) { while (global != 0) { } }
mit gcc -O3 -S
der resultierenden Assembly-Datei zu kompilieren und sie anzusehen. Auf meinem Computer ist dies der Fall movl global(%rip), %eax
. testl %eax, %eax
;; je .L1
;; .L4: jmp .L4
Das heißt, eine Endlosschleife, wenn die globale nicht Null ist. Versuchen Sie dann hinzuzufügen volatile
und sehen Sie den Unterschied.
volatile
teilt dem Compiler mit, dass Ihre Variable auf andere Weise geändert werden kann als durch den Code, der darauf zugreift. Beispielsweise kann es sich um einen E / A-zugeordneten Speicherort handeln. Wenn dies in solchen Fällen nicht angegeben ist, können einige variable Zugriffe optimiert werden, z. B. kann der Inhalt in einem Register gespeichert werden und der Speicherort wird nicht erneut eingelesen.
Siehe diesen Artikel von Andrei Alexandrescu, " volatile - Multithreaded Programmer's Best Friend "
Das flüchtige Schlüsselwort wurde entwickelt, um Compileroptimierungen zu verhindern, die bei bestimmten asynchronen Ereignissen zu falschem Code führen können. Wenn Sie beispielsweise eine primitive Variable als flüchtig deklarieren , darf der Compiler sie nicht in einem Register zwischenspeichern - eine häufige Optimierung, die katastrophal wäre, wenn diese Variable von mehreren Threads gemeinsam genutzt würde. Die allgemeine Regel lautet also: Wenn Sie Variablen vom primitiven Typ haben, die von mehreren Threads gemeinsam genutzt werden müssen, deklarieren Sie diese Variablen als flüchtig. Mit diesem Schlüsselwort können Sie jedoch noch viel mehr tun: Sie können damit Code abfangen, der nicht threadsicher ist, und dies zum Zeitpunkt der Kompilierung. Dieser Artikel zeigt, wie es gemacht wird; Die Lösung beinhaltet einen einfachen intelligenten Zeiger, der es auch einfach macht, kritische Codeabschnitte zu serialisieren.
Der Artikel gilt sowohl für C
als auch C++
.
Siehe auch den Artikel " C ++ und die Gefahren des Double-Checked Locking " von Scott Meyers und Andrei Alexandrescu:
Wenn Sie sich also mit einigen Speicherorten befassen (z. B. Speicherzuordnungsports oder Speicher, auf den ISRs [Interrupt Service Routines] verweisen), müssen einige Optimierungen ausgesetzt werden. flüchtig gibt es, um eine spezielle Behandlung für solche Orte festzulegen, insbesondere: (1) der Inhalt einer flüchtigen Variablen ist "instabil" (kann sich durch dem Compiler unbekannte Mittel ändern), (2) alle Schreibvorgänge in flüchtige Daten sind "beobachtbar", so dass sie muss religiös ausgeführt werden, und (3) alle Operationen an flüchtigen Daten werden in der Reihenfolge ausgeführt, in der sie im Quellcode erscheinen. Die ersten beiden Regeln gewährleisten das richtige Lesen und Schreiben. Das letzte ermöglicht die Implementierung von E / A-Protokollen, die Eingabe und Ausgabe mischen. Dies ist informell, was die volatilen Garantien von C und C ++ garantieren.
volatile
garantiert keine Atomizität.
Meine einfache Erklärung lautet:
In einigen Szenarien optimiert der Compiler basierend auf der Logik oder dem Code Variablen, von denen er glaubt, dass sie sich nicht ändern. Das volatile
Schlüsselwort verhindert, dass eine Variable optimiert wird.
Zum Beispiel:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Aus dem obigen Code geht der Compiler möglicherweise davon aus, dass er usb_interface_flag
als 0 definiert ist und dass er in der while-Schleife für immer Null ist. Nach der Optimierung wird es vom Compiler wie immer behandelt while(true)
, was zu einer Endlosschleife führt.
Um diese Art von Szenarien zu vermeiden, deklarieren wir das Flag als flüchtig. Wir teilen dem Compiler mit, dass dieser Wert durch eine externe Schnittstelle oder ein anderes Programmmodul geändert werden kann, dh bitte nicht optimieren. Das ist der Anwendungsfall für volatile.
Eine marginale Verwendung für flüchtige Stoffe ist die folgende. Angenommen, Sie möchten die numerische Ableitung einer Funktion berechnen f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Das Problem ist, dass aufgrund von Rundungsfehlern im x+h-x
Allgemeinen nicht gleich h
ist. Denken Sie darüber nach: Wenn Sie sehr nahe Zahlen subtrahieren, verlieren Sie viele signifikante Ziffern, die die Berechnung der Ableitung ruinieren können (denken Sie an 1.00001 - 1). Eine mögliche Problemumgehung könnte sein
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
Abhängig von Ihrer Plattform und den Compiler-Switches kann die zweite Zeile dieser Funktion von einem aggressiv optimierenden Compiler gelöscht werden. Also schreibst du stattdessen
volatile double hh = x + h;
hh -= x;
um den Compiler zu zwingen, den Speicherort zu lesen, der hh enthält, verfällt eine mögliche Optimierungsmöglichkeit.
h
oder der hh
abgeleiteten Formel? Wenn hh
berechnet wird, verwendet die letzte Formel sie wie die erste, ohne Unterschied. Vielleicht sollte es sein (f(x+h) - f(x))/hh
?
h
und hh
besteht darin, dass hh
durch die Operation eine negative Zweierpotenz abgeschnitten wird x + h - x
. In diesem Fall x + hh
und x
unterscheiden sich genau um hh
. Sie können auch Ihre Formel nehmen, sie ergibt das gleiche Ergebnis, da x + h
und x + hh
gleich sind (es ist der Nenner, der hier wichtig ist).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? ohne das flüchtige zu verwenden.
-ffast-math
oder gleichwertig angegeben wird.
Es gibt zwei Verwendungszwecke. Diese werden besonders häufig in der Embedded-Entwicklung eingesetzt.
Der Compiler optimiert nicht die Funktionen, die Variablen verwenden, die mit dem Schlüsselwort volatile definiert sind
Volatile wird verwendet, um auf genaue Speicherorte im RAM, ROM usw. zuzugreifen. Dies wird häufiger verwendet, um speicherabgebildete Geräte zu steuern, auf CPU-Register zuzugreifen und bestimmte Speicherorte zu lokalisieren.
Siehe Beispiele mit Baugruppenliste. Betreff: Verwendung des Schlüsselworts "volatile" C in der eingebetteten Entwicklung
Volatile ist auch nützlich, wenn Sie den Compiler zwingen möchten, eine bestimmte Codesequenz nicht zu optimieren (z. B. zum Schreiben eines Mikro-Benchmarks).
Ich werde ein anderes Szenario erwähnen, in dem flüchtige Stoffe wichtig sind.
Angenommen, Sie ordnen eine Datei für eine schnellere E / A zu und diese Datei kann sich hinter den Kulissen ändern (z. B. befindet sich die Datei nicht auf Ihrer lokalen Festplatte, sondern wird stattdessen von einem anderen Computer über das Netzwerk bereitgestellt).
Wenn Sie über Zeiger auf nichtflüchtige Objekte (auf Quellcodeebene) auf die Daten der Speicherzuordnungsdatei zugreifen, kann der vom Compiler generierte Code dieselben Daten mehrmals abrufen, ohne dass Sie sich dessen bewusst sind.
Wenn sich diese Daten ändern, verwendet Ihr Programm möglicherweise zwei oder mehr verschiedene Versionen der Daten und gerät in einen inkonsistenten Zustand. Dies kann nicht nur zu einem logisch inkorrekten Verhalten des Programms führen, sondern auch zu ausnutzbaren Sicherheitslücken, wenn es nicht vertrauenswürdige Dateien oder Dateien von nicht vertrauenswürdigen Speicherorten verarbeitet.
Wenn Sie Wert auf Sicherheit legen und dies sollten, ist dies ein wichtiges Szenario.
flüchtig bedeutet, dass sich der Speicher wahrscheinlich jederzeit ändert und geändert wird, jedoch außerhalb der Kontrolle des Benutzerprogramms. Dies bedeutet, dass das Programm, wenn Sie auf die Variable verweisen, immer die physikalische Adresse (dh eine zugeordnete Eingabe-Fifo) überprüfen und nicht zwischengespeichert verwenden sollte.
Das Wiki sagt alles über volatile
:
Und das Dokument des Linux-Kernels bietet auch eine hervorragende Notation über volatile
:
Meiner Meinung nach sollte man nicht zu viel erwarten volatile
. Schauen Sie sich zur Veranschaulichung das Beispiel in Nils Pipenbrincks hochgewählter Antwort an .
Ich würde sagen, sein Beispiel ist nicht geeignet für volatile
. volatile
wird nur verwendet, um: den Compiler daran zu
hindern, nützliche und wünschenswerte Optimierungen vorzunehmen . Es geht nicht um den Thread-sicheren, atomaren Zugriff oder sogar die Speicherreihenfolge.
In diesem Beispiel:
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;
}
Das gadget->data = data
Vorherige gadget->command = command
wird nur im kompilierten Code vom Compiler garantiert. Zur Laufzeit ordnet der Prozessor möglicherweise noch die Daten- und Befehlszuweisung in Bezug auf die Prozessorarchitektur neu an. Die Hardware könnte die falschen Daten erhalten (angenommen, das Gadget ist der Hardware-E / A zugeordnet). Die Speicherbarriere wird zwischen Daten- und Befehlszuweisung benötigt.
volatile
als würde die Leistung ohne Grund beeinträchtigt. Ob dies ausreicht, hängt von anderen Aspekten des Systems ab, über die der Programmierer möglicherweise mehr weiß als der Compiler. Wenn andererseits ein Prozessor garantiert, dass eine Anweisung zum Schreiben an eine bestimmte Adresse den CPU-Cache leert, ein Compiler jedoch keine Möglichkeit bietet, registrierungsgespeicherte Variablen zu leeren, von denen die CPU nichts weiß, wäre das Leeren des Caches nutzlos.
In der von Dennis Ritchie entworfenen Sprache verhält sich jeder Zugriff auf ein Objekt außer automatischen Objekten, deren Adresse nicht vergeben wurde, so, als würde er die Adresse des Objekts berechnen und dann den Speicher an dieser Adresse lesen oder schreiben. Dies machte die Sprache sehr leistungsfähig, aber die Optimierungsmöglichkeiten stark eingeschränkt.
Während es möglich gewesen wäre, ein Qualifikationsmerkmal hinzuzufügen, das einen Compiler auffordert, anzunehmen, dass ein bestimmtes Objekt nicht auf seltsame Weise geändert wird, wäre eine solche Annahme für die überwiegende Mehrheit der Objekte in C-Programmen angemessen, und dies wäre der Fall Es war unpraktisch, allen Objekten, für die eine solche Annahme angemessen wäre, ein Qualifikationsmerkmal hinzuzufügen. Andererseits müssen einige Programme einige Objekte verwenden, für die eine solche Annahme nicht gelten würde. Um dieses Problem zu beheben, geht der Standard davon aus, dass Compiler davon ausgehen können, dass bei Objekten, die nicht deklariert volatile
sind, ihr Wert nicht auf eine Weise beobachtet oder geändert wird, die außerhalb der Kontrolle des Compilers liegt oder außerhalb des Verständnisses eines vernünftigen Compilers liegt.
Da verschiedene Plattformen unterschiedliche Möglichkeiten haben, Objekte außerhalb der Kontrolle eines Compilers zu beobachten oder zu ändern, sollten sich Qualitätscompiler für diese Plattformen in ihrer genauen Handhabung der volatile
Semantik unterscheiden. Da der Standard nicht vorschlug, dass Qualitätscompiler, die für die Programmierung auf niedriger Ebene auf einer Plattform vorgesehen sind, so behandelt werden sollten volatile
, dass alle relevanten Auswirkungen eines bestimmten Lese- / Schreibvorgangs auf dieser Plattform erkannt werden, sind viele Compiler leider nicht in der Lage, dies zu tun Auf eine Weise, die es schwieriger macht, Dinge wie Hintergrund-E / A auf eine Weise zu verarbeiten, die effizient ist, aber nicht durch Compiler- "Optimierungen" unterbrochen werden kann.
In einfachen Worten, es weist den Compiler an, keine Optimierung für eine bestimmte Variable vorzunehmen. Variablen, die dem Geräteregister zugeordnet sind, werden vom Gerät indirekt geändert. In diesem Fall muss flüchtig verwendet werden.
Ein flüchtiger Bestandteil kann von außerhalb des kompilierten Codes geändert werden (z. B. kann ein Programm eine flüchtige Variable einem Speicherabbildungsregister zuordnen.) Der Compiler wendet bestimmte Optimierungen nicht auf Code an, der eine flüchtige Variable verarbeitet - zum Beispiel hat er gewonnen. t Laden Sie es in ein Register, ohne es in den Speicher zu schreiben. Dies ist wichtig beim Umgang mit Hardwareregistern.
Wie von vielen hier zu Recht vorgeschlagen, besteht die beliebte Verwendung des flüchtigen Schlüsselworts darin, die Optimierung der flüchtigen Variablen zu überspringen.
Der beste Vorteil , dass in den Sinn kommt, und erwähnenswert , nach etwa flüchtigen Lesen ist - zu verhindern ein Zurückrollen der Variablen im Falle eines longjmp
. Ein nicht lokaler Sprung.
Was bedeutet das?
Dies bedeutet einfach, dass der letzte Wert nach dem Abwickeln des Stapels beibehalten wird, um zu einem vorherigen Stapelrahmen zurückzukehren. in der Regel im Falle eines fehlerhaften Szenarios.
Da dies nicht in den Rahmen dieser Frage fällt, gehe ich hier nicht auf Details setjmp/longjmp
ein, aber es lohnt sich, darüber zu lesen. und wie die Volatilitätsfunktion verwendet werden kann, um den letzten Wert beizubehalten.