C++2a synchronisation inefficient in GCC 11

Thomas Rodgers rodgert@appliantology.com
Mon Mar 1 17:46:08 GMT 2021


I discussed the prematureness of the contention optimization with 
ogiroux this morning. Confirming, libc++ does do this, and in his 
measurements it saves on the order of 50us.

Regarding the use of _Hash_impl::hash(), that can be changed to just use 
the pointer value itself. This is what libatomic does. I'm happy to 
propose that as a subsequent patch to the most recent version of this 
code which I submitted recently.

On 2021-02-26 19:01, Thomas Rodgers wrote:

> On 2021-02-26 17:29, Thomas Rodgers wrote:
> 
>> On 2021-02-26 17:13, Thomas Rodgers wrote:
>> 
>> We are SPECIFICALLY NOT committing to an ABI for wait/notify with 
>> GCC11. These features are experimental and I have no intention of 
>> treating this as a blocker.
>> 
>> On 2021-02-25 14:50, Thiago Macieira via Libstdc++ wrote:
>> 
>> Hello all
>> 
>> I was investigating the implementation of std::atomic<T>::wait to use 
>> inside
>> my own code (Qt's QMutex and QSemaphore), when I realised that it has
>> huge inefficiencies. Since everything in this implementation is inline 
>> and,
>> once released, it will tie our hands until the next ABI break 
>> (libstdc++.so.
>> 7). And no, inline namespaces and ABI tags are no different than ABI 
>> breaks,
>> they only make the ABI break more or less silent in the process.
>> 
>> Here's a summary of the findings:
>> 
>> 1) everything is inline
>> 2) futex code is still behind a lot of code calling into 
>> std::_Hash_bytes
> 
> It's likely possible to remove the dependence on _Hash_bytes. I looked 
> at it while working on the most recent version of this code but decided 
> to not touch it for GCC11 because...it's experimental and we
> aren't committing to an ABI.
> 
>> 3) other int-sized (incl. enums) atomics don't use futex
> 
> The most recent version of this code extends the range of types which 
> use futex to all 32bit types.
> 
>> 4) std::latch and std::counting_semaphore defaults preclude from using
>> futex on Linux
>> 5) std::barrier implementation also uses a type that futex(2) can't 
>> handle
>> 
>> Here's what the code currently looks like: 
>> https://gcc.godbolt.org/z/MeP8rr
>> 
>> Given how far we are in the release cycle, a simple solution is to 
>> simply
>> disable the entire solution by #if 0'ing out bits/atomic_wait.h. If
>> __cpp_lib_atomic_wait isn't defined, the rest of the functionality 
>> cascades
>> disabled too. Then we can spend however long is necessary to get a 
>> good
>> implementation for GCC 12.
>> 
>> I have patches for all but the first issue, but they are a result of 
>> only 3
>> hours of hacking to prove the concept. I will post them, but I do not 
>> expect
>> them to get merged as-is.
>> 
>> Details:
>> 
>> 1) everything is inline
>> 
>> I think this is unnecessary optimisation, especially since it expands 
>> to a LOT
>> of code (see link above). A cursory research into libc++ and MS STL 
>> shows that
>> the actual wait and notification are out-of-line. I recommend we do 
>> the same
>> for libstdc++: if we're going to syscall anyway, the extra function 
>> call isn't
>> going to be the performance bottleneck.
>> 
>> In <https://gcc.gnu.org/pipermail/libstdc++/2020-May/050360.html>, 
>> Jonathan
>> wrote:
>> 
>> Eventually we'll want to move the rest of this function (which doesn't
>> depend on the template argument) into the compiled library, but it's
>> better to be header-only for now.
>> But that has never happened.
>> 
>> Having out-of-line implementations would allow the implementation for 
>> generic-
>> sized atomic wait/notify to fine-tune as time and experience 
>> progresses. The
>> std::__detail::__waiter implementation looks clever, but it's unclear 
>> if it is
>> the best for all workloads. Moreover, the implementation is limited to 
>> 16
>> waiters.
>> 
>> Moreover, it would allow us to have support in the future for a 64-bit 
>> futex.
>> Linus did shoot down any need for it 13 years ago, but the world has 
>> changed
>> since and it's not inconceivable to have a pointer-sized futex on 
>> every
>> platform, which on 64-bit would be 64-bit. Windows and Darwin do have 
>> support
>> for 64-bit compare-and-wait functionality.
>> 
>> And it would allow us to have futex-equivalent support in other OSes, 
>> added at
>> our leisure. The current implementation does not support Windows's
>> WaitOnAddress and could never support it if released in the current 
>> state.
>> It's also possible other OSes will eventually add equivalent 
>> functionality
>> because of the C++ standard. For example, FreeBSD already has futex() 
>> support
>> inside its kernel in order to run Linux binaries, but it is not 
>> exposed to the
>> FreeBSD-personality syscall ABI.
>> 
>> Inlining certain functionality at a later date is possible. 
>> De-inlining isn't.
>> 
>> 2) futex code for int is still behind a lot of boiler plate
>> 
>> It comes from the creation/use of std::__detail::__waiter in
>> std::__atomic_wait and __atomic_notify. That does allow one to detect 
>> whether
>> there is contention in the first place and avoid the system call, but 
>> I think
>> that's premature optimisation.
>> 
>> In <https://gcc.gnu.org/pipermail/libstdc++/2020-May/050362.html>, 
>> Thomas
>> says:
>> 
>> __atomic_notify checks to see if that count is non-zero
>> before invoking the platform/semaphore notify because it is cheaper
>> to do the atomic load than it is to make the syscall() when there are 
>> no
>> waiters.
>> That's true, but it ignores the fact that users of 
>> std::atomic<T>::wait() are
>> likely to track contention by themselves on the value of the atomic (I 
>> know I
>> do). libc++, MS STL and the original atomic_wait[1 [1]] implementation 
>> operate on
>> direct system calls and don't try to track contention. Because of 
>> that,
>> libstdc++ adding an extra layer of tracking is double book-keeping and
>> violates "don't pay for what you don't need".
>> 
>> [1] https://github.com/ogiroux/atomic_wait
> 
> THe final version that Olivier committed to libc++ does track 
> contention.
> 
>> 3) other int-sized atomics don't use futex
>> 
>> The kernel doesn't care what we store in those 32 bits, only that they 
>> are
>> comparable (FUTEX_OP does provide less-than and greater-than 
>> operations that
>> are signed, but we don't use that and the kernel API can be later 
>> extended to
>> unsigned compares). Therefore, I would recommend we extend futex 
>> support to
>> all 32-bit signed scalar types. This would add:
>> * pointers and long on 32-bit architectures
>> * unsigned
>> * untyped enums and typed enums on int & unsigned int
>> * float
>> 
>> 4) std::latch and std::counting_semaphore defaults too high
>> 
>> The standard requires a ptrdiff_t in the API, but it does allow us to 
>> set hard
>> limits and/or defaults for best performance. Right now, std::latch is
>> implemented using ptrdiff_t internally and std::counting_semaphore's 
>> default
>> limit uses PTRDIFF_MAX:
>> 
>> template<ptrdiff_t __least_max_value =
>> __gnu_cxx::__int_traits<ptrdiff_t>::__max>
>> class counting_semaphore
>> 
>> As a consequence, those two classes will use 64-bit atomics on 64-bit
>> architectures, which means they will not use futex. I recommend 
>> changing
>> std::latch's internal to int and lowering the default maximum in
>> std::counting_semaphore to INT_MAX.
>> 
>> 5) std::barrier uses a type that futex can't handle
>> 
>> The barrier phase is the actual type that gets waited / notified on, 
>> but it is
>> an enum class on unsigned char:
>> 
>> enum class __barrier_phase_t : unsigned char { };
>> 
>> This is a harder problem to solve, but I really do recommend making it 
>> 32-bit
>> in width. Whether each barrier phase holds only 256 values or the full 
>> 32
>> bits, I don't know (I've implemented with wrapping on 256).
>> 
>> What is of consequence is that the __state_t type could increase in 
>> size
>> ("could" because we can simply reduce the number of tickets in each 
>> state
>> entry). I haven't done enough research on this to know whether it is 
>> the right
>> way to go.
> 
> My understanding from Olivier Giroux, who's barrier implementation code 
> this is derived from, is that he extensively tested it's performance on 
> a wide variety of hardware, including time on Summit. Short of hard 
> data to the contrary, I am not inclined to second guess the 
> implementation he tested (which uses char tickets).
> 
> Links:
> ------
> [1] https://github.com/ogiroux/atomic_wait


More information about the Libstdc++ mailing list