Warum gibt es flüchtige?


222

Was macht das? volatile Schlüsselwort? Welches Problem löst es in C ++?

In meinem Fall habe ich es nie wissentlich gebraucht.


Hier ist eine interessante Diskussion über volatile in Bezug auf das Singleton-Muster: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
chessguy

3
Es gibt eine faszinierende Technik, mit der Ihr Compiler mögliche Rennbedingungen erkennt, die stark vom flüchtigen Schlüsselwort abhängen. Weitere Informationen finden Sie unter http://www.ddj.com/cpp/184403766 .
Neno Ganchev

Dies ist eine nette Ressource mit einem Beispiel dafür, wann volatilesie effektiv genutzt werden kann, zusammengestellt in hübschen Laienbegriffen. Link: publications.gbdirect.co.uk/c_book/chapter8/…
Optimierter Codierer

Ich benutze es für
sperrenfreien

Für mich volatilenützlicher als friendSchlüsselwort.
Acegs

Antworten:


268

volatile wird benötigt, wenn Sie von einer Stelle im Speicher lesen, auf die beispielsweise ein vollständig separater Prozess / Gerät / was auch immer geschrieben werden kann.

Ich habe mit Dual-Port-RAM in einem Multiprozessorsystem in Straight C gearbeitet. Wir haben einen hardwareverwalteten 16-Bit-Wert als Semaphor verwendet, um zu wissen, wann der andere fertig war. Im Wesentlichen haben wir dies getan:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

Ohne volatilesieht der Optimierer die Schleife als nutzlos an (Der Typ setzt nie den Wert! Er ist verrückt, wird diesen Code los!) Und mein Code würde fortfahren, ohne das Semaphor erworben zu haben, was später Probleme verursacht.


Was würde in diesem Fall passieren, wenn uint16_t* volatile semPtrstattdessen geschrieben würde? Dies sollte den Zeiger als flüchtig markieren (anstelle des Wertes, auf den gezeigt wird), damit Überprüfungen des Zeigers selbst, z. B. semPtr == SOME_ADDRmöglicherweise nicht optimiert werden. Dies impliziert jedoch auch wieder einen volatilen Spitzenwert. Nein?
Zyl

@ Zyl Nein, das tut es nicht. In der Praxis ist es wahrscheinlich, was Sie vorschlagen, was passieren wird. Theoretisch könnte man jedoch einen Compiler erhalten, der den Zugriff auf Werte optimiert, da entschieden wurde, dass keiner dieser Werte jemals geändert wird. Und wenn Sie flüchtig meinten, auf den Wert und nicht auf den Zeiger anzuwenden, wären Sie geschraubt. Auch dies ist unwahrscheinlich, aber es ist besser, Fehler zu machen, als das Verhalten auszunutzen, das heute funktioniert.
Iheanyi

1
@ Doug T. Eine bessere Erklärung ist dies
machineaddict

3
@curiousguy hat es nicht falsch entschieden. Auf der Grundlage der gegebenen Informationen wurde der korrekte Abzug vorgenommen. Wenn Sie etwas Flüchtiges nicht markieren, kann der Compiler davon ausgehen, dass es nicht flüchtig ist . Das macht der Compiler bei der Optimierung von Code. Wenn es mehr Informationen gibt, nämlich dass diese Daten tatsächlich flüchtig sind, liegt es in der Verantwortung des Programmierers, diese Informationen bereitzustellen. Was Sie von einem fehlerhaften Compiler behaupten, ist wirklich nur schlechte Programmierung.
Iheanyi

1
@curiousguy nein, nur weil das flüchtige Schlüsselwort einmal erscheint, heißt das nicht, dass plötzlich alles flüchtig wird. Ich habe ein Szenario angegeben, in dem der Compiler das Richtige tut und ein Ergebnis erzielt, das den Erwartungen des Programmierers widerspricht. Genau wie die "ärgerlichste Analyse" nicht das Zeichen des Compilerfehlers ist, ist dies auch hier nicht der Fall.
Iheanyi

82

