[PATCH 1/2] Optimize generic spinlock code and use C11 like atomic macros.
Stefan Liebler
stli@linux.vnet.ibm.com
Mon Feb 20 12:15:00 GMT 2017
On 02/18/2017 05:57 PM, Torvald Riegel wrote:
> On Wed, 2017-02-15 at 10:35 +0100, Stefan Liebler wrote:
>> On 02/13/2017 09:29 PM, Torvald Riegel wrote:
>>> Thanks for working on this. Detailed comments below.
>>>
>>> Generally, I think we need to keep working on optimizing this (this can
>>> happen in follow-up patches). For example, we should have
>>> microbenchmarks for the exchange vs. CAS choice.
>>>
>>> Also, some of the tuning knobs such as
>>> SPIN_TRYLOCK_USE_CMPXCHG_INSTEAD_OF_XCHG apply to atomics in general and
>>> not just the spinlock. I'd prefer if these where in a state in which we
>>> could add them as property that's part of the atomics interface.
>>>
>>> On Wed, 2017-02-08 at 15:49 +0100, Stefan Liebler wrote:
>>>> diff --git a/include/atomic.h b/include/atomic.h
>>>> index 7f32640..770db4a 100644
>>>> --- a/include/atomic.h
>>>>
>>>> +++ b/include/atomic.h
>>>>
>>>> @@ -54,7 +54,7 @@
>>>> and following args. */
>>>> #define __atomic_val_bysize(pre, post, mem, ...) \
>>>> ({ \
>>>> - __typeof (*mem) __atg1_result; \
>>>>
>>>> + __typeof ((__typeof (*(mem))) *(mem)) __atg1_result; \
>>>>
>>>> if (sizeof (*mem) == 1) \
>>>> __atg1_result = pre##_8_##post (mem, __VA_ARGS__); \
>>>> else if (sizeof (*mem) == 2) \
>>>> @@ -162,9 +162,9 @@
>>>> /* Store NEWVALUE in *MEM and return the old value. */
>>>> #ifndef atomic_exchange_acq
>>>> # define atomic_exchange_acq(mem, newvalue) \
>>>> - ({ __typeof (*(mem)) __atg5_oldval; \
>>>>
>>>> + ({ __typeof ((__typeof (*(mem))) *(mem)) __atg5_oldval; \
>>>>
>>>> __typeof (mem) __atg5_memp = (mem); \
>>>> - __typeof (*(mem)) __atg5_value = (newvalue); \
>>>>
>>>> + __typeof ((__typeof (*(mem))) *(mem)) __atg5_value = (newvalue); \
>>>>
>>>> \
>>>> do \
>>>> __atg5_oldval = *__atg5_memp; \
>>>> @@ -668,7 +668,7 @@ void __atomic_link_error (void);
>>>>
>>>> # ifndef atomic_load_relaxed
>>>> # define atomic_load_relaxed(mem) \
>>>> - ({ __typeof (*(mem)) __atg100_val; \
>>>>
>>>> + ({ __typeof ((__typeof (*(mem))) *(mem)) __atg100_val; \
>>>>
>>>> __asm ("" : "=r" (__atg100_val) : "0" (*(mem))); \
>>>> __atg100_val; })
>>>> # endif
>>>
>>> You could keep these changes, but it's a bit odd because you only apply
>>> them for the functions you needed them for. I think it would be better
>>> to just remove the volatile qualification in the caller (ie, cast lock
>>> to nonvolatile in pthread_spin_lock etc.
>>>
>> Yes, I've only applied them for the functions needed in spinlock-code.
>> Removing volatile from type pthread_spinlock_t is no option and
>> casting the volatile pointer to a non-volatile pointer is undefined.
>> See comment from Florian:
>> On 12/16/2016 09:12 PM, Florian Weimer wrote:
>>> That's undefined:
>>>
>>> âIf an attempt is made to refer to an object defined with a
>>> volatile-qualified type through use of an lvalue with
>>> non-volatile-qualified type, the behavior is undefined.â
>>>
>>> But we cannot drop the volatile qualifier from the definition of
>>> pthread_spinlock_t because it would change the C++ mangling of
>>> pthread_spinlock_t * and similar types.
>
> Generally, I wouldn't agree with Florian's comment. However, all we are
> interested in is the storage behind the volatile-qualified type. Users
> aren't allowed the object through other means than the pthread_spin*
> functions, so if we cast everywhere, we'll never have any
> volatile-qualified accesses. I believe none of the architectures we
> support makes weaker requirements on alignment for volatile-qualified
> than for non-volatile-qualified types, so I can't see any problem in
> practice with the cast.
>
>
>>>>
>>>> + if (__glibc_likely (atomic_exchange_acquire (lock, 1) == 0))
>>>>
>>>> return 0;
>>>>
>>>> + int val;
>>>>
>>>> do
>>>> {
>>>> /* The lock is contended and we need to wait. Going straight back
>>>> @@ -50,20 +53,39 @@ pthread_spin_lock (pthread_spinlock_t *lock)
>>>> On the other hand, we do want to update memory state on the local core
>>>> once in a while to avoid spinning indefinitely until some event that
>>>> will happen to update local memory as a side-effect. */
>>>> - if (SPIN_LOCK_READS_BETWEEN_CMPXCHG >= 0)
>>>>
>>>> +
>>>>
>>>> +#if SPIN_LOCK_READS_BETWEEN_CMPXCHG >= 0
>>>>
>>>> + /* Use at most SPIN_LOCK_READS_BETWEEN_CMPXCHG plain reads between the
>>>>
>>>> + atomic compare and exchanges. */
>>>>
>> Also changed this comment to:
>> /* Use at most SPIN_LOCK_READS_BETWEEN_CMPXCHG relaxed MO reads between
>> the atomic compare and exchanges. */
>
> Thanks.
>
>>> A while ago we tried hard to remove all code that would fail silently
>>> when a macro had a typo in the name, for example. We still have a few
>>> of them left (e.g., in the atomics), but I think this here doesn't
>>> warrant an exception.
>>>
>> Okay. You're right.
>>
>> In comment of patch 2/2, you've mentioned a header where an architecture
>> shall define those parameters.
>> Thus I've added the file nptl/pthread_spin_parameters.h.
>
> I think the stub version should be in sysdeps/ but the wiki is missing
> this information (default ENOTSUP location):
> https://sourceware.org/glibc/wiki/Style_and_Conventions#Proper_sysdeps_Location
In case we need a new header, shall we move it to sysdeps/nptl/ ?
>
> I think we should just get rid of SPIN_LOCK_READS_BETWEEN_CMPXCHG, and
> make some choice for SPIN_TRYLOCK_LOAD_AND_TEST_BEFORE_XCHG. There is
> no technical reason for throwing in a CAS every now and then, and so far
> we have no evidence that it can improve performance (if that would be
> the case, we'd need to think harder about that, because we have
> spin-waiting loops elsewhere that don't want to modify the value after
> waiting for a change of the value).
Okay. For s390 we don't need a CAS every now and then.
>
> With that applied you don't have to solve the header problem in this
> patch.
>
> I'll write a separate email with suggestions about how to refactor the
> spinlock code project-wide. Also, some more comments on the tuning
> parameters below.
>
> I'm still a bit undecided about how to deal with other arch's
> pthread_spin_lock.c files that just set SPIN_LOCK_READS_BETWEEN_CMPXCHG
> and then include the generic pthread_spin_lock.c. Maybe it's best if we
> just remove them in this patch of yours.
>
I think the archs currently using the generic implementation have just
copied the default value to get rid of the warning "machine-dependent
file should define SPIN_LOCK_READS_BETWEEN_CMPXCHG". But this is only an
assumption.
In general removing those "wrapper"-pthread_spin_lock.c files and using
information from a header like the proposed pthread_spin_parameters.h or
atomic-machine.h is a good approach.
>>>>
>>>> +#if SPIN_TRYLOCK_USE_CMPXCHG_INSTEAD_OF_XCHG == 1
>>>>
>>>> + /* Load and test the spinlock and only try to lock the spinlock if it is
>>>>
>>>> + free. */
>>>>
>>>> + int val = atomic_load_relaxed (lock);
>>>>
>>>> + if (__glibc_likely (val == 0 && atomic_exchange_acquire (lock, 1) == 0))
>>>
>>> I think that's not quite the same as a choice between CAS and exchange.
>> Yes. You are right.
>>> Doing a load first could be helpful for *both* CAS and exchange. OTOH,
>>> if we assume that trylock will succeed most of the time, then the load
>>> is unnecessary and might be more costly (eg, if it causes the state of
>>> the cache line to change twice).
>>>
>> E.g. on s390, there exists no instruction which atomically exchanges the
>> value. Instead a CAS loop is used. The CAS instruction locks the
>> cache-line exclusively whether the value in memory equals the expected
>> old-value or not. Therefore the value is loaded and compared before
>> using the CAS instruction.
>> If no other CPU has accessed the lock-value, the load will cause
>> the state of the cache line to exclusive.
>> If the lock is not acquired, the subsequent CAS instruction does not
>> need to change the state of the cache line.
>> If another CPU has accessed the lock-value e.g. by acquiring the lock,
>> the load will cause the state of the cache line either to read-only
>> or exclusive. This depends on the other CPU - whether it has already
>> stored a new value or not.
>>
>> As this behaviour depends on the architecture, the architecture has to
>> decide whether it should load and test the lock-value before the
>> atomic-macro.
>
> I agree that the there are architecture-specific properties of atomic
> operations that have a significant effect on performance. For s390, you
> list the following points:
> * atomic exchange (I suppose all atomic read-modify-write ops?) are
> implemented through CAS.
atomic exchange is implemented by a CAS loop due to lack of an exchange
instruction. For exchanging to a "0", s390 can use the load-and-and
instruction instead of a CAS loop (See my atomic-machine.h patch; For
gcc, we plan to emit such a load-and-and instruction for builtin
__atomic_exchange_n in future;). This also saves one register usage
compared to the CAS loop.
Exchanging to "0" is e.g. used for lll_unlock macro or in
malloc_consolidate.
Starting with z196 zarch CPUs, the following instructions which are used
by the appropriate __atomic_fetch_xyz builtins instead of a CAS loop:
load-and-add, load-and-and, load-and-exclusive-or, load-and-or.
As information:
I will update my atomic-machine.h patch to use at least some of the C11
atomic builtins or all depending on gcc version.
As additional information:
E.g. RHEL 7 / SLES 12 are using z196 as architecture baseline level.
A 64bit s390x glibc build always use zarch instructions.
A 31bit s390 glibc build does not use zarch instructions by default, but
somebody can build glibc with gcc -mzarch option.
The latter means, that a CAS loop is used for a default 31bit s390 build.
> * CAS always brings the cache line into an exclusive state.
Yes that's true.
>
> (Is the second point correct? I'm not quite sure I understood you
> correctly: for example, you write that a load move a cache line to an
> exclusive state, which seems odd).
Not in all cases, but in the case if no other CPU accesses this cache
line. Otherwise it is read-only.
>
> These properties are specific to the architecture, but they are not
> specific to the synchronization code (eg, to spinlocks). Thus, I think
> such settings should be in the atomics headers (i.e., in
> atomic-machine.h).
> This should probably be a separate patch. I would propose the following
> macros (both are bool):
> /* Nonzero iff all atomic read-modify-write operations (e.g., atomic
> exchange) are implemented using a CAS loop. */
> #define ATOMIC_RMW_USES_CAS 1
Is one macro enough to describe all read-modify-write operations in
include/atomic.h?
Please also see my comment above.
> /* Nonzero iff CAS always assumes it will store, and thus has cache
> performance effects similar to a store (e.g., it always puts the
> respective cacheline into an exclusive state, even if the comparison
> against the expected value fails). */
> ATOMIC_CAS_ALWAYS_PREPARES_FOR_STORE 1
>
> I'm not sure whether there are architectures for which the second macro
> would not be true. It would be good to investigate this, maybe we don't
> need to add this at all.
>
We plan that in future gcc will emit e.g. a load-and-test instruction in
front of the CAS instruction if the old-value is a constant zero.
>
>
> For the spin lock specifically, we have the following possibilities:
>
> 1) If we assume that trylock will most likely succeed in practice:
> * We just do an exchange. The properties above don't matter.
>
> 2) If we want to bias towards cases where trylock succeeds, but don't
> rule out contention:
> * If exchange not a CAS loop, and exchange is faster than CAS, do an
> exchange.
> * If exchange is a CAS loop, use a weak CAS and not an exchange so we
> bail out after the first failed attempt to change the state.
>
> 3) If we expect contention to be likely:
> * If CAS always brings the cache line into an exclusive state, then load
> first. Then do 2).
>
> Do we have consensus yet on whether we assume 2 or 3 (I don't think 1
> buys us anything over 2)?
>
Yes. Sounds good.
>
>>>> diff --git a/nptl/pthread_spin_unlock.c b/nptl/pthread_spin_unlock.c
>>>> index 5fd73e5..f83b696 100644
>>>> --- a/nptl/pthread_spin_unlock.c
>>>>
>>>> +++ b/nptl/pthread_spin_unlock.c
>>>>
>>>> @@ -23,7 +23,9 @@
>>>> int
>>>> pthread_spin_unlock (pthread_spinlock_t *lock)
>>>> {
>>>> - atomic_full_barrier ();
>>>>
>>>> - *lock = 0;
>>>>
>>>> + /* The atomic_store_release synchronizes-with the atomic_exchange_acquire
>>>>
>>>> + or atomic_compare_exchange_weak_acquire in pthread_spin_lock /
>>>>
>>>> + pthread_spin_trylock. */
>>>>
>>>> + atomic_store_release (lock, 0);
>>>>
>>>> return 0;
>>>> }
>>>
>>> I agree with this change. However, depending on how one interprets
>>> POSIX' memory model, one may expect lock releases to be sequentially
>>> consistent. Nonetheless, IMO, glibc should use only release MO.
>>>
>>> But we need to announce this change. Some of us have been considering
>>> an additional section in the manual were we specify where we deviate
>>> from POSIX; this change might be the first we're listing there (though
>>> to be fair, this is a deviation only on some architectures because on
>>> others such as powerpc I think, we've been using release MO forever).
>>>
>>>
>> As not all architectures use the generic spinlock implementation, what
>> about a NEWS entry like:
>>
>> * The new generic spinlock implementation uses C11 like atomics.
>> The pthread_spin_unlock implementation is now using release
>> memory order instead of sequentially consistent memory order.
>
> That's a good start, but I think we need to be more specific. We also
> don't need to say that we use C11-like atomics because that's an
> implementation detail.
>
> I'd add something like this instead:
>
> The synchronization that pthread_spin_unlock performs has been changed
> to now be equivalent to a C11 atomic store with release memory order to
> the spin lock's memory location. This ensures correct synchronization
> for the spin lock's operations and critical sections protected by a spin
> lock. Previously, several (but not all) architectures used stronger
> synchronization (e.g., containing what is often called a full barrier).
> This change can improve performance, but may affect odd fringe uses of
> spin locks that depend on the previous behavior (e.g., using spin locks
> as atomic variables to try to implement Dekker's mutual exclusion
> algorithm).
>
> This should highlight the potential change for the weird (ab)uses of
> spin locks but also makes it clear that this won't affect correctness of
> all the common code.
>
Okay. The next patch-iteration will use the text above.
More information about the Libc-alpha
mailing list