Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::atomic<int>: Difference between x.fetch_add(1) and x++;

What is the difference between

extern std::atomic<int> x;
int i = x++;

and

extern std::atomic<int> x;
int i = x.fetch_add(1);

I feel like the second version is safer, but I couldn't see any differences in testing between these two versions.

like image 775
user14717 Avatar asked Aug 15 '14 18:08

user14717


People also ask

What is Fetch_add Atomic?

atomic::fetch_addFetches the value stored in *this , and then adds a specified value to the stored value. C++ Copy.

Is X ++ an atomic?

I know ++ is atomic (if you perform x++; in two different threads "simultaneously", you will always end up with x increased by 2, as opposed to x=x+1 with optimization switched off.)

What does std :: atomic do?

Module std::sync::atomic. Atomic types provide primitive shared-memory communication between threads, and are the building blocks of other concurrent types. Rust atomics currently follow the same rules as C++20 atomics, specifically atomic_ref .


2 Answers

The difference is definitely not about the safety = atomicity which is guaranteed for both methods.

The most important difference I think is that fetch_add() can take a different memory order argument while for increment operator it is always memory_order_seq_cst.

Another obvious difference is that fetch_add() can take not only 1 as argument while on the other hand, operator++ is more likely to be implemented using lock inc instruction (though, theoretically nothing prevents a compiler from such an optimization for fetch_add(1) as well)

So answering your exact question, there is no any semantically important difference between x++ and x.fetch_add(1). The doc says:

This function behaves as if atomic::fetch_add was called with 1 and memory_order_seq_cst as arguments.

like image 137
Anton Avatar answered Oct 21 '22 12:10

Anton


x.fetch_add(1) and x++ are exactly the same for std::atomic

If you believe cppreference, https://en.cppreference.com/w/cpp/atomic/atomic/operator_arith says:

T operator++() volatile noexcept; (1)

T* operator++() volatile noexcept; (2)
  1. Performs atomic pre-increment. Equivalent to fetch_add(1)+1.

  2. Performs atomic post-increment. Equivalent to fetch_add(1).

https://en.cppreference.com/w/cpp/atomic/atomic/fetch_add then documents:

T fetch_add( T arg, std::memory_order order = std::memory_order_seq_cst ) noexcept;

so we see that the std::memory_order of operator++ defaults to std::memory_order_seq_cst, which is the stronger one available, see also: What do each memory_order mean?

C++11 standard quotes

If you don't believe cppreference, the C++11 N3337 draft 29.6.5/33 "Requirements for operations on atomic types" says:

C A ::operator++(int) volatile noexcept;
C A ::operator++(int) noexcept;

Returns: fetch_add(1)

29.6.5/2 clarifies C and A:

  • an A refers to one of the atomic types.
  • a C refers to its corresponding non-atomic type

I wasn't able to find it explained clearly but I suppose Returns: fetch_add(1) implies that fetch_add(1) is called for its side effect of course.

It is also worth looking at the prefix version a bit further:

C A ::operator++() volatile noexcept;
C A ::operator++() noexcept;

Effects: fetch_add(1)

Returns: fetch_add(1) + 1

which indicates that this one returns the value + 1 like the regular prefix increment for integers.

GCC 4.8

libstdc++-v3/include/std/atomic says atomic<int> inherits __atomic_base<int>:

struct atomic<int> : __atomic_base<int>

libstdc++-v3/include/bits/atomic_base.h implements it like:

__int_type
operator++(int) noexcept
{ return fetch_add(1); }

__int_type
operator++(int) volatile noexcept
{ return fetch_add(1); }

__int_type
operator++() noexcept
{ return __atomic_add_fetch(&_M_i, 1, memory_order_seq_cst); }

__int_type
operator++() volatile noexcept
{ return __atomic_add_fetch(&_M_i, 1, memory_order_seq_cst); }

_GLIBCXX_ALWAYS_INLINE __int_type
fetch_add(__int_type __i,
memory_order __m = memory_order_seq_cst) noexcept
{ return __atomic_fetch_add(&_M_i, __i, __m); }

_GLIBCXX_ALWAYS_INLINE __int_type
fetch_add(__int_type __i,
memory_order __m = memory_order_seq_cst) volatile noexcept
{ return __atomic_fetch_add(&_M_i, __i, __m); }

I do not understand why the postfix calls the fetch_add helper and the prefix uses the built-in directly, but in the end they all come down to the GCC intrinsics __atomic_fetch_add and __atomic_add_fetch which do the real work.