Does the C/C++ memory model allow atomics at different granularities for the same bytes?

Viewed 125

Suppose that I have the following type:

struct T {
  int32 high;
  int32 low;
};

Is it defined behavior to perform atomic accesses (using, e.g. atomic_load, atomic_fetch_add) on all of x, &x->high, and &x->low (assuming U* x)?

My understanding is that the C/C++ memory models are defined using histories of over individual locations (to accommodate weak memory architectures). If accesses can cross locations, does this mean synchronization across locations? If that is the case, then I assume that would imply that histories are essentially per-byte and accessing an int is just like synchronizing across the underlying 4 (or 8) bytes.

edit: revised the example to avoid the union since the main part of the question is about the concurrency model.

edit: revised to use the standard atomics from stdatomic.h

2 Answers

For C11/C18 (I cannot speak about C++) the Standard atomic_xxx() functions of <stdatomic.h> are only defined to take _Atomic qualified arguments. So to do atomic_xxx() operations on the fields of your struct T you would need:

struct T {
  _Atomic int32 high;
  _Atomic int32 low;
} ;

struct T foo, bar ;

and then you would be able to do (for example) atomic_fetch_add(&foo->high, 42). But bar = atomic_load(&foo) would be undefined.

Conversely, you could have:

struct T {
  int32 high;
  int32 low;
} ;

_Atomic struct T foo ;
        struct T bar ;

and now bar = atomic_load(&foo) is defined. But access to any individual field in foo is undefined -- whether or not it's _Atomic.

Going by the Standard, _Atomic xxxx objects should be thought of as entirely distinct from "ordinary" xxxx objects -- they may have different sizes, representations and alignments. Casting an xxxx to/from an _Atomic xxxx is, therefore, no more sensible than casting one struct to/from another, different struct.

But, for gcc and the __atomic_xxx() built-ins, you can do whatever the processor will support. Indeed, for gcc the (otherwise) standard atomic_xxx() will accept arguments which are not_Atomic qualified types, and are mapped to the built-ins. clang, on the other hand, treats passing a not _Atomic qualified type to the standard functions as an error. IMHO this is a bug in gcc's <stdatomic.h>.

Loading an inactive union member yields an indeterminate value regardless of using or not using __atomic_load to do that load.

Related