Johan511 Blog

volatile specifier

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:

  1. These reads/writes need not be from main memory and can be from the local cache.
  2. 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:

  1. Ordinary reads and writes and relaxed memory order operations on all architectures
  2. 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)