volatilewird benötigt, wenn eingebettete Systeme oder Gerätetreiber entwickelt werden, bei denen Sie ein speicherabgebildetes Hardwaregerät lesen oder schreiben müssen. Der Inhalt eines bestimmten Geräteregisters kann sich jederzeit ändern. Sie benötigen daher das volatileSchlüsselwort, um sicherzustellen, dass solche Zugriffe nicht vom Compiler optimiert werden.


9
Dies gilt nicht nur für eingebettete Systeme, sondern für die Entwicklung aller Gerätetreiber.
Mladen Janković

Das einzige Mal, dass ich es jemals auf einem 8-Bit-ISA-Bus brauchte, bei dem Sie dieselbe Adresse zweimal gelesen haben - der Compiler hatte einen Fehler und ignorierte ihn (frühes Zortech c ++)
Martin Beckett

Flüchtig ist für die Steuerung externer Geräte sehr selten ausreichend. Seine Semantik ist für modernes MMIO falsch: Sie müssen zu viele Objekte flüchtig machen und es schadet der Optimierung. Das moderne MMIO verhält sich jedoch wie normaler Speicher, bis ein Flag gesetzt wird, sodass keine flüchtigen Verbindungen erforderlich sind. Viele Fahrer verwenden nie flüchtig.
Neugieriger

69

Einige Prozessoren haben Gleitkommaregister mit einer Genauigkeit von mehr als 64 Bit (z. B. 32-Bit x86 ohne SSE, siehe Peters Kommentar). Auf diese Weise erhalten Sie eine Antwort mit höherer Genauigkeit, wenn Sie mehrere Operationen mit Zahlen mit doppelter Genauigkeit ausführen, als wenn Sie jedes Zwischenergebnis auf 64 Bit kürzen würden.

Dies ist normalerweise großartig, bedeutet jedoch, dass Sie je nachdem, wie der Compiler Register zugewiesen und Optimierungen vorgenommen hat, unterschiedliche Ergebnisse für genau dieselben Operationen mit genau denselben Eingaben erzielen. Wenn Sie Konsistenz benötigen, können Sie mithilfe des Schlüsselworts volatile erzwingen, dass jede Operation in den Speicher zurückkehrt.

Dies ist auch nützlich für einige Algorithmen, die keinen algebraischen Sinn ergeben, aber Gleitkommafehler reduzieren, wie z. B. die Kahan-Summierung. Algebraisch gesehen ist es ein NOP, daher wird es oft falsch optimiert, es sei denn, einige Zwischenvariablen sind flüchtig.


5
Wenn Sie numerische Ableitungen berechnen, ist es auch nützlich, um sicherzustellen, dass x + h - x == h hh = x + h - x als flüchtig definiert, damit ein richtiges Delta berechnet werden kann.
Alexandre C.

5
+1, tatsächlich gab es meiner Erfahrung nach einen Fall, in dem Gleitkommaberechnungen in Debug und Release zu unterschiedlichen Ergebnissen führten, sodass für eine Konfiguration geschriebene Komponententests für eine andere fehlschlugen. Wir haben es gelöst, indem wir eine Gleitkommavariable volatile doubleanstelle von nur deklariert haben double, um sicherzustellen, dass sie von der FPU-Genauigkeit auf die 64-Bit-Genauigkeit (RAM) gekürzt wird, bevor weitere Berechnungen fortgesetzt werden. Die Ergebnisse waren aufgrund einer weiteren Übertreibung des Gleitkommafehlers wesentlich unterschiedlich.
Serge Rogatch

Ihre Definition von "modern" ist ein bisschen falsch. Davon ist nur 32-Bit-x86-Code betroffen, der SSE / SSE2 vermeidet, und er war noch vor 10 Jahren nicht "modern". MIPS / ARM / POWER verfügen alle über 64-Bit-Hardwareregister, ebenso wie x86 mit SSE2. C ++ - Implementierungen x86-64 verwenden immer SSE2, und Compiler haben Optionen g++ -mfpmath=sse, die sie auch für 32-Bit-x86 verwenden möchten. Sie können das gcc -ffloat-storeRunden überall erzwingen , auch wenn Sie x87 verwenden, oder Sie können die x87-Genauigkeit auf 53-Bit-Mantisse einstellen : randomascii.wordpress.com/2012/03/21/… .
Peter Cordes

