C++ Data-Dependency Ordering: Function Annotation

ISO/IEC JTC1 SC22 WG21 N2493 = 08-0003 - 2008-02-03

Paul E. McKenney, paulmck@linux.vnet.ibm.com
Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org

Introduction

Data dependency ordering can provide significant performance improvements to concurrent data structures that are read frequently and seldom modified. The rationale and primary design for data dependency ordering is in the primary proposal, N2492. An understanding of that proposal is necessary to understanding this proposal.

Reasonable compilation strategies for data dependencies will truncate the dependencies at function boundaries when the implementations of those functions are unknown or unmodifiable. This document presents function annotations that assist compilers in following those data dependencies across function and translation-unit boundaries, avoiding prematurely truncating the data dependency tree, and thus improving program performance and scalability.

This proposal does not affect existing standard library functions. Such changes (for example, annotating the Vector templates) were considered, but rejected because current uses of data dependency ordering are generally restricted to highly tuned concurrent data structures using only basic operations such as indirection, field selection, array access, and casting. Longer term experience might indicate that a future proposal affecting library classes is warranted, however, there is insufficient motivation for such a proposal at this time.

This proposal is based on N2153 by Silvera, Wong, McKenney, and Blainey, on N2176 by Hans Boehm, on N2195 by Peter Dimov, on N2359, N2360, and N2361 by Paul E. McKenney, on discussions on the cpp-threads list, on discussions in the concurrency workgroup at the 2007 Oxford and Toronto meetings, and in particular discussions with Hans Boehm.

Proposal

We propose to annotate function declarations so that compilers may assume that compilation on the other side of the the function boundary will properly respect data dependency ordering. In analogy with the definition of data depencency ordering, we use the annotation [[carries_dependency]] to indicate that the compiler should not truncate the dependency tree. Such annotations attach to parameter declarations, and to the function declaration for its return value.

If a given function is annotated, the compilation of the caller must preserve dependency ordering on the function return value. If a particular argument of a given function is annotated, the compilation of the callee must preserve dependency ordering on the function argument.

We believe the syntax of the attributes is consistent with N2418 Towards support for attributes in C++ (Revision 3). In any event, we will adapt this proposal to the final attribute proposal.

For example, the following carries dependencies through argument y to the return value:

int *f(int *y [[carries_dependency]]) [[carries_dependency]]
{
        return y;
}

The following example carries dependency trees in, but not out:

int f(int *y [[carries_dependency]])
{
        return *y;
}

The following carries dependency trees out, but not in:

struct foo *f(int i) [[carries_dependency]]
{
        return &foo_head[i].load(memory_order_consume);
}

In N2176, Hans Boehm lists a number of example optimizations that can break dependency trees. Most of these are addressed in N2492, the last is covered below.

N2176 example code:

r1 = x.load(memory_order_consume);
if (r1)
	f(&y);
else
	g(&y);

In this case, there is no data dependency leading into f() and g(), so this code-dependency example is out of scope. Modifying the example by replacing &y with r1 to create a data dependency leading into the two functions:

r1 = x.load(memory_order_consume);
if (r1)
	f(r1);
else
	g(r1);

In this case, an implementation might emit a memory fence prior to calling f() and g(). (Of course, a more sophisticated implementation with visibility into these two functions might be able to optimize this memory fence away). In order to prevent the fence, the programmer would annotate f() and g().

Recoding this based on this proposal and on N2492:

void f(struct foo * p [[carries_dependency]]);
void g(struct foo * p [[carries_dependency]]);

r1 = x.load(memory_order_consume);
if (r1)
	f(r1);
else
	g(r1);

Assuming that x is an atomic, the x.load(memory_order_consume) will form the head of a dependency tree. The [[carries_dependency]] annotations will inform the compiler that it can assume data depencencies are properly respected within f() and g(), so that the compiler need not emit a memory fence prior to invoking these functions.

Alternatives Considered

Implementation

For trivial implementations of data dependency ordering, implementations of [[carries_dependency]] simply ignore this attribute.

For non-trivial implementations of data dependency ordering, there are three implementation options for the [[carries_dependency]] attribute: