C++ carries_dependency Attribute Explained
The [[carries_dependency]] attribute in C++ optimizes
memory ordering across function boundaries by preserving dependency
chains initiated by std::memory_order_consume. When values
are loaded using consume operations, weakly ordered architectures can
avoid emitting expensive memory fence instructions by relying on
hardware-level data dependencies. The
[[carries_dependency]] attribute explicitly informs the
compiler that these dependency chains enter or exit a function,
preventing the compiler from pessimistically generating unnecessary
memory barriers at call sites.
The
Problem with Function Boundaries and
memory_order_consume
The C++ memory model includes std::memory_order_consume
to establish a dependency-ordered-before relationship. Under consume
semantics, subsequent operations that depend directly on the loaded
value (such as dereferencing a pointer) are guaranteed to see memory
writes committed before that value was stored by another thread.
On weakly ordered processors (such as ARM or PowerPC), hardware automatically enforces ordering for dependent data without issuing full memory fences. However, compilers frequently lose track of dependency chains across function boundaries, especially when functions are compiled in separate translation units or cannot be inlined.
Because the compiler cannot guarantee that the callee maintains the
dependency chain, it typically takes a conservative approach: it
upgrades the memory_order_consume to a stronger
std::memory_order_acquire or inserts an explicit hardware
fence prior to passing the value. This default behavior eliminates the
performance benefit of using consume operations over acquire
operations.
How
[[carries_dependency]] Operates
The [[carries_dependency]] attribute resolves this issue
by acting as a contract between the caller and the callee. It can be
applied in two primary locations:
On Function Parameters: Applying the attribute to a function parameter indicates that the caller passes a value that is part of an active dependency chain. The function body is expected to maintain that dependency.
void process_data(int* [[carries_dependency]] ptr);When the caller invokes
process_data, the compiler knows that the dependency chain is safely carried into the function. Consequently, it avoids generating a memory fence or promoting the upstreammemory_order_consumeload tomemory_order_acquirebefore the call.On Function Return Types: Applying the attribute to a function declaration indicates that the returned value continues a dependency chain initiated within the function.
[[carries_dependency]] int* fetch_data();Inside
fetch_data, the implementation can load a pointer withstd::memory_order_consumeand return it directly. The compiler does not need to emit an acquire fence before returning. The caller, in turn, is notified that dereferencing the returned pointer remains protected by the hardware dependency mechanism.
Impact on Generated Machine Code
The primary impact of [[carries_dependency]] is on
instruction generation rather than execution correctness:
- Elimination of Fences: On architectures that do not
reorder data-dependent reads (e.g., ARM, AArch64), preserving the
dependency chain allows the compiler to omit instructions like
dmb(Data Memory Barrier). - Reduced Register Spilling: Without the need to emit barriers or reorganize register allocation to satisfy conservative barrier semantics, code density increases and execution pipeline stalls decrease.
- Architecture-Neutral Guarantees: On strongly
ordered architectures like x86, loads inherently possess acquire
semantics, meaning
[[carries_dependency]]typically produces no difference in assembly. On weakly ordered architectures, however, it serves as the mechanism that makesmemory_order_consumeusable in modular, non-inlined codebases.