Aber immer noch eine gute Antwort: Bei veraltetem x87-Code-Gen können Sie das volatileRunden an einigen bestimmten Stellen erzwingen, ohne die Vorteile überall zu verlieren.
Peter Cordes

1
Oder verwechsle ich ungenau mit inkonsistent?
Chipster

49

Aus einem Artikel von Dan Saks über "Flüchtig als Versprechen" :

(...) Ein flüchtiges Objekt ist eines, dessen Wert sich spontan ändern kann. Das heißt, wenn Sie ein Objekt als flüchtig deklarieren, teilen Sie dem Compiler mit, dass das Objekt möglicherweise den Status ändert, obwohl keine Anweisungen im Programm dies zu ändern scheinen. "

Hier sind Links zu drei seiner Artikel zum volatileKeyword:


23

Sie MÜSSEN volatile verwenden, wenn Sie sperrenfreie Datenstrukturen implementieren. Andernfalls kann der Compiler den Zugriff auf die Variable optimieren, wodurch sich die Semantik ändert.

Anders ausgedrückt, volatile teilt dem Compiler mit, dass der Zugriff auf diese Variable einer Lese- / Schreiboperation für den physischen Speicher entsprechen muss.

So wird beispielsweise InterlockedIncrement in der Win32-API deklariert:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);

Sie müssen eine Variable NICHT als flüchtig deklarieren, um InterlockedIncrement verwenden zu können.
Neugieriger

Diese Antwort ist veraltet, da C ++ 11 sie bereitstellt, std::atomic<LONG>sodass Sie sichereren Code sicherer schreiben können, ohne Probleme damit zu haben, reine Ladungen / reine Speicher zu optimieren oder neu zu ordnen oder was auch immer.
Peter Cordes

10

Eine große Anwendung, an der ich Anfang der neunziger Jahre gearbeitet habe, enthielt die C-basierte Ausnahmebehandlung mit setjmp und longjmp. Das flüchtige Schlüsselwort war für Variablen erforderlich, deren Werte in dem Codeblock beibehalten werden mussten, der als "catch" -Klausel diente, damit diese Variablen nicht in Registern gespeichert und vom longjmp gelöscht werden.


10

In Standard C ist einer der zu verwendenden Orte volatileein Signalhandler. In Standard C können Sie in einem Signalhandler nur eine volatile sig_atomic_tVariable ändern oder schnell beenden. In der Tat, AFAIK, ist dies der einzige Ort in Standard C, an dem die Verwendung von volatileerforderlich ist, um undefiniertes Verhalten zu vermeiden.

ISO / IEC 9899: 2011 §7.14.1.1 Die signalFunktion

¶5 Wenn das Signal nicht als Ergebnis des Aufrufs der Funktion abortoder auftritt raise, ist das Verhalten undefiniert, wenn der Signalhandler auf ein Objekt mit statischer oder Thread-Speicherdauer verweist, das kein sperrfreies atomares Objekt ist, außer durch Zuweisen eines Werts für ein Objekt, das als deklariert ist volatile sig_atomic_t, oder der Signalhandler ruft eine andere Funktion in der Standardbibliothek als die abortFunktion, die _ExitFunktion, die quick_exitFunktion oder die signalFunktion auf, wobei das erste Argument der Signalnummer entspricht, die dem Signal entspricht, das den Aufruf von verursacht hat Handler. Wenn ein solcher Aufruf der signalFunktion zu einer SIG_ERR-Rückgabe führt, ist der Wert von errnounbestimmt. 252)

252) Wenn ein Signal von einem asynchronen Signalhandler erzeugt wird, ist das Verhalten undefiniert.

Das bedeutet, dass Sie in Standard C schreiben können:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

und sonst nicht viel.

POSIX ist viel milder in Bezug auf das, was Sie in einem Signalhandler tun können, aber es gibt immer noch Einschränkungen (und eine der Einschränkungen besteht darin, dass die Standard-E / A-Bibliothek - printf()et al. - nicht sicher verwendet werden kann).


7

