Improve volatile Usage with volatile_load() and volatile_store()

Updated: 20190322

A C++ proposal for deprecating the volatile keyword has surfaced. This may surprise our readers, because as Michael Caisse said, "volatile is the embedded keyword."

The original intent of the volatile keyword in C89 is to suppress read/write optimizations:

No cacheing through this lvalue: each operation in the abstract semantics must be performed (that is, no cacheing assumptions may be made, since the location is not guaranteed to contain any previous value). In the absence of this qualifier, the contents of the designated location may be assumed to be unchanged except for possible aliasing.

The problem with its use in C++ is that the meaning is much less clear, as it is mentioned 322 times in the C++17 draft of the C++ Standard.

One problematic and common assumption is that volatile is equivalent to "atomic". This is not the case. All the volatile keyword denotes is that the variable may be modified externally, and thus reads/writes cannot be optimized. This means that the volatile keyword only has a meaningful impact on load and store operations.

Where programmers run into trouble is using volatile variables in a read-modify-write operation, such as with the increment (++) and decrement (--) operators. Such operations create a potential for a non-obvious race condition, depending on how the operation is implemented in the compiler and platform.

volatile int i = 2; //probably atomic
i++; //not atomic ...

Other problematic volatile use cases can be found, such as chained assignments of volatile values:

// is b re-read before storing the value to a, or not?
a = b = c

We recommend using volatile_load<T>() and volatile_store<T>() template functions to encourage better volatile behavior in our programs.

auto r = volatile_load(&i);
r++;
volatile_store(&i, r);

You can use these functions to refactor your programs and control volatile use cases. While this implementation does not meet the proposed specification, it's a step toward cleaning up our use of the volatile keyword.

#include <cassert>
#include <type_traits>

/** Read from a volatile variable
 *
 * @tparam TType the type of the variable. This will be deduced by the compiler.
 * @note TType shall satisfy the requirements of TrivallyCopyable.
 * @param target The pointer to the volatile variable to read from.
 * @returns the value of the volatile variable.
 */
template<typename TType>
constexpr inline TType volatile_load(const TType* target)
{
    assert(target);
    static_assert(std::is_trivially_copyable<TType>::value,
        "Volatile load can only be used with trivially copiable types");
    return *static_cast<const volatile TType*>(target);
}

/** Write to a volatile variable
 *
 * Causes the value of `*target` to be overwritten with `value`.
 *
 * @tparam TType the type of the variable. This will be deduced by the compiler.
 * @note TType shall satisfy the requirements of TrivallyCopyable.
 * @param target The pointer to the volatile variable to update.
 * @param value The new value for the volatile variable.
 */
template<typename TType>
inline void volatile_store(TType* target, TType value)
{
    assert(target);
    static_assert(std::is_trivially_copyable<TType>::value,
        "Volatile store can only be used with trivially copiable types");
    *static_cast<volatile TType*>(target) = value;
}

As Odin Holmes pointed out in the comments, refactoring our code to use volatile_load() and volatile_store() can also boost the performance of our programs. This is because we are constraining the optimizer more clearly.

This traditional volatile code:

volatile uint32_t* register_x;
* register_x &= ~mask;
* register_x |= value;

Will not be as performant as this version:

auto r = volatile_load(&register_x);
r &=~mask;
r |= value;
volatile_store(&register_x, r);

Further Reading

Change Log

  • 20190322:
    • Added comments from Odin Holmes regarding optimizations.