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 - x or x * 0 reduce an expression to a constant, stripping away the data dependency on x at 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.