Navigation

Was ist volatile?

Was ist volatile?

Viele Leute erkennen eher durch Zufall, daß es mit volatile etwas Besonderes auf sich hat: vielleicht stolpert man in fremden Source über eine Programmzeile, wo eine Variable deklariert wird, bei der volatile in der Deklaration vorkommt, vielleicht hat jemand probiert, eine Variable, Klasse oder Funktion "volatile" zu nennen und der Compiler hat das nicht angenommen. Und tatsächlich: volatile ist ein Schlüsselwort in C und C++, genauer gesagt ein Type specifier und somit aus der gleichen Familie wie die Schlüsselworte const und restrict. Was volatile nun ganz genau besagt und wie man es nutzt, ist Thema dieses Artikels.

Anmerkung: Das Verhalten der hier vorgestellten Codebeispiele hängt häufig vom verwendeten Compiler ab (warum, wird im Verlauf des Artikels erklärt). Hier wurde der gcc 3.3.3 auf einem Linux System verwendet.

1. Definition des volatile Type Specifiers

Was das Schlüsselwort volatile genau ist, wird im C99 Standard (ISO/IEC 9899:1999) im Abschnitt 6.7.3.6 beschrieben. Sinngemäß kann man die Funktionsweise von volatile demnach - sehr verkürzt - so zusammenfassen:

Objekte, welche mit dem volatile Type Specifier qualifiziert sind, können durch Mechanismen verändert werden, welche der C/C++ Compiler nicht kennt. Diese haben Seiteneffekte, welche unbekannt sind.

Um die Tragweite dieser Aussage zu verstehen, schauen wir uns folgendes Codefragment an:










10 
11 
volatile int x;
...
/* Setze den Wert der Variablen x */
x = 4711;
...
/* Weiterer Code, welcher jdoch x nicht verändert. */
...
/* Verzweige mäß Wert von x */
if (x != 4711) {
    puts("Der Wert der Variablen x hat sich verändert!);
}

Wir deklarieren in Zeile 1 eine Variable x, weisen ihr in Zeile 4 einen Wert zu und greifen danach nicht mehr schreibend auf sie zu; d.h. es findet weder direkt noch indirekt eine Wertzuweisung statt. Da wir x mit dem volatile Type Specifier deklariert haben, teilen wir dem Compiler auf diesem Wege mit, daß wir es dennoch für möglich halten, daß sich x zwischenzeitlich geändert hat, also daß das Programm in Zeile 10 springen könnte. Das klingt zunächst ziemlich widersinnig und es drängen sich mindestens zwei Kernfragen auf:

Diese und weitere Fragen sollen im Folgenden nun geklärt werden:

2. Beispiele für unbekannte Seiteneffekte

Wir betrachten das folgende Programm unter Linux:










10 
11 
12 
13 
14 
15 
#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo) {
    quit = 1;
}

int main() {
    signal(SIGINT, handler);

    while(!quit) {
    }
}

Das Programm installiert zunächst mittels signal einen Signalhandler (Zeile 11), welcher darauf reagiert, wenn der Benutzer die Tastenkombination CTRL-C drückt. In diesem Fall wird die Routine handler (Zeile 6-8) aufgerufen, die die Variable quit (Zeile 4) auf 1 setzt. Danach macht das Programm eigentlich nicht viel: in einer Schleife (Zeile 13-14) wird geprüft, ob quit noch 0 ist. Solange das der Fall ist, wird die Schleife weiter ausgeführt. Das Programm ist mit Sicherheit sinnlos, aber man kann dennoch das erwartete Verhalten wie folgt definieren: Wenn der Benutzer CTRL-C drückt, so beendet sich das Programm, in allen anderen Fällen läuft es weiter. Tatsächlich aber hängt das Verhalten des Programms davon ab, ob man das Programm optimiert compiliert oder nicht. Compiliert man das Programm

  • ohne Optimierungen, also etwa mittels "gcc -oprg prg.c", so tut es das Erwartete: Sobald man CTRL-c drückt, terminiert es, ansonsten läuft es weiter.
  • mit Optimierungen (z.B. mittels "gcc -O3 -oprg prg.c"), so reagiert das Programm generell auf keinen Tastendruck und terminiert niemals.

D.h. es hängt hier von der Optimierung ab, ob das Programm letztlich korrekt arbeitet oder nicht - ein Umstand auf den wir später noch ausführlich zu sprechen kommen. An dieser Stelle mag es genügen zu erkennen, daß die Variable quit durch main nicht verändert wird, jedenfalls nicht auf eine für den Compiler nachvollziehbare Art: zwar wird in Zeile 11 ein Signalhandler initialisert, dieser jedoch nicht sofort aufgerufen, so daß in main lediglich in Zeile 13 ein einziger Zugriff auf quit erfolgt - und zwar lesend. Allerdings wissen wir mehr als der Compiler: Wir wissen, daß das Drücken von CTRL-C dazu führt, daß der zuvor installierte Signalhandler aufgerufen wird, und damit quit verändert wird. Der Compiler kann dies im Zweifel garnicht wissen, weil er dann wissen müsste, was signal rein semantisch macht. Aus Sicht des Compilers ist beim Drücken von CTRL-C damit ein unbekannter Seiteneffekt auf die Variable quit gegeben.

