Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is std::atomic<T>::is_lock_free() not static as well as constexpr?

Can anyone tell me whether std::atomic<T>::is_lock_free() isn't static as well as constexpr? Having it non-static and / or as non-constexpr doesn't make sense for me.

Why wasn't it designed like C++17's is_always_lock_free in the first place?

like image 838
Bonita Montero Avatar asked Nov 12 '19 10:11

Bonita Montero


4 Answers

As explained on cppreference:

All atomic types except for std::atomic_flag may be implemented using mutexes or other locking operations, rather than using the lock-free atomic CPU instructions. Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.

The C++ standard recommends (but does not require) that lock-free atomic operations are also address-free, that is, suitable for communication between processes using shared memory.

As mentioned by multiple others, std::is_always_lock_free might be what you are really looking for.


Edit: To clarify, C++ object types have an alignment value that restricts the addresses of their instances to only certain multiples of powers of two ([basic.align]). These alignment values are implementation-defined for fundamental types, and need not equal the size of the type. They can also be more strict than what the hardware could actually support.

For example, x86 (mostly) supports unaligned accesses. However, you will find most compilers having alignof(double) == sizeof(double) == 8 for x86, as unaligned accesses have a host of disadvantages (speed, caching, atomicity...). But e.g. #pragma pack(1) struct X { char a; double b; }; or alignas(1) double x; allows you to have "unaligned" doubles. So when cppreference talks about "aligned memory accesses", it presumably does so in terms of the natural alignment of the type for the hardware, not using a C++ type in a way that contradicts its alignment requirements (which would be UB).

Here is more information: What's the actual effect of successful unaligned accesses on x86?

Please also check out the insightful comments by @Peter Cordes below!

like image 86
Max Langhof Avatar answered Nov 16 '22 00:11

Max Langhof


std::atomic<T>::is_lock_free() may in some implementations return true or false depending on runtime conditions.

As pointed out by Peter Cordes in comments, the runtime conditions is not alignment, as atomic will (over-)align internal storage for efficient lock-free operations, and forcing misalignment is UB that may manifest as loss of atomicity.

It is possible to make an implementation that will not enforce alignment and would do runtime dispatch based on alignment, but it is not what a sane implementation would do. It only make sense to support pre-C++17, if __STDCPP_DEFAULT_NEW_ALIGNMENT__ is less than required atomic alignment, as overalignment for dynamic allocation does not work until C++17.

Another reason where runtime condition may determine atomicity is runtime CPU dispatch.

On x86-64, an implementation may detect the presence of cmpxchg16b via cpuid at initialization, and use it for 128-bit atomics, the same applies to cmpxchg8b and 64-bit atomic on 32-bit. If corresponding cmpxchg is not found, lock-free atomic is unimplementable, and the implementation uses locks.

MSVC doesn't do runtime CPU dispatch currently. It doesn't do it for 64-bit due to ABI compatibility reasons, and doesn't do it for 32-bit as already doesn't support CPUs without cmpxchg8b. Boost.Atomic doesn't do this by default (assumes cmpxchg8b and cmpxhg16b presence), but can be configured for the detection. I haven't bothered to look what other implementations do yet.

like image 31
Alex Guteniev Avatar answered Nov 16 '22 00:11

Alex Guteniev


You may use std::is_always_lock_free

is_lock_free depends on the actual system and can't be determined at compile time.

Relevant explanation:

Atomic types are also allowed to be sometimes lock-free, e.g. if only aligned memory accesses are naturally atomic on a given architecture, misaligned objects of the same type have to use locks.

like image 43
darune Avatar answered Nov 16 '22 01:11

darune


I've got installed Visual Studio 2019 on my Windows-PC and this devenv has also an ARMv8-compiler. ARMv8 allows unaligned accesses, but compare and swaps, locked adds etc. are mandated to be aligned. And also pure load / pure store using ldp or stp (load-pair or store-pair of 32-bit registers) are only guaranteed to be atomic when they're naturally aligned.

So I wrote a little program to check what is_lock_free() returns for an arbitrary atomic-pointer. So here's the code:

#include <atomic>
#include <cstddef>

using namespace std;

bool isLockFreeAtomic( atomic<uint64_t> *a64 )
{
    return a64->is_lock_free();
}

And this is the disassembly of isLockFreeAtomic

|?isLockFreeAtomic@@YA_NPAU?$atomic@_K@std@@@Z| PROC
    movs        r0,#1
    bx          lr
ENDP

This is just returns true, aka 1.

This implementation chooses to use alignof( atomic<int64_t> ) == 8 so every atomic<int64_t> is correctly aligned. This avoids the need for runtime alignment checks on every load and store.

(Editor's note: this is common; most real-life C++ implementations work this way. This is why std::is_always_lock_free is so useful: because it's usually true for types where is_lock_free() is ever true.)

like image 35
Bonita Montero Avatar answered Nov 15 '22 23:11

Bonita Montero