Bei der Entwicklung für ein Embedded habe ich eine Schleife, die eine Variable überprüft, die in einem Interrupt-Handler geändert werden kann. Ohne "flüchtig" wird die Schleife zu einem Noop - soweit der Compiler es beurteilen kann, ändert sich die Variable nie und optimiert so die Überprüfung.

Dasselbe würde für eine Variable gelten, die in einer traditionelleren Umgebung in einem anderen Thread geändert werden kann, aber dort führen wir häufig Synchronisationsaufrufe durch, sodass der Compiler bei der Optimierung nicht so frei ist.


7

Ich habe es in Debug-Builds verwendet, wenn der Compiler darauf besteht, eine Variable zu optimieren, die ich beim Durchlaufen des Codes sehen möchte.


7

Neben der bestimmungsgemäßen Verwendung wird bei der (Vorlagen-) Metaprogrammierung flüchtig verwendet. Es kann verwendet werden, um ein versehentliches Überladen zu verhindern, da das flüchtige Attribut (wie const) an der Überlastungsauflösung beteiligt ist.

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

Das ist legal; Beide Überladungen können möglicherweise aufgerufen werden und tun fast dasselbe. Die Besetzung in der volatileÜberladung ist legal, da wir wissen, dass die Bar Tohnehin nicht flüchtig ist. Die volatileVersion ist jedoch streng schlechter, daher niemals in der Überlastungsauflösung gewählt, wenn die nichtflüchtig istf verfügbar ist.

Beachten Sie, dass der Code niemals vom volatileSpeicherzugriff abhängt .


Könnten Sie dies bitte anhand eines Beispiels erläutern? Es würde mir wirklich helfen, besser zu verstehen. Vielen Dank!
Batbrat

" Die Besetzung in der flüchtigen Überlastung " Eine Besetzung ist eine explizite Umwandlung. Es ist ein SYNTAX-Konstrukt. Viele Leute machen diese Verwirrung (sogar Standardautoren).
Neugieriger

6
  1. Sie müssen es verwenden, um Spinlocks sowie einige (alle?) sperrenfreie Datenstrukturen zu implementieren
  2. Verwenden Sie es mit atomaren Operationen / Anweisungen
  3. hat mir einmal geholfen, den Fehler des Compilers zu überwinden (falsch generierter Code während der Optimierung)

5
Verwenden Sie besser eine Bibliothek, Compiler-Eigenschaften oder Inline-Assembly-Code. Flüchtig ist unzuverlässig.
Zan Lynx

1
1 und 2 verwenden beide atomare Operationen, aber flüchtig bietet keine atomare Semantik, und die plattformspezifischen Implementierungen von atomar ersetzen die Notwendigkeit der Verwendung von flüchtig. Für 1 und 2, ich bin anderer Meinung, benötigen Sie KEINE flüchtigen Operationen.

Wer sagt etwas über die flüchtige Bereitstellung atomarer Semantik? Ich sagte, Sie müssen flüchtige mit atomaren Operationen verwenden und wenn Sie nicht glauben, dass es wahr ist, schauen Sie sich die Deklarationen der ineinandergreifenden Operationen der win32-API an (dieser Typ erklärte dies auch in seiner Antwort)
Mladen Janković

4

Das volatile Schlüsselwort soll verhindern, dass der Compiler Optimierungen auf Objekte anwendet, die sich auf eine Weise ändern können, die vom Compiler nicht bestimmt werden kann.

Als deklarierte Objekte volatilewerden bei der Optimierung nicht berücksichtigt, da ihre Werte jederzeit durch Code außerhalb des Bereichs des aktuellen Codes geändert werden können. Das System liest immer den aktuellen Wert eines volatileObjekts aus dem Speicherort, anstatt seinen Wert an dem Punkt, an dem es angefordert wird, im temporären Register zu belassen, selbst wenn eine vorherige Anweisung nach einem Wert von demselben Objekt gefragt hat.

Betrachten Sie die folgenden Fälle

1) Globale Variablen, die durch eine Interrupt-Serviceroutine außerhalb des Gültigkeitsbereichs geändert wurden.

2) Globale Variablen in einer Multithread-Anwendung.

Wenn wir kein flüchtiges Qualifikationsmerkmal verwenden, können die folgenden Probleme auftreten

