Doc. No.: WG21/N4013
Revision of: WG21/N4013
Date: 2014-05-26
Reply to: Hans-J. Boehm
Email: [email protected]

N4013: Atomic operations on non-atomic data

C++11 atomic operations are designed to only apply to atomic types; We cannot use an atomic operation on a plain integer. There are great reasons for that:

  1. Without a separate type, it is easy to forget to identify especially atomic loads of concurrently modified data. This often results in subtle "word-tearing" or memory ordering issues, which are nearly impossible to debug. Since it also violates the compilers assumptions about when variables change, it can also result in even more subtle "mis"compilation.
  2. There is no reasonable way to completely portably apply atomic operations to data not declared as atomic. On a machine on which an "int" is a 32-bit quantity, aligned on a 16-bit boundary, an int may straddle a cache line. This usually prevents hardware supported atomic accesses, and would force the use of a lock for 32-bit atomic operations, largely defeating the point of the construct.

Nonetheless, such access is possible on the vast majority of interesting cases, in which T and atomic<T> have bitwise identical representations and identical alignment constraints. For example, the Linux kernel assumes that atomic operations on int and similar types are supported.

When we designed the C++11 atomics, I was under the misimpression that it would be possible to semi-portably apply atomic operations to data not declared to be atomic, using code such as

int x; reinterpret_cast<atomic<int>&>(x).fetch_add(1);

This would clearly fail if the representations of atomic<int> and int differ, or if their alignments differ. But I know that this is not an issue on platforms I care about. And, in practice, I can easily test for a problem by checking at compile time that sizes and alignments match.

However this is not guaranteed to be reliable, even on platforms on which one might expect it to work, since it may confuse type-based alias analysis in the compiler. A compiler may assume that an int is not also accessed as an atomic<int>. (See 3.10, [Basic.lval], last paragraph.)

Here we address the question of whether there should be some mechanism for applying atomic operations to non-atomic data. As pointed out in the next section, we address a somewhat different set of problems from prior discussion of non-atomic operations on atomic data (c.f. the last part of N3710 or the Issaquah discussion.

Why do we need atomic operations on plain data?

There is a strong argument that without any legacy considerations, all data that must be accessed atomically should be declared appropriately as atomic<T>. And we want to strongly encourage that practice.

But we do have large legacy code bases, many of which have declared “atomic” variables as e.g. volatile int instead of atomic<int>. Such code typically uses various platform-dependent atomics libraries, or uses vendor-specific extensions (such as the gcc __sync primitives or the Microsoft Interlocked primitives). There are great reasons for such code to switch to the C++11 primitives: Well-defined semantics, better control over memory ordering, better portability, better expected support by future compilers. Replacing the legacy primitives with the corresponding C++11 primitives would be an easy gain. But it is often difficult to update all data structure declarations and function prototypes to reflect the fact that some data must occasionally be accessed atomically. Such changes tend to be viral and affect much of the code base, even pieces that don't deal with atomic operations.

A good example of that is an interpreter for a language L that itself supports atomic access, possibly on a per-access basis. To express this correctly in C++11, every memory location that might conceivably be updated atomically should be declared as an atomic object. That would require non-atomic operations on atomics to implement the remaining operations. (One could instead declare memory as a byte array or union, But I don't believe that can be made fully correct, if object initialization in L is non-atomic, as it is in C++11.) This is likely to require pervasive changes to the interpreter for L. (Not to mention requiring non-atomic accesses to atomics, which we don't yet have.)

Another good example of a legacy code base that currently uses per-access atomicity is the Linux kernel. And Linus Torvalds has in the past expressed concerns about moving to the C11 model because it requires atomically updateable objects to be identified in their declaration.

Strawman proposal

In order to solve this problem, we need two facilities: One to test whether the platform supports conversion between T and atomic<T>, and a facility to actually perform the conversion.

Depending on whether there is WG14 interest, the convertibility test could use macros, and/or could use a type trait to test whether an implementation supports interpreting a T& as an atomic<T>&:

template<class T> class convertible_to_atomic<T>

An instance has a true value member if and only if the conversion function below has well-defined behavior and results in a reference to an atomic. (Open issue: How should this behave if T is not a valid argument to atomic?) Unlike is_lock_free(), the result should always be determinable at compile time.

The function to perform the conversion would be a template in C++, and possibly again a type-generic macro in C:

template<class T> atomic<T>* as_atomic(volatile T*);

If convertible_to_atomic<T> holds, the result of as_atomic can be used to update the reference passed to it; such updates are atomic, while accesses directly to the argument are not. The conversion function should probably traffic in pointers, rather than references, for C compatibility.

(Open issue: Paul McKenney points out that the treatment of volatile arguments requires some care and choices. There is an argument for preserving volatility of the argument, yielding a volatile atomic<T> if the argument is volatile. However there are also many cases in which this is undesirable, because the original code uses volatile as a replacement for atomic. Two variants may be required to handle this, or we could require programmers to address the issue with an explicit const_cast.)

Implementation

For most implementations as_atomic is an identity function, and convertible_to_atomic<T> returns true for all types T, or at least those that can be used as an argument to atomic<>. Even lock-based implementations should be fine, so long as an external lock table is used.

The presence of these functions would imply that if convertible_to_atomic<T> holds for T, then the compiler has to view T& and atomic<T>& as potential aliases. We suspect that for most implementations this is already true, since the implementation of atomic<T> contains a T field under the covers. But this requires further investigation.

Thanks

Clark Nelson, Paul McKenney, and Lawrence Crowl provided helpful comments.