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:

  1. 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 upstream memory_order_consume load to memory_order_acquire before the call.

  2. 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 with std::memory_order_consume and 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 makes memory_order_consume usable in modular, non-inlined codebases.