1) Der Code funktioniert möglicherweise nicht wie erwartet, wenn die Optimierung aktiviert ist.

2) Code funktioniert möglicherweise nicht wie erwartet, wenn Interrupts aktiviert und verwendet werden.

Flüchtig: Der beste Freund eines Programmierers

https://en.wikipedia.org/wiki/Volatile_(computer_programming)


Der von Ihnen gepostete Link ist extrem veraltet und spiegelt nicht die aktuellen Best Practices wider.
Tim Seguine

2

Neben der Tatsache, dass das flüchtige Schlüsselwort verwendet wird, um den Compiler anzuweisen, den Zugriff auf eine Variable (die durch einen Thread oder eine Interruptroutine geändert werden kann) nicht zu optimieren, kann dies auch der Fall sein verwendet werden, um einige Compilerfehler zu entfernen - JA, das kann es sei ---.

Ich habe zum Beispiel auf einer eingebetteten Plattform gearbeitet, auf der der Compiler einige falsche Aussagen bezüglich eines Werts einer Variablen gemacht hat. Wenn der Code nicht optimiert wäre, würde das Programm in Ordnung laufen. Bei Optimierungen (die wirklich benötigt wurden, weil es eine kritische Routine war) würde der Code nicht richtig funktionieren. Die einzige Lösung (wenn auch nicht sehr korrekt) bestand darin, die 'fehlerhafte' Variable als flüchtig zu deklarieren.


3
Es ist eine fehlerhafte Annahme, dass der Compiler den Zugriff auf flüchtige Stoffe nicht optimiert. Der Standard weiß nichts über Optimierungen. Der Compiler muss die Vorgaben des Standards einhalten, kann jedoch Optimierungen vornehmen, die das normale Verhalten nicht beeinträchtigen.
Terminus

3
Nach meiner Erfahrung sind 99,9% aller Optimierungs- "Fehler" im gcc-Arm Fehler des Programmierers. Keine Ahnung, ob dies auf diese Antwort zutrifft. Nur ein Scherz über das allgemeine Thema
odinthenerd

@Terminus " Es ist eine fehlerhafte Annahme, dass der Compiler den Zugriff auf flüchtige Stoffe nicht optimiert. " Quelle?
Neugieriger

2

Ihr Programm scheint auch ohne zu funktionieren volatile Schlüsselwort ? Vielleicht ist das der Grund:

Wie bereits erwähnt, volatilehilft das Schlüsselwort in Fällen wie

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

Sobald eine externe oder Nicht-Inline-Funktion aufgerufen wird, scheint dies jedoch fast keine Auswirkungen zu haben. Z.B:

while( *p!=0 ) { g(); }

Dann mit oder ohne volatile fast das gleiche Ergebnis erzeugt.

Solange g () vollständig inline sein kann, kann der Compiler alles sehen, was vor sich geht, und kann daher optimieren. Wenn das Programm jedoch einen Ort aufruft, an dem der Compiler nicht sehen kann, was vor sich geht, kann der Compiler keine Annahmen mehr treffen. Daher generiert der Compiler Code, der immer direkt aus dem Speicher liest.

Aber Vorsicht vor dem Tag, an dem Ihre Funktion g () inline wird (entweder aufgrund expliziter Änderungen oder aufgrund der Cleverness von Compilern und Linkern), kann Ihr Code beschädigt werden, wenn Sie das volatileSchlüsselwort vergessen haben !

Daher empfehle ich, das volatileSchlüsselwort hinzuzufügen, auch wenn Ihr Programm ohne zu funktionieren scheint. Dies macht die Absicht klarer und robuster in Bezug auf zukünftige Änderungen.


Beachten Sie, dass der Code einer Funktion eingefügt werden kann, während weiterhin ein Verweis (zum Zeitpunkt der Verknüpfung aufgelöst) auf die Gliederungsfunktion generiert wird. Dies ist der Fall bei einer teilweise inline rekursiven Funktion. Die Semantik einer Funktion könnte auch vom Compiler "inliniert" werden, dh der Compiler geht davon aus, dass die Nebenwirkungen und das Ergebnis innerhalb der möglichen Nebenwirkungen und Ergebnisse liegen, die gemäß seinem Quellcode möglich sind, ohne sie jedoch zu inlinieren. Dies basiert auf der "effektiven One Definition Rule", die besagt, dass alle Definitionen eines Unternehmens effektiv gleichwertig sein müssen (wenn nicht genau identisch).
Neugieriger

