1. 5
  1. 2

    The implementation of _Atomic by GCC was rushed with inadequate review. At some point, I’d like to define a new ABI that adds a lock word to any atomic types that can’t be represented with native atomic operations for the target (with futex-like interfaces in the kernel, this can be very efficient). Supporting this kind of use was part of the original design rationale.

    I totally agree with the error-checking analogy. If you do ‘ordinary’ arithmetic on an atomic variable in C/C++, you get sequentially consistent behaviour. This is almost always correct, just slow. You need explicit operations only for performance (in C++, I’d love to have the default memory ordering in the type as well, but you can’t have everything). There are some exceptions. If you use operators like ++, you get atomic read-modify-write, which is probably what you meant, but if you do an assignment with a load of the same variable on the right (e.g. something like a = a + b instead of a += b) then you will get something racy. Having the atomic as part of the type makes it possible to add compiler warnings for this kind of pattern.

    1. 1

      I’d like to define a new ABI

      I am very curious how you intend to make that happen :)

      1. 1

        At some point, I’d like to define a new ABI that adds a lock word to any atomic types that can’t be represented with native atomic operations for the target (with futex-like interfaces in the kernel, this can be very efficient).

        When would you ever want this behaviour?

        The only times you use atomics are when you need specific performance guarantees, i.e. performance is correctness, and when you’re implementing the concurrency primitives yourself. In both cases adding mutexes around the ops breaks the code.

        The C++ atomic stuff does behave like you described which is why you have to static_assert(std::atomic::<T>::is_always_lock_free) (or fall back to the intrinsics pre C++17, which then breaks relacy/cdschecker…) in any code that uses it.

        1. 1

          When would you ever want this behaviour?

          Because the current implementation is painful.

          The only times you use atomics are when you need specific performance guarantees, i.e. performance is correctness, and when you’re implementing the concurrency primitives yourself. In both cases adding mutexes around the ops breaks the code.

          That is how they are implemented now. Anything larger than a certain size calls out to the atomics support library, which has a fixed-size table of mutexes and acquires the lock corresponding to the object being operated on. Because the table is fixed size, there’s a potential for contention between unrelated atomics.

          The C++ atomic stuff does behave like you described which is why you have to static_assert(std::atomic::::is_always_lock_free) (or fall back to the intrinsics pre C++17, which then breaks relacy/cdschecker…) in any code that uses it.

          Nope, C++‘s std::atomic<T> is layout-compatible with _Atomic(T) (this was not guaranteed by the standard, now it is thanks to Hans Boehm’s paper), it does not have an inline lock and will call back to exactly the same calls to the libatomics libraries.

          Oh, and the intrinsics will also turn into the libcalls for anything where is_always_lock_free would return false.

          1. 1

            Ah so I misunderstood what you were pushing for.

            Oh, and the intrinsics will also turn into the libcalls for anything where is_always_lock_free would return false.

            Right, been a while since I worked with this stuff. Footguns all the way down :( EDIT: with the tiny silver lining that if you don’t -latomic they at least turn into link errors.

            1. 1

              Footguns all the way down

              Yup. The original WG21 proposal was not bad but the way WG14 ported it to C was a disaster. For example, a bunch of the functions in stdatomic.h have volatile qualifiers, when they should just need _Atomic. GCC then made it worse by implicitly assuming that _Atomic(T) and T had the same representation.