Und tatsächlich verhält es sich so, daß wenn man die Deklaration in Zeile 4 um ein volatile bereichert, das Programm stets korrekt arbeitet - unabhängig davon, ob es nun mit oder ohne Optimierung kompiliert wurde:

volatile int quit = 0;

Ganz ähnliche Effekte lassen sich z.B. in folgenden Situationen beobachten:

  • Bei Multithreaded Anwendungen, bei denen der eine Thread nur lesend, der andere jedoch schreibend direkt auf eine [globale] Variable zugreift. Hier kännen prinzipiell die gleichen Probleme auftreten, wie im o.g. Beispiel, weil der Optimierer des Compilers im Zweifel nicht die geringste Ahnung von Multithreading hat.
  • Jede Form der Interprocess Communication, z.B. der Zugriff auf Speicher im Shared Memory stellt ebenfalls eine solche Situation dar.
  • Bei besonders hardwarenahen Programmen, welche z.B. I/O Ports direkt auf C Variablen abbilden, laufen ebenfalls in die Problematik unbekannter Seiteneffekte, weil naturgemäßig I/O Ports von der darunterliegenden Hardware beschrieben und verändert werden und nicht etwa in dem C Programm selbst.
  • C und C++ erlauben das einbinden von Assemblercode, z.B. durch asm Blöcke. Allerdings ist der Inhalt dieser Assemblereinschübe für den C Compiler meist eine Blackbox: wird dort auf eine Variable schreibend zugegriffen, so kann dies der Compiler nicht wissen - wieder eine Quelle unbekannter Seiteneffekte.

All dies sind Beispiele, wo es Sinn macht, volatile zu verwenden, weil sonst das Programm ggf. nicht mehr korrekt funktioniert. Im folgenden Abschnitt wollen wir nun untersuchen, was der Compiler bei einer volatile anders macht:

3. Wirkungsweise von volatile auf den Compiler

Die eingangs abgegebene Definitions des volatile Type pecifiers war sehr verkürzt wiedergegeben - der C Standard wäre nicht der C Standard, wenn er nicht genauer definieren würde, was mit Variablen zu geschehen hat, die mit volatile deklariert sind. Stark vereinfachend kann man sagen, daß ein Compiler den Zugriff auf solche Variablen nicht wegoptimieren darf bzw. die Ausdrücke, in die volatile Objekte involviert sind, nicht intern umstruktieren darf (der Standard drückt das alles wesentlich präziser aus, wer also der Sache komplett auf den Grund gehen will, sollte den ANSI C Standard zu Rate ziehen). Damit ist auch eine Erklärung gefunden, warum der Beispielcode von oben nur ohne Optimierung funktioniert. Stellt man nämlich die Optimierung ein, so wird der Compiler bei der Analyse der Funktion main etwa folgende Schlüsse machen (immer unter der Voraussetzung, daß die Variable quit nicht volatile sei):










...
int quit = 0;
...
int main() {
    signal(SIGINT, handler);

    while (!quit) {
    }
}

Zunächst erkennt der Optimizer, daß lediglich in Zeile 7 auf die Variable quit lesend zugegriffen wird. Da er so erst einmal annehmen kann, daß sich quit in main nicht ändert, wäre es ziemlich ineffizient, den Wert von quit bei jedem Schleifendurchlauf abzufragen. Folglich würde er möglichwerweise die Variable nur ein einziges Mal abfragen, indem er die while Schleife (Zeile 7-8) wie folgt umstellt:










10 
11 
...
int quit = 0;
...
int main() {
    signal(SIGINT, handler);

    if (!quit) {
        while (1) {
        }
    }
}

Im nächsten Schritt könnte der Optimierer erkennen, daß quit eigentlich immer 0 ist, und daher die umgebende if Anweisung komplett entfernen:










...
int quit = 0;
...
int main() {
    signal(SIGINT, handler);

    while (1) {
    }
}

All diese eigentlich vom Compiler nur nett gemeinten Optimierungen sind in unserer speziellen Situation eher unerwünscht, können jedoch gezielt mit der Verwendung von volatile unterbunden werden.

4. Fazit

Der volatile Type Specifier wird im Vergleich etwa zu const eher selten verwendet. Dies bedeutet jedoch nicht, daß er keine Berechtigung hat - ganz im Gegenteil: In Situation, wo er wirklich gebraucht wird, optimiert er in letzter Konsequenz Performance, weil die Alternative in einem Programm generell auf Optimierungen zu verzichten, eher Performance kostet.


Ihre Meinung ist mir wichtig

Haben Sie einen Fehler gefunden, wollen etwas anmerken oder fragen? Gefällt Ihnen etwas nicht oder wollen Sie ein Lob loswerden? - Nur raus damit!