Das tragbare Inlining eines Aufrufs (oder "Inlining" seiner Semantik) durch eine Funktion, deren Körper für den Compiler sichtbar ist (auch zum Zeitpunkt der Verknüpfung mit globaler Optimierung), ist mithilfe eines volatilequalifizierten Funktionszeigers möglich:void (* volatile fun_ptr)() = fun; fun_ptr();
neugieriger

2

In den frühen Tagen von C interpretierten Compiler alle Aktionen, die l-Werte lesen und schreiben, als Speicheroperationen, die in derselben Reihenfolge ausgeführt wurden, in der die Lese- und Schreibvorgänge im Code erschienen. Die Effizienz könnte in vielen Fällen erheblich verbessert werden, wenn den Compilern ein gewisses Maß an Freiheit bei der Neuordnung und Konsolidierung von Vorgängen eingeräumt würde. Dies war jedoch problematisch. Sogar Operationen wurden oft in einer bestimmten Reihenfolge spezifiziert, nur weil es notwendig war, sie in einigen zu spezifizieren Ordnung und damit der Programmierer nahm eine von vielen gleich guten Alternativen, die nicht immer der Fall war. Manchmal ist es wichtig, dass bestimmte Operationen in einer bestimmten Reihenfolge ausgeführt werden.

Welche Details der Sequenzierung wichtig sind, hängt von der Zielplattform und dem Anwendungsbereich ab. Anstatt eine besonders detaillierte Steuerung bereitzustellen, entschied sich der Standard für ein einfaches Modell: Wenn eine Folge von Zugriffen mit nicht qualifizierten Werten durchgeführt wird, volatilekann ein Compiler diese nach eigenem Ermessen neu anordnen und konsolidieren. Wenn eine Aktion mit einem volatilequalifizierten Wert ausgeführt wird, sollte eine Qualitätsimplementierung alle zusätzlichen Bestellgarantien bieten, die für Code erforderlich sind, der auf die beabsichtigte Plattform und das Anwendungsfeld abzielt, ohne dass die Verwendung einer nicht standardmäßigen Syntax erforderlich ist.

Leider haben sich viele Compiler dafür entschieden, die vom Standard vorgeschriebenen Mindestgarantien anzubieten, anstatt zu ermitteln, welche Garantien Programmierer benötigen würden. Dies macht volatileviel weniger nützlich als es sein sollte. Bei gcc oder clang muss beispielsweise ein Programmierer, der einen grundlegenden "Hand-off-Mutex" implementieren muss [einer, bei dem eine Aufgabe, die einen Mutex erworben und freigegeben hat, dies erst wieder tun, wenn die andere Aufgabe dies getan hat], einen ausführen von vier Dingen:

  1. Stellen Sie die Erfassung und Freigabe des Mutex in eine Funktion, die der Compiler nicht inline verwenden kann und auf die er die Optimierung des gesamten Programms nicht anwenden kann.

  2. Qualifizieren Sie alle vom Mutex volatilegeschützten Objekte als - etwas, das nicht erforderlich sein sollte, wenn alle Zugriffe nach dem Erwerb des Mutex und vor dessen Freigabe erfolgen.

  3. Verwenden Sie die Optimierungsstufe 0, um den Compiler zu zwingen, Code zu generieren, als wären alle Objekte, die nicht qualifiziert registersind volatile.

  4. Verwenden Sie gcc-spezifische Anweisungen.

Im Gegensatz dazu hätte man bei Verwendung eines höherwertigen Compilers, der besser für die Systemprogrammierung geeignet ist, wie z. B. icc, eine andere Option:

  1. volatileStellen Sie sicher, dass an jedem Ort, an dem eine Erfassung oder Freigabe erforderlich ist , ein qualifizierter Schreibvorgang ausgeführt wird.

