Introduction
Occasionally, I come across either a use or explanation of the volatile
keyword in C (or C++). Most often, that use or explanation is simply wrong. So here, hopefully once and for all, is what volatile
in C (and C++) actually (and only) does.
Legitimate Uses of volatile
There are only three legitimate uses of volatile
in C (and C++):
To tell the compiler that objects qualified with
volatile
may either be modified or cause side effects in ways unknown to the compiler; therefore, not to optimize away accesses to such objects nor reorder their accesses with respect to other operations that have visible side effects.For use in a signal-handler.
For use with
setjmp()
.
That’s it. Any other use of volatile
is innocuous, inefficient, or simply wrong.
Use 1: Optimization Suppression
The C11 standard (§ 6.7.3 ¶ 7) has the following to say about the first use of volatile
:
An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.
This typically means memory-mapped I/O, that is particular memory addresses are mapped either to specialized hardware or functions, so that reading from such an address actually reads a current value from such hardware (say, some kind of sensor) or writing to such an address actually causes a change in such hardware (say, sends a value over an old-fashioned modem).
For example, consider this code that sends all the characters of a C string over a modem that is mapped to the address 0x1000
:
char *const MODEM = (char*)0x1000; // memory-mapped address
void modem_send_s( char const *s ) {
while ( *s )
*MODEM = *s++;
}
Writing a character to *MODEM
sends that character. The problem is that a compiler might be clever and optimize that code to be:
void modem_send_s( char const *s ) { // compiler optimized code
while ( *s++ )
;
*MODEM = s[-1];
}
Under ordinary circumstances, it would be within its rights to do so (because if MODEM
were a pointer to ordinary memory, only the last write matters). But in the memory-mapped I/O case, it would be wrong (because the compiler doesn’t know that address 0x1000
is special).
The C11 standard continues:
Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine....
That means no reads from nor writes to such an object will be optimized away nor instructions reordered by the compiler even if it normally would do so.
The way to tell the compiler not to optimize accesses to an object is by qualifying it with volatile
:
char volatile *const MODEM = (char*)0x1000;
(Read from right-to-left: MODEM
is a constant pointer to a volatile char
; or use cdecl. Just as with const
, volatile
can be written to the right or “east” of the base type.)
Use 2: Signal Handling
The C11 standard (§ 7.14.1.1 ¶ 5) has the following to say about the second use of volatile
:
If the signal occurs ..., the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as
volatile sig_atomic_t
....
This means that if you want to refer to any object outside of the signal handler function, the type of that object (other than lock-free atomics) must be volatile sig_atomic_t
. For example:
sig_atomic_t volatile last_sig_val;
void signal_handler( int sig_val ) {
// ...
last_sig_val = sig_val;
}
Use 3: setjmp()
The C11 standard (§ 7.13.2.1 ¶ 3) has the following to say about the third use of volatile
:
... the values of objects of automatic storage duration that are local to the function containing the invocation of the ...
setjmp
macro that do not havevolatile
-qualified type and have been changed between thesetjmp
invocation andlongjmp
call are indeterminate.
This means that if you modify a local variable between the time setjmp()
is called and longjmp()
returns, that variable must be declared volatile
. For example:
void f() {
volatile int count = 0;
if ( setjmp( jmp_buf ) != 0 )
g( ++count );
// ...
}
That’s it: those are the only three legitimate uses of volatile
in C (and C++).
For more about setjmp()
, see here.
A Digression on “Atomic”
Since the word “atomic” came up, it should be explained because it can have multiple meanings:
- A value operation completes with no possible intervening operation by another thread, e.g., writing single-byte values on any CPU, or 16- or 32-bit values on a 32-bit CPU, or 64-bit values on a 64-bit CPU.
- An updated value is visible to other CPUs.
- Multiple value operations complete with no possible intervening operation by another thread, e.g., updating A and B together (“transactional”).
“Atomic” always means #1; it usually also means #2; or all three. In the case of sig_atomic_t
, however, it means only #1.
volatile
in Other Languages
I’ve been consistently mentioning volatile
as it pertains to C (and C++) to emphasize that this explanation of volatile
pertains only to C (and C++) because the volatile
keyword appears in other languages, for example Java and C#.
In those languages, volatile
provides stronger guarantees regarding operations and therefore can be used for limited forms of thread-safety. And this can sometimes cause confusion as to what volatile
means in C (and C++).
Wrong Uses of volatile
In C (and C++), volatile
:
- Is not a synonym for either
_Atomic
(in C) orstd::atomic<T>
(in C++). - Does not use memory barriers.
- Therefore, does not guarantee thread-safety.
- Limits only what optimizations the compiler may do.
- Does not limit what the hardware can do.
In particular, the hardware is still free to do things like memory caching, instruction parallelization, and speculative execution.
Conclusion
volatile
in C (and C++) is used only for memory-mapped I/O, signal handlers, or with setjmp()
. Since those uses are rarely needed, it’s entirely possible to go years or even one’s entire career without ever using volatile
in a C (or C++) program.