Understanding volatile
Often, during discussions with peers, I notice a common misunderstanding of what the volatile
specifier in C++ does, and I hope to clarify it with the following blog post.
References:
Reading an object designated by a volatile glvalue (7.2.1), modifying an object, calling a library I/O
function, or calling a function that does any of those operations are all side effects, which are changes in the
state of the execution environment. Evaluation of an expression (or a subexpression) in general includes both
value computations (including determining the identity of an object for glvalue evaluation and fetching a
value previously assigned to an object for prvalue evaluation) and initiation of side effects. When a call to a
library I/O function returns or an access through a volatile glvalue is evaluated the side effect is considered
complete, even though some external actions implied by the call (such as the I/O itself) or by the volatile
access may not have completed yet.
The following excerpt details that all accesses (reads and writes) to a volatile
qualified variable are considered as side-effects, hence qualifying a variable as volatile
only disables compiler optimizations on reads/write instructions to it. It absolutely doesn’t provide any guarantees on memory ordering of these read and write instructions. The read/write instructions generated would be identical to that of an non volatile
qualified variable. At the assembly level there is no notion of which address (or variable) was marked as volatile
.
Since volatile
read/writes do not have any acquire/release memory ordering. Hence, while reading (without any explicit acquire memory ordering) from an volatile
qualified variable, it should be expected that the write (without any explicit release memory ordering) from a different unit of execution might be delayed indefinitely.
NOTE:
- These reads/writes need not be from main memory and can be from the local cache.
- Casting down a
volatile
defined variable to a non-volatile
variable is UB.
When to use volatile
?
Traditionally compilers were built without concurrent programs in mind and often perform optimizations assuming there is no other unit of execution affecting the control flow being analyzed.
Hence, volatile
is used when a variable is shared between multiple concurrent units of execution and you don’t need any sort of acquire-release memory ordering.
NOTE:
- Ordinary reads and writes and relaxed memory order operations on all architectures
- On x86 architectures, TSO is guaranteed by the architecture hence, all reads and writes are acquire and release operations respectively
void lock(volatile int *flag)
{
while(*flag){}
}
In the absence of the volatile
qualifier, the compiler would optimize away the while loop because it has no reason to believe that the value of or the value pointed to by flag
can change in the body of the function.
Note that atomic reads are not considered side effects, hence a simple relaxed atomic read is not equivalent to a volatile
read.
Reference from LKMM standard paper
Note that the volatile is absolutely required: Non-volatile memory_order_relaxed is not sufficient. To see this, consider that READ_ONCE() can be used to prevent concurrently modified accesses from being hoisted out of a loop or out of unrolled instances of a loop. For example, given this loop:
```
while (tmp = atomic_load_explicit(a, memory_order_relaxed))
do_something_with(tmp);
```
The compiler would be permitted to unroll it as follows:
```
while (tmp = atomic_load_explicit(a, memory_order_relaxed)) {
do_something_with(tmp);
do_something_with(tmp);
do_something_with(tmp);
do_something_with(tmp);
}
```
This would be unacceptable for real-time applications, which need the value to be reloaded from `a` on each iteration, unrolled or not. The volatile qualifier prevents this transformation. For example, consider the following loop:
```
while (tmp = READ_ONCE(a))
do_something_with(tmp);
```
This loop could still be unrolled, but the read would also need to be unrolled, for example, like this:
```
for (;;) {
if (!(tmp = READ_ONCE(a)))
break;
do_something_with(tmp);
if (!(tmp = READ_ONCE(a)))
break;
do_something_with(tmp);
if (!(tmp = READ_ONCE(a)))
break;
do_something_with(tmp);
if (!(tmp = READ_ONCE(a)))
break;
do_something_with(tmp);
}
```
const volatile
Consider the following variable definition
const volatile int32_t *input
Why would you ever have a const volatile
variable?
The whole point of volatile
is making accesses to it side effects
All reads from a const
variable are going to be the same value, so why the compiler from optimizing the reads out?
Well, the answer is that const
simply means C/C++ code can’t change the value of the variable
If you have a snippet of assembly code, it can still change the value of the variable
A use case for const volatile
could be a variable which denotes the input from an I/O device
The device driver should be allowed to change the value of this variable (possibly using assembly instructions)
But, the user of the library should not be allowed to write to this value (hence, the const
)