Why C++ Compilers Treat consume as acquire
The C++11 memory model introduced
std::memory_order_consume to enable performant,
dependency-ordered reads on weakly ordered architectures without the
overhead of heavy synchronization instructions. Despite its theoretical
benefits, major compilers like GCC, Clang, and MSVC routinely promote
memory_order_consume to the stronger
std::memory_order_acquire. This article explains the
technical challenges, compiler optimization conflicts, and specification
ambiguities that forced compiler vendors to abandon native
consume implementations in favor of simple promotion to
acquire.
The Original Intent
of memory_order_consume
In concurrent programming, an acquire operation prevents subsequent memory reads and writes from being reordered before the acquire. On architectures like ARM and PowerPC, enforcing acquire semantics requires either dedicated load-acquire instructions or explicit memory fence instructions, both of which incur execution overhead.
However, modern CPUs naturally preserve ordering when a subsequent
instruction has a direct data dependency on a loaded value—such as
reading a pointer and then dereferencing that pointer. The designers of
C++11 created memory_order_consume to exploit this
hardware-level dependency ordering (often used in Read-Copy-Update
patterns in the Linux kernel). The goal was to provide safe cross-thread
publication without paying the price of hardware fence instructions.
The Problem of Preserving Dependencies in Optimizing Compilers
While hardware processors respect architectural data dependencies, optimizing compilers operate on abstract intermediate representations where such dependencies can be silently erased.
Compilers actively simplify expressions to improve throughput:
- Value Speculation and Propagation: If the compiler determines that a pointer or index loaded via a dependency chain is likely to be a constant or a known variable, it may substitute that value directly, eliminating the dependency.
- Algebraic Simplification: Operations such as
x - xorx * 0reduce an expression to a constant, stripping away the data dependency onxat the machine code level. - Common Subexpression Elimination: If a dependent memory access matches an earlier computation, the compiler may reuse the earlier result, decoupling the operation from the consume load.
If a compiler optimizes away a data dependency, the underlying processor will no longer enforce the ordering guarantee. A processor might reorder a subsequent read ahead of the initial load, leading to race conditions and memory corruption.
Specification and Implementation Complexity
The C++ standard attempted to define dependency chains formally using
the concept of "carries a dependency," accompanied by the
[[carries_dependency]] attribute to help chains cross
function boundaries.
In practice, this formalization proved excessively complex:
- Compiler front-ends and back-ends would need deep modifications to track dependency graphs through every optimization pass.
- Treating dependency-carrying expressions as volatile-like black boxes required disabling major, highly profitable optimizations across large regions of code.
- The formal definitions in the standard contained edge cases that made it difficult to determine whether a given optimization was legally compliant.
The Cost-Benefit Trade-Off
Compiler vendors faced a difficult choice: build an intricate, error-prone tracking system that drastically degraded optimization efficiency, or guarantee safety by substituting a stronger memory order.
Because std::memory_order_acquire provides strictly
stronger synchronization guarantees than
std::memory_order_consume, promoting consume
to acquire is always functionally correct under the
standard. Furthermore, on architectures like x86, every load instruction
inherently exhibits acquire semantics at the hardware level with zero
performance penalty, making the distinction completely irrelevant on the
dominant desktop architecture.
On architectures where acquire instructions do have a runtime cost, compiler implementers found that the penalty of a fence or load-acquire instruction was often lower than the performance lost by disabling core compiler optimizations to preserve fragile dependency chains.
Formal Discouragement by WG21
Recognizing that no major vendor had successfully implemented the
feature as designed, the ISO C++ Standards Committee (WG21) acknowledged
the issue. The standard was updated to explicitly state that the
specification of memory_order_consume is temporarily
discouraged, with ongoing proposals working to replace it with a
simpler, more implementable model. Until a viable specification emerges,
compilers continue to implement memory_order_consume as
memory_order_acquire.