C++ Logo

std-discussion

Advanced search

std::atomic_ref<T> enables mixed size and mixed atomicity accesses which are not defined under C++11 memory model

From: Andy Wang <cbeuw.andy_at_[hidden]>
Date: Wed, 25 May 2022 23:24:25 +0100
 The C++11 memory model and later revisions are defined in terms of "atomic
objects". This restricts the amount of operations performable on atomically
accessed locations. Notably, it was not possible to non-atomically access a
std::atomic<T>, or access the first 16 bits of a std::atomic<int32_t>
(whether atomically or non-atomically). The formalisation of the memory
model was done on this basis:

The Problem of Programming Language Concurrency Semantics
<https://www.cl.cam.ac.uk/~jp622/the_problem_of_programming_language_concurrency_semantics.pdf>,
Batty et al.

The C++11 standard prose refers to “atomic objects” as if they are quite
> different from non-atomic objects, and the mathematical model of Batty et
> al. [8] for the C++11 and C11 concurrency primitives followed suit by
> imposing a simple type discipline: a location kind map in each candidate
> execution partitioned locations into atomic, nonatomic, and mutex
> locations. The definition of consistent execution permitted atomic accesses
> only at atomic locations, and the only nonatomic accesses allowed at atomic
> locations were atomic initialisations.
>

Mixed-Size Concurrency: ARM, POWER, C/C++11, and SC
<https://www.cl.cam.ac.uk/~pes20/popl17/mixed-size.pdf>, Flur et al.

In the ISO C standard mixed-size overlapping atomic accesses are forbidden
> by the effective type rules
>

Both mixed size and mixed atomicity accesses are now possible in C++20 with
std::atomic_ref<T>. Note that one is not allowed to access the underlying
pointer of atomic_ref during its lifetime, this ensures that any such two
types of accesses cannot race with any other accesses. Nonetheless, the
memory model was written without considering even these restricted
scenarios:

https://godbolt.org/z/5s4Prvax6

std::pair<int16_t, int16_t> mixed_size() {
    int32_t x = 0;

    {
        auto x_atomic = std::atomic_ref<int32_t>(x);
        x_atomic.store(0xabbafafa, std::memory_order_relaxed);
        // atomic_ref lifetime ends so we can use x again
    }

    auto x_parts = reinterpret_cast<int16_t*>(&x);
    int16_t& x_left = x_parts[0];
    int16_t& x_right = x_parts[1];

    auto x_left_atomic = std::atomic_ref<int16_t>(x_left);
    auto x_right_atomic = std::atomic_ref<int16_t>(x_right);

    // Atomic loads have to read-from a store
    // These both read-from line 11 but they read different things???
    int16_t left = x_left_atomic.load(std::memory_order_relaxed);
    int16_t right = x_right_atomic.load(std::memory_order_relaxed);
    return std::pair<int16_t, int16_t>(left, right);
}
std::int32_t mixed_atomicity() {
    int32_t x = 0;

    {
        auto x_atomic = std::atomic_ref<int32_t>(x);
        x_atomic.store(42, std::memory_order_relaxed);
        // atomic_ref lifetime ends so we can use x again
    }

    // Obviously reading 42 is sane here, but there is no codified semantics
    // governing non-atomically reading an atomic location in any
circumstance
    int32_t read = x;
    return read;
}

If it is indeed fine to perform a mixed size and/or atomicity access when
it is related by happens-before with all other accesses, then we should
update the standard to reflect this. I think the simple Read-Read and
Write-Read coherence rules should be applicable.

Andy

Received on 2022-05-25 22:24:37