Das Erwerben eines einfachen "Hand-Off-Mutex" erfordert ein volatileLesen (um zu sehen, ob es bereit ist) und sollte auch kein volatileSchreiben erfordern (die andere Seite wird nicht versuchen, es wieder zu erwerben, bis es zurückgegeben wird), muss es aber Ein bedeutungsloses volatileSchreiben ist immer noch besser als alle unter gcc oder clang verfügbaren Optionen.


1

Eine Verwendung, an die ich Sie erinnern sollte, ist, dass Sie in der Signalhandlerfunktion, wenn Sie auf eine globale Variable zugreifen / diese ändern möchten (z. B. als exit = true markieren), diese Variable als 'flüchtig' deklarieren müssen.


1

Alle Antworten sind ausgezeichnet. Darüber hinaus möchte ich ein Beispiel nennen.

Unten ist ein kleines CPP-Programm:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

Lassen Sie uns nun die Assembly des obigen Codes generieren (und ich werde nur die Teile der Assembly einfügen, die hier relevant sind):

Der Befehl zum Generieren einer Assembly:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

Und die Versammlung:

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Sie können in der Assembly sehen, dass der Assemblycode nicht generiert wurde, sprintfda der Compiler davon ausgegangen ist, dass xsich dies außerhalb des Programms nicht ändert. Gleiches gilt für die whileSchleife. whileSchleife wurde insgesamt aufgrund der Optimierung entfernt , da Compiler sie als nutzlos Code sah und somit direkt zugeordnet 5zu x(siehe movl $5, x(%rip)).

Das Problem tritt auf, wenn ein externer Prozess / eine externe Hardware den Wert von xirgendwo zwischen x = 8;und ändern würde if(x == 8). Wir würden erwarten else, dass Block funktioniert, aber leider hat der Compiler diesen Teil herausgeschnitten.

Nun, um dieses Problem zu lösen, in der assembly.cpp, lassen Sie uns ändern int x;zu volatile int x;schnell und die Assembler - Code sehen erzeugt:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Hier können Sie sehen , dass die Montage - Codes für sprintf, printfund whileSchleife erzeugt wurden. Der Vorteil ist, dass ein Teil des Codes ausgeführt wird, wenn die xVariable durch ein externes Programm oder eine externe Hardware geändert sprintfwird. In ähnlicher Weise whilekann die Schleife jetzt zum Warten verwendet werden.


0

In anderen Antworten wird bereits erwähnt, dass Optimierungen vermieden werden müssen, um:

  • Verwenden Sie speicherabgebildete Register (oder "MMIO").
  • Gerätetreiber schreiben
  • Ermöglichen ein einfacheres Debuggen von Programmen
  • Gleitkommaberechnungen deterministischer machen

Volatile ist immer dann wichtig, wenn Sie einen Wert benötigen, der von außen zu kommen scheint und unvorhersehbar ist, und Compileroptimierungen vermeiden, die auf einem bekannten Wert basieren, und wenn ein Ergebnis nicht tatsächlich verwendet wird, sondern berechnet werden muss oder aber verwendet wird Sie möchten es mehrmals für einen Benchmark berechnen und benötigen die Berechnungen, um an genauen Punkten zu beginnen und zu enden.

Ein flüchtiger Lesevorgang ist wie eine Eingabeoperation (wie scanfoder eine Verwendung von cin): Der Wert scheint von außerhalb des Programms zu kommen, daher muss jede Berechnung, die vom Wert abhängig ist, danach beginnen .

Ein flüchtiges Schreiben ist wie eine Ausgabeoperation (wie printfoder eine Verwendung von cout): Der Wert scheint außerhalb des Programms kommuniziert zu werden. Wenn der Wert also von einer Berechnung abhängt, muss er vorher beendet werden .

So kann ein Paar flüchtiger Lese- / Schreibvorgänge verwendet werden, um Benchmarks zu zähmen und die Zeitmessung sinnvoll zu gestalten .

Ohne flüchtig könnte Ihre Berechnung zuvor vom Compiler gestartet werden, da nichts eine Neuordnung von Berechnungen mit Funktionen wie Zeitmessung verhindern würde .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.