Auto-queued ticketlock
Subsystems (10)
RFC PATCH 1/1 Auto-queued ticketlock
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
arch/x86/include/asm/spinlock.h +41 -0
@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired
@@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;
@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);
arch/x86/include/asm/spinlock_types.h +12 -2
@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)
@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
include/linux/kernel.h +1 -0
@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))
kernel/Kconfig.locks +18 -0
@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.
kernel/Makefile +1 -0
@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.o
kernel/tktqlock.c new file +333 -0
333 lines not on this page · raw message
Quoted 24 lines · Paul E. McKenney
arch/x86/include/asm/spinlock_types.h +8
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)
arch/x86/include/asm/spinlock_types.h typedef struct arch_spinlock { · line 30
Quoted 4 lines
arch/x86/include/asm/spinlock_types.h +1
-0@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__
Is there such a thing as a BIG_ENDIAN x86 box? This is in arch/x86/include/asm/spinlock_types.h
-- Steve
Quoted 7 lines
arch/x86/include/asm/spinlock_types.h +3
-0+__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
Quoted 24 lines · Steven Rostedt
Quoted 22 lines · Paul E. McKenney
arch/x86/include/asm/spinlock_types.h +8
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif
Quoted 9 lines
arch/x86/include/asm/spinlock_types.h typedef struct arch_spinlock { · line 30
Quoted 6 lines
arch/x86/include/asm/spinlock_types.h +1
-0#define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__Is there such a thing as a
BIG_ENDIANx86 box? This is inarch/x86/include/asm/spinlock_types.h
Nope. Preparation work for moving this to common code.
Good point though, I should have skipped this #ifdef until I got everything into common code.
Thanx, Paul
Quoted 11 lines
-- Steve
Quoted 7 lines
arch/x86/include/asm/spinlock_types.h +3
-0+__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
Quoted 5 lines · Steven Rostedt
Quoted 1 line · Paul E. McKenney
+#ifdef
__BIG_ENDIAN__Is there such a thing as a
BIG_ENDIANx86 box? This is inarch/x86/include/asm/spinlock_types.h
That's just an habit for people who have been forced to deal with BE CPUs.
The sad thing is that BE CPUs have been designed by folks who blindly expanded BCD computing without understanding the hardware implications.
Unfortunately they managed to inflict BE to the network protocols which in turn manifested the only excuse of keeping the BE nonsense alive.
Thanks,
tglx
Quoted 10 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +9
-0+#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc);
arch/x86/include/asm/spinlock.h static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) · line 85
Quoted 10 lines
arch/x86/include/asm/spinlock.h +10
-0+for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } + barrier(); /* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +
To avoid the above code duplication, I would have this instead:
#ifdef CONFIG_TICKET_LOCK_QUEUED
bool tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc); #define __TKT_SPIN_INC 2
#else
static inline bool tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc)
{
return false;
}#define __TKT_SPIN_INC 1
#endif
static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)
{
register struct __raw_tickets inc = { .tail = __TKT_SPIN_INC };
inc = xadd(&lock->tickets, inc);
for (;;) {
if (inc.head == inc.tail || tkt_spin_pass(lock, inc))
break;
cpu_relax();
inc.head = ACCESS_ONCE(lock->tickets.head);
}
barrier; /* make sure nothing creeps before the lock is taken */
}-- Steve
Quoted 10 lines · Thomas Gleixner
Quoted 5 lines · Steven Rostedt
Quoted 1 line · Paul E. McKenney
+#ifdef
__BIG_ENDIAN__Is there such a thing as a
BIG_ENDIANx86 box? This is inarch/x86/include/asm/spinlock_types.hThat's just an habit for people who have been forced to deal with BE CPUs.
Quoted 7 lines
The sad thing is that BE CPUs have been designed by folks who blindly expanded BCD computing without understanding the hardware implications.
Unfortunately they managed to inflict BE to the network protocols which in turn manifested the only excuse of keeping the BE nonsense alive.
Hey, we had to do -something-, after all, EBCDIC lost out to ASCII!!! ;-)
Thanx, Paul
Quoted 15 lines · Steven Rostedt
Quoted 14 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +13
-0+#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head);
Quoted 8 lines
arch/x86/include/asm/spinlock.h static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) · line 85
Quoted 6 lines
arch/x86/include/asm/spinlock.h +6
-0+ } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +To avoid the above code duplication, I would have this instead:
Nice! I have updated accordingly.
Thanx, Paul
Quoted 35 lines
#ifdef
CONFIG_TICKET_LOCK_QUEUEDbool
tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc); #define__TKT_SPIN_INC2#else
static inline bool
tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc){ return false; }#define
__TKT_SPIN_INC1#endif
static
__always_inlinevoid__ticket_spin_lock(arch_spinlock_t *lock){ register struct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc = xadd(&lock->tickets, inc); for (;;) { if (inc.head == inc.tail || tkt_spin_pass(lock, inc)) break; cpu_relax(); inc.head = ACCESS_ONCE(lock->tickets.head); } barrier; /* make sure nothing creeps before the lock is taken */ }-- Steve
Quoted 20 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Quoted 5 lines
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>
This looks a great idea ;)
arch/x86/include/asm/spinlock.h static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) · line 119
Quoted 6 lines
arch/x86/include/asm/spinlock.h +6
-0+ +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 );
head = xadd(&lock->tickets.head, head);
Quoted 20 lines
kernel/tktqlock.c new file +19
-0+if (head & 0x1) +tkt_q_do_wake(lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + */ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL )
kernel/tktqlock.c line 148
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +}
EXPORT_SYMBOL(tkt_q_do_wake) ?
Hmm, unfortunately I lack time this week to fully read the patch !
Quoted 21 lines · Eric Dumazet
Quoted 20 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Quoted 6 lines
Quoted 4 lines
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>This looks a great idea ;)
Glad you like it! Hopefully workloads like it as well. ;-)
Quoted 8 lines
arch/x86/include/asm/spinlock.h static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) · line 119
Quoted 6 lines
arch/x86/include/asm/spinlock.h +6
-0+ +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 );head = xadd(&lock->tickets.head, head);
Yikes! Good catch, fixed.
Quoted 24 lines
Quoted 24 lines
kernel/tktqlock.c new file +23
-0+if (head & 0x1) +tkt_q_do_wake(lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + */ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax ();
Quoted 8 lines
kernel/tktqlock.c line 148
Quoted 6 lines
kernel/tktqlock.c new file +6
-0+ } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +}
EXPORT_SYMBOL(tkt_q_do_wake)?
Good point, just in case we want to use spinlocks in modules. ;-) Same for tkt_spin_pass(), I guess.
Quoted 1 line
Hmm, unfortunately I lack time this week to fully read the patch !
I suspect that there is very little danger of this patch going in this week, so you should have some additional time. ;-)
Thanx, Paul
kernel/tktqlock.c line 77
Quoted 6 lines · Paul E. McKenney
kernel/tktqlock.c new file +6
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp)
BTW, what does "asp" mean? arch_spinlock? If so, can we just call it "lock" and be consistent with all the other spinlock calls in the kernel. Because, I keep thinking this has something to do with Microsoft dynamic web pages.
-- Steve
Quoted 12 lines
kernel/tktqlock.c new file +12
-0+{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} +
Quoted 10 lines · Steven Rostedt
kernel/tktqlock.c line 77
Quoted 6 lines · Paul E. McKenney
kernel/tktqlock.c new file +6
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp)BTW, what does "asp" mean?
arch_spinlock?
"arch_spinlock pointer", but yes. Or I suppose a millenia-late warning to Cleopatra.
Quoted 4 lines
If so, can we just call it
"lock" and be consistent with all the other spinlock calls in the kernel. Because, I keep thinking this has something to do with Microsoft dynamic web pages.
Fair enough!
Thanx, Paul
Quoted 16 lines
-- Steve
Quoted 12 lines
kernel/tktqlock.c new file +12
-0+{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} +
Quoted 328 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +39
-0+#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +175
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;
kernel/tktqlock.c line 185
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next);
Boy this is tricky code! I thought I found a race window here, but as I went to write my email saying "Gotcha!" I found that it wasn't a race after all. But as I went though the effort of writing this, I figured I would send this out as documentation for others to see. Hmm, I wonder if we can use this email to add more comments. Anyway, here's what I thought was wrong ;-)
OK, I originally thought there was a race window here. Let's say that an NMI hit right here, and it happens to be a big one, where lots of things can happen on other CPUs right now.
The scenario is that there's just one item on the queue, which is waiting for the lock to be released, and is spinning below in the:
while (ACCESS_ONCE(tq.cpu) != -1) cpu_relax();
And then the lock is released, where in tkt_q_do_wake() the following is called:
ACCESS_ONCE(tqp->cpu) = -1;
Now the old queued task is released. But it's tq->next hasn't been set yet, and is still NULL. It leaves by doing:
ACCESS_ONCE(tqhp->spin) = tq.next; return true;
All before this task gets to set *oldtail to &tq. But, I then looked below...
kernel/tktqlock.c line 197
Quoted 12 lines
kernel/tktqlock.c new file +12
-0+ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) {
This checks for that scenario. As if the old task were to come out spinning, the problem would only be if it was the last one on the list, and its tq.next was NULL. But if that was the case, then we set spin to NULL and do the next trick, where I thought I gotcha again...
kernel/tktqlock.c line 205
Quoted 8 lines
kernel/tktqlock.c new file +8
-0+ +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next)
Here, I was thinking, oh wait, what happens if this is called right before the xchg() above. Then we would set spin_tail but not update the old tq.next. But wait! look at what we assign spin_tail to. It's the address of spin, which would be what oldtail would point to above, and then above would set spin to the new tq!
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
This code is starting to make me look like Gollum :-p
-- Steve
Quoted 128 lines
kernel/tktqlock.c new file +128
-0+return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head) &0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (TICKET_T_CMP_GE (ACCESS_ONCE (ap->tickets.tail) -TKT_Q_SWITCH , +ACCESS_ONCE (ap->tickets.head))) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +cpu_relax (); +return false; +}
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
Because "we might hit loads" is just not good enough. I would counter with "hiding problems causes more of them".
Linus
Quoted 15 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
Quoted 7 lines
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
I guess the point of this patch is to lower the cache ping pong effect of spin locks. I believe Rik was doing something similar to this as well.
Now, when we switch from ticket to queue, we basically blow away the old FIFO and all the tasks do a thundering herd to get on the queue. Even if the task was next to get the lock, it could end up being stuck at the back of the queue again and have to wait. When I put my real-time hat on, this bothers me. Even though it's still a bound latency, as it only gets put to the end of the queue once, it just doubled the length of the worse case grabbing of a lock.
-- Steve
Quoted 506 lines
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +333
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head) &0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (TICKET_T_CMP_GE (ACCESS_ONCE (ap->tickets.tail) -TKT_Q_SWITCH , +ACCESS_ONCE (ap->tickets.head))) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +cpu_relax (); +return false; +}
Quoted 14 lines · Linus Torvalds
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
Quoted 6 lines
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
Because "we might hit loads" is just not good enough. I would counter with "hiding problems causes more of them".
Hi, all
Off-topic, although I am in this community for several years, I am not exactly clear with this problem.
1) In general case, which lock is the most competitive in the kernel? what it protects for?
2) In which special case, which lock is the most competitive in the kernel? what it protects for?
3) In general case, which list is the most hot list?
4) In which special case, which list is the most hot list?
thanks,
Lai
Quoted 21 lines · Steven Rostedt
Quoted 20 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Quoted 6 lines
Quoted 2 lines
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
I guess the point of this patch is to lower the cache ping pong effect of spin locks. I believe Rik was doing something similar to this as well.
Yep, as was Michel Lespinasse, IIRC.
Quoted 7 lines
Now, when we switch from ticket to queue, we basically blow away the old FIFO and all the tasks do a thundering herd to get on the queue. Even if the task was next to get the lock, it could end up being stuck at the back of the queue again and have to wait. When I put my real-time hat on, this bothers me. Even though it's still a bound latency, as it only gets put to the end of the queue once, it just doubled the length of the worse case grabbing of a lock.
Almost.
The size of the switch-to-queue thundering herd is limited by the ticket gap that initiates the switch. So what you are saying is that in an RT kernel, you might want to tune down the ticket gap. Which reminds me -- I do need to make this tuning more explicit.
Thanx, Paul
Quoted 512 lines
-- Steve
Quoted 506 lines
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +333
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head) &0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (TICKET_T_CMP_GE (ACCESS_ONCE (ap->tickets.tail) -TKT_Q_SWITCH , +ACCESS_ONCE (ap->tickets.head))) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +cpu_relax (); +return false; +}
Quoted 339 lines · Steven Rostedt
Quoted 337 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +39
-0+#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +184
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */
Quoted 8 lines
kernel/tktqlock.c line 185
Quoted 1 line
kernel/tktqlock.c new file +1
-0+ oldtail =xchg (&tqhp->spin_tail, &tq.next);Boy this is tricky code! I thought I found a race window here, but as I went to write my email saying "Gotcha!" I found that it wasn't a race after all. But as I went though the effort of writing this, I figured I would send this out as documentation for others to see. Hmm, I wonder if we can use this email to add more comments. Anyway, here's what I thought was wrong ;-)
If you didn't know any better, you might even think that I had done something like this before. ;-)
Quoted 25 lines
OK, I originally thought there was a race window here. Let's say that an NMI hit right here, and it happens to be a big one, where lots of things can happen on other CPUs right now.
The scenario is that there's just one item on the queue, which is waiting for the lock to be released, and is spinning below in the:
while (ACCESS_ONCE(tq.cpu) != -1) cpu_relax();And then the lock is released, where in
tkt_q_do_wake()the following is called:ACCESS_ONCE(tqp->cpu) = -1;Now the old queued task is released. But it's
tq->nexthasn't been set yet, and is still NULL. It leaves by doing:ACCESS_ONCE(tqhp->spin) = tq.next; return true;All before this task gets to set *oldtail to &tq. But, I then looked below...
Quoted 14 lines
kernel/tktqlock.c line 197
Quoted 12 lines
kernel/tktqlock.c new file +12
-0+ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) {This checks for that scenario.
Yep!
Quoted 6 lines
As if the old task were to come out
spinning, the problem would only be if it was the last one on the list, and its tq.next was NULL. But if that was the case, then we set spin to NULL and do the next trick, where I thought I gotcha again...
Quoted 14 lines
kernel/tktqlock.c line 205
Quoted 8 lines
kernel/tktqlock.c new file +8
-0+ +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next)Here, I was thinking, oh wait, what happens if this is called right before the
xchg()above. Then we would setspin_tailbut not update the old tq.next. But wait! look at what we assignspin_tailto. It's the address of spin, which would be what oldtail would point to above, and then above would set spin to the new tq!
Yep again!
Quoted 5 lines
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
This code is starting to make me look like Gollum :-p
Hmmm... The time and effort to do this might almost have been worthwhile just to accomplish that! ;-)
But yes, this would need better comments, design documentation, or maybe both.
Thanx, Paul
Quoted 132 lines
-- Steve
Quoted 128 lines
kernel/tktqlock.c new file +128
-0+return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head) &0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (TICKET_T_CMP_GE (ACCESS_ONCE (ap->tickets.tail) -TKT_Q_SWITCH , +ACCESS_ONCE (ap->tickets.head))) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +cpu_relax (); +return false; +}
Quoted 4 lines · Linus Torvalds
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
Heh! You should see what it looks like if you make slightly different design decisions. For example, just you try switching back from queued to ticket mode while there are still CPUs spinning on the lock! ;-)
Quoted 8 lines
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
If the only effect of this patch submission is to give people a bit more motivation to solve the underlying lock-contention problems, I am happy.
Quoted 5 lines
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
Because "we might hit loads" is just not good enough. I would counter with "hiding problems causes more of them".
Agreed. As I said in the first paragraph of the commit log:
... if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
The reason that I created this patch was that I was seeing people arguing for locks optimized for high contention, and the ones that I saw required the developer to predict which locks would encounter high levels of contention. Changes in workloads would of course invalidate those predictions.
But again, if the only effect of this patch submission is to give people a bit more motivation to solve the underlying lock-contention problems, I am happy.
Thanx, Paul
Quoted 24 lines · Lai Jiangshan
Quoted 14 lines · Linus Torvalds
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
Quoted 6 lines
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
Because "we might hit loads" is just not good enough. I would counter with "hiding problems causes more of them".
Hi, all
Quoted 7 lines
Off-topic, although I am in this community for several years, I am not exactly clear with this problem.
1) In general case, which lock is the most competitive in the kernel? what it protects for?
2) In which special case, which lock is the most competitive in the kernel? what it protects for?
3) In general case, which list is the most hot list?
4) In which special case, which list is the most hot list?
Others would know better than I, but mmap_sem has been called out as a prime offender for some workloads. There is of course some debate as to whether the fault lies mmap_sem or with the workloads. There have been some efforts to solve this one on LKML, plus some in academia have worked on this as well:
http://people.csail.mit.edu/nickolai/papers/clements-bonsai.pdf
http://pdos.csail.mit.edu/papers/radixvm:eurosys13.pdf
And IIRC this was the subject of a session at a recent minisummit.
There are a few locks within the RCU implementation that have popped up from time to time on very large systems, but I have dealt with those and have plans for each should it become a problem. The plans probably won't survive first contact with a real workload, but having thought things through is very helpful.
Thanx, Paul
Quoted 450 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +255
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(asp->head_tail); + asnew = asold; + if (asnew.tickets.head & 0x1) { + + /* Someone beat us to it, back out. */ + smp_mb(); + ACCESS_ONCE(tqhp->ref) = NULL; + + /* Spin on the queue element they set up. */ + return tkt_q_do_spin(asp, inc); + } + + /* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |= 0x1;
kernel/tktqlock.c line 265
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +}
Just small revise.
I just move " tqhp->head_tkt = asold.tickets.head;" into the loop, so we can use "asp->tickets.head & 0x1" to
indicates that queued spinlock is prepared instead of by "tqhp->ref == asp".
See the append diff.
(And I guess, after it you can force only the CPUs which "inc.tail - tqhp->head_tkt > TKT_Q_SWITCH"
do queued spin to remove the thundering herd)
kernel/tktqlock.c +2 -6
@@ -27,7 +27,6 @@struct tkt_q {int cpu; -__ticket_t tail;struct tkt_q *next; };
@@ -127,9 +126,8 @@ void tkt_q_do_wake(arch_spinlock_t *asp)struct tkt_q_head *tqhp;struct tkt_q *tqp; -/* If the queue is still being set up, wait for it. */ -while ((tqhp =tkt_q_find_head (asp)) ==NULL ) -cpu_relax (); + tqhp =tkt_q_find_head (asp); +BUG_ON (!tqhp);for (;;) {
@@ -141,8 +139,6 @@ void tkt_q_do_wake(arch_spinlock_t *asp)return ;/* No element, successfully removed queue. */ cpu_relax (); } -if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) -ACCESS_ONCE (tqhp->head_tkt) =-1 ;smp_mb ();/* Order pointer fetch and assignment against handoff. */ ACCESS_ONCE (tqp->cpu) =-1 ; }
@@ -164,20 +160,16 @@ bool tkt_q_do_spin(arch_spinlock_t *asp, struct
__raw_tickets inc)
*/
smp_mb(); /* See above block comment. */
- /* If there no longer is a queue, leave. */
tqhp = tkt_q_find_head(asp);
- if (tqhp == NULL)
- return false;
+ BUG_ON(!tqhp);
/* Initialize our queue element. */
tq.cpu = raw_smp_processor_id();
- tq.tail = inc.tail;
tq.next = NULL;
/* Check to see if we already hold the lock. */
if (ACCESS_ONCE(tqhp->head_tkt) == inc.tail) {
/* The last holder left before queue formed, we hold lock. */
- tqhp->head_tkt = -1;
return true;
}
@@ -251,16 +243,14 @@ tkt_q_init_contend(int i, arch_spinlock_t *asp, struct __raw_tickets inc)
return tkt_q_do_spin(asp, inc);
}
+ /* Point the queue at the lock and go spin on it. */
+ tqhp->head_tkt = asold.tickets.head;
/* The low-order bit in the head counter says "queued". */ asnew.tickets.head |= 0x1;
} while (cmpxchg(&asp->head_tail,
asold.head_tail,
asnew.head_tail) != asold.head_tail);
- /* Point the queue at the lock and go spin on it. */
- tqhp->head_tkt = asold.tickets.head;
- smp_mb(); /* Ensure head_tkt is set prior to queuers seeing tqhp. */
- ACCESS_ONCE(tqhp->ref) = asp;
return tkt_q_do_spin(asp, inc);
}
@@ -282,14 +272,9 @@ bool tkt_q_start_contend(arch_spinlock_t *asp, struct __raw_tickets inc)
* the lock with the corresponding queue.
*/
do {
- /*
- * Use 0x1 to mark the queue in use, but also avoiding
- * any spinners trying to use it before we get it all
- * initialized.
- */
if (cmpxchg(&tkt_q_heads[i].ref,
NULL,
- (arch_spinlock_t *)0x1) == NULL) {
+ asp) == NULL) {
/* Succeeded, now go initialize it. */ return tkt_q_init_contend(i, asp, inc);
Quoted 456 lines · Lai Jiangshan
Quoted 454 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +259
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); +
Quoted 8 lines
kernel/tktqlock.c line 265
Quoted 6 lines
kernel/tktqlock.c new file +6
-0+/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +}Just small revise.
Sorry it is wrong. tkt_q_find_head() will returns wrong.
could we use only tkt_q_heads[tkt_q_hash(asp)] instead of find a free one?
Quoted 102 lines
I just move "
tqhp->head_tkt= asold.tickets.head;" into the loop, so we can use "asp->tickets.head&0x1" to
indicates that queued spinlock is prepared instead of by "tqhp->ref== asp".See the append diff.
(And I guess, after it you can force only the CPUs which "inc.tail -tqhp->head_tkt>TKT_Q_SWITCH"
do queued spin to remove the thundering herd)kernel/tktqlock.c +6
-21@@ -27,7 +27,6 @@struct tkt_q {int cpu; -__ticket_t tail;struct tkt_q *next; };@@ -127,9 +126,8 @@ void tkt_q_do_wake(arch_spinlock_t *asp)struct tkt_q_head *tqhp;struct tkt_q *tqp; -/* If the queue is still being set up, wait for it. */ -while ((tqhp =tkt_q_find_head (asp)) ==NULL ) -cpu_relax (); + tqhp =tkt_q_find_head (asp); +BUG_ON (!tqhp);for (;;) {@@ -141,8 +139,6 @@ void tkt_q_do_wake(arch_spinlock_t *asp)return ;/* No element, successfully removed queue. */ cpu_relax (); } -if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) -ACCESS_ONCE (tqhp->head_tkt) =-1 ;smp_mb ();/* Order pointer fetch and assignment against handoff. */ ACCESS_ONCE (tqp->cpu) =-1 ; }@@ -164,20 +160,16 @@ bool tkt_q_do_spin(arch_spinlock_t *asp, struct__raw_tickets inc) */ smp_mb ();/* See above block comment. */ -/* If there no longer is a queue, leave. */ tqhp =tkt_q_find_head (asp); -if (tqhp ==NULL ) -return false; +BUG_ON (!tqhp);/* Initialize our queue element. */ tq.cpu =raw_smp_processor_id (); - tq.tail = inc.tail; tq.next =NULL ;/* Check to see if we already hold the lock. */ if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) {/* The last holder left before queue formed, we hold lock. */ - tqhp->head_tkt =-1 ;return true; }@@ -251,16 +243,14 @@ tkt_q_init_contend(int i, arch_spinlock_t *asp, struct __raw_tickets inc)return tkt_q_do_spin (asp, inc); } +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head;/* The low-order bit in the head counter says "queued". */ asnew.tickets.head |=0x1 ; }while (cmpxchg (&asp->head_tail, asold.head_tail, asnew.head_tail) != asold.head_tail); -/* Point the queue at the lock and go spin on it. */ - tqhp->head_tkt = asold.tickets.head; -smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ -ACCESS_ONCE (tqhp->ref) = asp;return tkt_q_do_spin (asp, inc); }@@ -282,14 +272,9 @@ bool tkt_q_start_contend(arch_spinlock_t *asp,struct __raw_tickets inc) * the lock with the corresponding queue. */ do { - /* - * Use 0x1 to mark the queue in use, but also avoiding - * any spinners trying to use it before we get it all - * initialized. - */ if (cmpxchg(&tkt_q_heads[i].ref, NULL, - (arch_spinlock_t *)0x1) == NULL) { + asp) == NULL) { /* Succeeded, now go initialize it. */
Quoted 1 line
return tkt_q_init_contend(i, asp, inc);
if (tkt_q_heads[i].ref == asp)
Quoted 2 lines · Paul E. McKenney
Quoted 1 line · Steven Rostedt
On Sun, 2013-06-09 at 12:36 -0700, Paul E. McKenney wrote:
[ . . . ]
Quoted 111 lines
Quoted 30 lines
Quoted 30 lines
kernel/tktqlock.c new file +30
-0+bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ Quoted 8 lines
kernel/tktqlock.c line 185
Quoted 1 line
kernel/tktqlock.c new file +1
-0+ oldtail =xchg (&tqhp->spin_tail, &tq.next);Boy this is tricky code! I thought I found a race window here, but as I went to write my email saying "Gotcha!" I found that it wasn't a race after all. But as I went though the effort of writing this, I figured I would send this out as documentation for others to see. Hmm, I wonder if we can use this email to add more comments. Anyway, here's what I thought was wrong ;-)
If you didn't know any better, you might even think that I had done something like this before. ;-)
Quoted 25 lines
OK, I originally thought there was a race window here. Let's say that an NMI hit right here, and it happens to be a big one, where lots of things can happen on other CPUs right now.
The scenario is that there's just one item on the queue, which is waiting for the lock to be released, and is spinning below in the:
while (ACCESS_ONCE(tq.cpu) != -1) cpu_relax();And then the lock is released, where in
tkt_q_do_wake()the following is called:ACCESS_ONCE(tqp->cpu) = -1;Now the old queued task is released. But it's
tq->nexthasn't been set yet, and is still NULL. It leaves by doing:ACCESS_ONCE(tqhp->spin) = tq.next; return true;All before this task gets to set *oldtail to &tq. But, I then looked below...
Quoted 14 lines
kernel/tktqlock.c line 197
Quoted 12 lines
kernel/tktqlock.c new file +12
-0+ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) {This checks for that scenario.
Yep!
Quoted 6 lines
As if the old task were to come out
spinning, the problem would only be if it was the last one on the list, and its tq.next was NULL. But if that was the case, then we set spin to NULL and do the next trick, where I thought I gotcha again...Quoted 14 lines
kernel/tktqlock.c line 205
Quoted 8 lines
kernel/tktqlock.c new file +8
-0+ +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next)Here, I was thinking, oh wait, what happens if this is called right before the
xchg()above. Then we would setspin_tailbut not update the old tq.next. But wait! look at what we assignspin_tailto. It's the address of spin, which would be what oldtail would point to above, and then above would set spin to the new tq!Yep again!
Quoted 4 lines
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
Quoted 7 lines
Quoted 1 line
This code is starting to make me look like Gollum :-p
Hmmm... The time and effort to do this might almost have been worthwhile just to accomplish that! ;-)
But yes, this would need better comments, design documentation, or maybe both.
And for whatever it might be worth, here is an attempted upgrade for comments.
First, I upgrade the comment for the xchg() that does the enqueue:
/* * Add our element to the tail of the queue. Note that if the * queue is empty, the ->spin_tail pointer will reference * the queue's head pointer, namely ->spin. */ oldtail = xchg(&tqhp->spin_tail, &tq.next); ACCESS_ONCE(*oldtail) = &tq;
Next, I upgrade the comment for the dequeue operation:
/*
* Remove our element from the queue. If the queue is now empty,
* update carefully so that the next acquisition will enqueue itself
* at the head of the list. Of course, the next enqueue operation
* might be happening concurrently, and this code needs to handle all
* of the possible combinations, keeping in mind that the enqueue
* operation happens in two stages: (1) update the tail pointer and
* (2) update the predecessor's ->next pointer. With this in mind,
* the following code needs to deal with three scenarios:
*
* 1. tq is the last entry. In this case, we use cmpxchg to
* point the list tail back to the list head (->spin). If
* the cmpxchg fails, that indicates that we are instead
* in scenario 2 below. If the cmpxchg succeeds, the next
* enqueue operation's tail-pointer exchange will enqueue
* the next element at the queue head, because the ->spin_tail
* pointer now references the queue head.
*
* 2. tq is the last entry, and the next entry has updated the
* tail pointer but has not yet updated tq.next. In this
* case, tq.next is NULL, the cmpxchg will fail, and the
* code will wait for the enqueue to complete before completing
* removal of tq from the list.
*
* 3. tq is not the last pointer. In this case, tq.next is non-NULL,
* so the following code simply removes tq from the list.
*/
if (tq.next == NULL) {
/* Mark the queue empty. */
tqhp->spin = NULL;
/* Try to point the tail back at the head. */
if (cmpxchg(&tqhp->spin_tail,
&tq.next,
&tqhp->spin) == &tq.next)
return true; /* Succeeded, queue is now empty. */
/* Failed, if needed, wait for the enqueue to complete. */
while (tq.next == NULL)
cpu_relax();
/* The following code will repair the head. */
}
smp_mb(); /* Force ordering between handoff and critical section. */
/*
* Advance list-head pointer. This same task will be the next to
* access this when releasing the lock, so no need for a memory
* barrier after the following assignment.
*/
ACCESS_ONCE(tqhp->spin) = tq.next;
return true;
}Thanx, Paul
Quoted 10 lines · Paul E. McKenney
Quoted 8 lines
Off-topic, although I am in this community for several years, I am not exactly clear with this problem.
1) In general case, which lock is the most competitive in the kernel? what it protects for?
2) In which special case, which lock is the most competitive in the kernel? what it protects for?
3) In general case, which list is the most hot list?
4) In which special case, which list is the most hot list?Others would know better than I, but
mmap_semhas been called out as a
If the contention is with mmap_sem, then I doubt this is going to help much, as that's a sleeping rw semaphore. Now, rw semaphores are implemented with raw spinlocks, but I doubt that would be the main point of contention, compared to the sleeping part.
-- Steve
Quoted 5 lines
prime offender for some workloads. There is of course some debate as to whether the fault lies
mmap_semor with the workloads. There have been some efforts to solve this one on LKML, plus some in academia have worked on this as well:
Quoted 20 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Quoted 4 lines
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney<[email protected]>
This is an interesting patch and I think it is useful for workloads that run on systems with a large number of CPUs.
Quoted 13 lines
arch/x86/include/asm/spinlock_types.h +1
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else
arch/x86/include/asm/spinlock_types.h line 22
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +7
-0+#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2>= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2>= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2>= (unsigned int)((a) - (b)))#endif
It is theoretically possible that a large number of CPUs (says 64 or more with CONFIG_NR_CPUS < 128) can acquire a ticket from the lock before the check for TICKET_T_CMP_GE() in tkt_spin_pass(). So the check will fail even when there is a large number of CPUs contending for the lock. The chance of this happening is, of course, extremely rare. This is not an error as the lock is still working as it should be without your change.
Quoted 9 lines
arch/x86/include/asm/spinlock_types.h +8
-0+/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2)
arch/x86/include/asm/spinlock_types.h
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +10
-0+ +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ];
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined CONFIG_NR_CPUS to be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.
Quoted 7 lines
arch/x86/include/asm/spinlock_types.h +7
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{
arch/x86/include/asm/spinlock_types.h
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +10
-0+int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}
With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 14 lines
arch/x86/include/asm/spinlock_types.h +14
-0+/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { +
arch/x86/include/asm/spinlock_types.h
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +10
-0+/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ;
In case NR_CPUS is 32768 or higher, the ticket will be of type u32 and tqhp->head_tkt is s32. So -1 will be a valid ticket number. You may have to conditionally define head_tkt to be s64 when the ticket is u32.
Do you have any data on how much this patch can actually improve performance on certain workloads? This will help the discussion here.
Regards,
Longman
Quoted 2 lines · Waiman Long
This is an interesting patch and I think it is useful for workloads that run on systems with a large number of CPUs.
I would say it is definitely a fun academic patch, now if it is something for a production environment remains to be seen.
Quoted 25 lines
Quoted 14 lines
arch/x86/include/asm/spinlock_types.h +1
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else arch/x86/include/asm/spinlock_types.h line 22
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +7
-0+#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2>= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2>= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2>= (unsigned int)((a) - (b)))#endif
Quoted 7 lines
It is theoretically possible that a large number of CPUs (says 64 or more with
CONFIG_NR_CPUS< 128) can acquire a ticket from the lock before the check forTICKET_T_CMP_GE()intkt_spin_pass(). So the check will fail even when there is a large number of CPUs contending for the lock. The chance of this happening is, of course, extremely rare. This is not an error as the lock is still working as it should be without your change.
Can you explain this more. How can you acquire the ticket and update at the same time? If a queue has been set, then you can't acquire the ticket as the head has a 1 appended to it.
Quoted 16 lines
Quoted 16 lines
arch/x86/include/asm/spinlock_types.h +15
-0+/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2)
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+ +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ];I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.
Yeah, it can be allocated dynamically at boot.
Quoted 15 lines
Quoted 15 lines
arch/x86/include/asm/spinlock_types.h +14
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp) + return&tkt_q_heads[i];
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
We could add a limit, but in practice I'm not sure that would have any issue. I thought the same thing when I first saw this, but to hit most of the list, would require a large collision in the hash algorithm, would could probably be fixed with a better hash.
Quoted 21 lines
Quoted 21 lines
arch/x86/include/asm/spinlock_types.h +20
-0+/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 4 lines
arch/x86/include/asm/spinlock_types.h +4
-0+cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ;In case
NR_CPUSis 32768 or higher, the ticket will be of type u32 andtqhp->head_tktis s32. So -1 will be a valid ticket number. You may have to conditionally definehead_tktto be s64 when the ticket is u32.
Good point.
Quoted 3 lines
Do you have any data on how much this patch can actually improve performance on certain workloads? This will help the discussion here.
Yeah, that's come up already in the thread. Linus wants to see hard numbers *and* an explanation of why the contended locks can't be fixed, before he even considers merging this type of change.
-- Steve
Quoted 465 lines · Lai Jiangshan
Quoted 450 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +255
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(asp->head_tail); + asnew = asold; + if (asnew.tickets.head & 0x1) { + + /* Someone beat us to it, back out. */ + smp_mb(); + ACCESS_ONCE(tqhp->ref) = NULL; + + /* Spin on the queue element they set up. */ + return tkt_q_do_spin(asp, inc); + } + + /* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |= 0x1;kernel/tktqlock.c line 265
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +}Just small revise.
Quoted 1 line
I just move "
tqhp->head_tkt= asold.tickets.head;" into the loop, so
Excellent point, this allows removing the memory barrier because we can rely on the cmpxchg()'s barriers instead. I have made this change. Now to see if it still works... And it does, at least for light testing.
Quoted 2 lines
we can use "
asp->tickets.head&0x1" to
indicates that queued spinlock is prepared instead of by "tqhp->ref== asp".
Hmmm...
Suppose the following sequence of events happens:
o Several of the CPUs notices that there are too many CPUs spinning,
so they each allocate different queue elements.
o They all set their tqhp->ref to asp, so that there are (say)
three consecutive queue entries that are labeled as belonging to this lock.
o Suppose that the CPU corresponding to the last of these three
entries succeeds in setting the low-order bit in the ticket lock. The other two CPUs will the set their tqhp->ref to NULL, but before that can happen...
o One of the other CPUs that is spinning notices the low-order
bit, and therefore searches for a queue element with the right value of tqhp->ref -- and finds the first one, enqueuing itself.
o The CPU corresponding to this same entry now sets tqhp->ref to
NULL, so that the spinning CPU is now spinning forever on a queue entry that is no longer marked as in use.
How does your change prevent this from happening? It looks to me like it can in fact happen. My patch avoids this by refusing to mark the queue element with asp until after the bit has been set.
Also, when you remove the setting of ->head_tkt to -1, what prevents spurious lock grants after the counters wrap during a long time spent in queued mode?
Your elimination of the tkt_q structure's ->tail field is OK for production, but this field is very helpful for debugging.
Quoted 4 lines
See the append diff.
(And I guess, after it you can force only the CPUs which "inc.tail -tqhp->head_tkt>TKT_Q_SWITCH"
do queued spin to remove the thundering herd)
I could see taking that sort of approach, but why not instead simply choose a relatively small value for TKT_Q_SWITCH? The unfairness is very sharply bounded in that case, so are you sure that removing the thundering herd is really worth it?
Thanx, Paul
Quoted 94 lines
kernel/tktqlock.c +6
-21@@ -27,7 +27,6 @@struct tkt_q {int cpu; -__ticket_t tail;struct tkt_q *next; };@@ -127,9 +126,8 @@ void tkt_q_do_wake(arch_spinlock_t *asp)struct tkt_q_head *tqhp;struct tkt_q *tqp; -/* If the queue is still being set up, wait for it. */ -while ((tqhp =tkt_q_find_head (asp)) ==NULL ) -cpu_relax (); + tqhp =tkt_q_find_head (asp); +BUG_ON (!tqhp);for (;;) {@@ -141,8 +139,6 @@ void tkt_q_do_wake(arch_spinlock_t *asp)return ;/* No element, successfully removed queue. */ cpu_relax (); } -if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) -ACCESS_ONCE (tqhp->head_tkt) =-1 ;smp_mb ();/* Order pointer fetch and assignment against handoff. */ ACCESS_ONCE (tqp->cpu) =-1 ; }@@ -164,20 +160,16 @@ bool tkt_q_do_spin(arch_spinlock_t *asp, struct__raw_tickets inc) */ smp_mb ();/* See above block comment. */ -/* If there no longer is a queue, leave. */ tqhp =tkt_q_find_head (asp); -if (tqhp ==NULL ) -return false; +BUG_ON (!tqhp);/* Initialize our queue element. */ tq.cpu =raw_smp_processor_id (); - tq.tail = inc.tail; tq.next =NULL ;/* Check to see if we already hold the lock. */ if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) {/* The last holder left before queue formed, we hold lock. */ - tqhp->head_tkt =-1 ;return true; }@@ -251,16 +243,14 @@ tkt_q_init_contend(int i, arch_spinlock_t *asp, struct __raw_tickets inc)return tkt_q_do_spin (asp, inc); } +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head;/* The low-order bit in the head counter says "queued". */ asnew.tickets.head |=0x1 ; }while (cmpxchg (&asp->head_tail, asold.head_tail, asnew.head_tail) != asold.head_tail); -/* Point the queue at the lock and go spin on it. */ - tqhp->head_tkt = asold.tickets.head; -smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ -ACCESS_ONCE (tqhp->ref) = asp;return tkt_q_do_spin (asp, inc); }@@ -282,14 +272,9 @@ bool tkt_q_start_contend(arch_spinlock_t *asp,struct __raw_tickets inc) * the lock with the corresponding queue. */ do { - /* - * Use 0x1 to mark the queue in use, but also avoiding - * any spinners trying to use it before we get it all - * initialized. - */ if (cmpxchg(&tkt_q_heads[i].ref, NULL, - (arch_spinlock_t *)0x1) == NULL) { + asp) == NULL) { /* Succeeded, now go initialize it. */return
tkt_q_init_contend(i, asp, inc);
Quoted 53 lines · Waiman Long
Quoted 20 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Quoted 4 lines
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney<[email protected]>This is an interesting patch and I think it is useful for workloads that run on systems with a large number of CPUs.
Quoted 13 lines
arch/x86/include/asm/spinlock_types.h +1
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else arch/x86/include/asm/spinlock_types.h line 22
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +7
-0+#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2>= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2>= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2>= (unsigned int)((a) - (b)))#endif
Quoted 7 lines
It is theoretically possible that a large number of CPUs (says 64 or more with
CONFIG_NR_CPUS< 128) can acquire a ticket from the lock before the check forTICKET_T_CMP_GE()intkt_spin_pass(). So the check will fail even when there is a large number of CPUs contending for the lock. The chance of this happening is, of course, extremely rare. This is not an error as the lock is still working as it should be without your change.
Good point, I need to change the limits from 128 and 32768 to 64 and 16384 in order to guarantee that the comparison will work correctly. Done.
Quoted 16 lines
Quoted 16 lines
arch/x86/include/asm/spinlock_types.h +16
-0+/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) +
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 2 lines
arch/x86/include/asm/spinlock_types.h +2
-0+/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ];I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.
But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using CONFIG_TICKET_LOCK_QUEUED=n, right?
Quoted 14 lines
Quoted 14 lines
arch/x86/include/asm/spinlock_types.h +14
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp) + return&tkt_q_heads[i];
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Quoted 21 lines
Quoted 21 lines
arch/x86/include/asm/spinlock_types.h +21
-0+/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax ();
Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+ } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ;In case
NR_CPUSis 32768 or higher, the ticket will be of type u32 andtqhp->head_tktis s32. So -1 will be a valid ticket number. You may have to conditionally definehead_tktto be s64 when the ticket is u32.
Good catch! For the moment, I just made head_tkt unconditionally s64. I bet that the extra comparison work has no system-visible effect. ;-)
Quoted 3 lines
Do you have any data on how much this patch can actually improve performance on certain workloads? This will help the discussion here.
I could post some microbenchmark numbers if that would help.
Thanx, Paul
Quoted 7 lines · Steven Rostedt
Quoted 2 lines · Waiman Long
This is an interesting patch and I think it is useful for workloads that run on systems with a large number of CPUs.
I would say it is definitely a fun academic patch, now if it is something for a production environment remains to be seen.
At the moment, it should be considered an intellectual exercise. Might be useful at some point, but I would personally rather that the offending lock be broken up to reduce its contention.
Quoted 32 lines
Quoted 24 lines
Quoted 13 lines
arch/x86/include/asm/spinlock_types.h +1
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else arch/x86/include/asm/spinlock_types.h line 22
Quoted 10 lines
arch/x86/include/asm/spinlock_types.h +7
-0+#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2>= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2>= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2>= (unsigned int)((a) - (b)))#endif Quoted 7 lines
It is theoretically possible that a large number of CPUs (says 64 or more with
CONFIG_NR_CPUS< 128) can acquire a ticket from the lock before the check forTICKET_T_CMP_GE()intkt_spin_pass(). So the check will fail even when there is a large number of CPUs contending for the lock. The chance of this happening is, of course, extremely rare. This is not an error as the lock is still working as it should be without your change.
Quoted 3 lines
Can you explain this more. How can you acquire the ticket and update at the same time? If a queue has been set, then you can't acquire the ticket as the head has a 1 appended to it.
Suppose that CONFIG_NR_CPUS=127, and suppose that 65 CPUs atomically increment ->tail before ...
Ah, good point. If TKT_Q_SWITCH is less than 64, then at least one CPU will see the need to switch to queued mode, and will do so regardless of what the other CPUs think. The key point is that each CPU will get its ticket from the xadd(), and these will be issued in order. I therefore backed out my change of the limits.
Quoted 24 lines
Quoted 15 lines
Quoted 15 lines
arch/x86/include/asm/spinlock_types.h +15
-0+/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2)Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+ +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ];I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.
Quoted 1 line
Yeah, it can be allocated dynamically at boot.
But let's first demonstrate the need. Keep in mind that an early-boot deadlock would exercise this code. Yes, it is just a check for NULL, but on the other hand I didn't get the impression that you thought that this code was too simple. ;-)
Quoted 61 lines
Quoted 14 lines
Quoted 14 lines
arch/x86/include/asm/spinlock_types.h +14
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp) + return&tkt_q_heads[i];Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 3 lines
arch/x86/include/asm/spinlock_types.h +3
-0+while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
We could add a limit, but in practice I'm not sure that would have any issue. I thought the same thing when I first saw this, but to hit most of the list, would require a large collision in the hash algorithm, would could probably be fixed with a better hash.
Quoted 21 lines
Quoted 21 lines
arch/x86/include/asm/spinlock_types.h +20
-0+/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 4 lines
arch/x86/include/asm/spinlock_types.h +4
-0+cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ;In case
NR_CPUSis 32768 or higher, the ticket will be of type u32 andtqhp->head_tktis s32. So -1 will be a valid ticket number. You may have to conditionally definehead_tktto be s64 when the ticket is u32.Good point.
Quoted 1 line
Quoted 6 lines
Quoted 2 lines
Do you have any data on how much this patch can actually improve performance on certain workloads? This will help the discussion here.
Yeah, that's come up already in the thread. Linus wants to see hard numbers *and* an explanation of why the contended locks can't be fixed, before he even considers merging this type of change.
A point that I am definitely -not- arguing with. ;-)
Thanx, Paul
Quoted 10 lines · Steven Rostedt
Quoted 9 lines · Paul E. McKenney
Quoted 8 lines
Off-topic, although I am in this community for several years, I am not exactly clear with this problem.
1) In general case, which lock is the most competitive in the kernel? what it protects for?
2) In which special case, which lock is the most competitive in the kernel? what it protects for?
3) In general case, which list is the most hot list?
4) In which special case, which list is the most hot list?
Quoted 6 lines
Quoted 1 line
Others would know better than I, but
mmap_semhas been called out as aIf the contention is with
mmap_sem, then I doubt this is going to help much, as that's a sleeping rw semaphore. Now, rw semaphores are implemented with raw spinlocks, but I doubt that would be the main point of contention, compared to the sleeping part.
If I remember correctly, someone actually hit this earlier this year, which prompted use of a special-purpose queued lock to guard the semaphore data. I don't recall whether it was mmap_sem or not, so cannot say whether it was a straight mutex or an rw semaphore.
Thanx, Paul
Quoted 9 lines
-- Steve
Quoted 5 lines
prime offender for some workloads. There is of course some debate as to whether the fault lies
mmap_semor with the workloads. There have been some efforts to solve this one on LKML, plus some in academia have worked on this as well:
Quoted 466 lines · Lai Jiangshan
Quoted 456 lines · Lai Jiangshan
Quoted 454 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention?
This commit therefore allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
This has been only lightly tested in the kernel, though a userspace implementation has survived substantial testing.
Signed-off-by Paul E. McKenney <[email protected]>arch/x86/include/asm/spinlock.h +41
-0@@ -34,6 +34,8 @@# define UNLOCK_LOCK_PREFIX #endif +#ifndef CONFIG_TICKET_LOCK_QUEUED +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -62,6 +64,25 @@ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)barrier ();/* make sure nothing creeps before the lock is taken */ } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) +{ + registerstruct __raw_tickets inc = { .tail =2 }; + + inc =xadd (&lock->tickets, inc); +for (;;) { +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc)) +break ; + inc.head =ACCESS_ONCE (lock->tickets.head); + } +barrier ();/* smp_mb() on Power or ARM. */ +} + +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ + static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock) {arch_spinlock_t old, new;@@ -70,17 +91,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *asp); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head,2 ); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +18
-0@@ -223,3 +223,21 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +259
-0@@ -0,0 +1,333 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s32 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (16 * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *asp) +{ +return (((unsigned long )asp) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); +do +if (tkt_q_heads[i].ref == asp) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *asp,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*asp); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *asp) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (asp)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (asp, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (asp); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* Add our element to the tail of the queue. */ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will queue itself +* at the head of the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* Advance list-head pointer. */ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *asp,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (asp->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (asp, inc); + } + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&asp->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); +Quoted 8 lines
kernel/tktqlock.c line 265
Quoted 6 lines
kernel/tktqlock.c new file +6
-0+/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head; +smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ +ACCESS_ONCE (tqhp->ref) = asp; +return tkt_q_do_spin (asp, inc); +}Just small revise.
Quoted 2 lines
Sorry it is wrong.
tkt_q_find_head()will returns wrong.
could we use only tkt_q_heads[tkt_q_hash(asp)] instead of find a free one?
Glad you agree. ;-)
I did consider doing that, but was too worried about hash collisions. The hash function could be improved, but that would make it more expensive, which is not a good thing for code on the critical lock-handoff path.
Another approach is to permanently associate queues with each lock, but that increases the size of the lock -- something that has raised concerns in the past. But if adding 32 bytes to each ticketlock was OK, this simplifies things quite a bit.
Thanx, Paul
Quoted 105 lines
Quoted 101 lines
I just move "
tqhp->head_tkt= asold.tickets.head;" into the loop, so we can use "asp->tickets.head&0x1" to
indicates that queued spinlock is prepared instead of by "tqhp->ref== asp".See the append diff.
(And I guess, after it you can force only the CPUs which "inc.tail -tqhp->head_tkt>TKT_Q_SWITCH"
do queued spin to remove the thundering herd)kernel/tktqlock.c +6
-21@@ -27,7 +27,6 @@struct tkt_q {int cpu; -__ticket_t tail;struct tkt_q *next; };@@ -127,9 +126,8 @@ void tkt_q_do_wake(arch_spinlock_t *asp)struct tkt_q_head *tqhp;struct tkt_q *tqp; -/* If the queue is still being set up, wait for it. */ -while ((tqhp =tkt_q_find_head (asp)) ==NULL ) -cpu_relax (); + tqhp =tkt_q_find_head (asp); +BUG_ON (!tqhp);for (;;) {@@ -141,8 +139,6 @@ void tkt_q_do_wake(arch_spinlock_t *asp)return ;/* No element, successfully removed queue. */ cpu_relax (); } -if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) -ACCESS_ONCE (tqhp->head_tkt) =-1 ;smp_mb ();/* Order pointer fetch and assignment against handoff. */ ACCESS_ONCE (tqp->cpu) =-1 ; }@@ -164,20 +160,16 @@ bool tkt_q_do_spin(arch_spinlock_t *asp, struct__raw_tickets inc) */ smp_mb ();/* See above block comment. */ -/* If there no longer is a queue, leave. */ tqhp =tkt_q_find_head (asp); -if (tqhp ==NULL ) -return false; +BUG_ON (!tqhp);/* Initialize our queue element. */ tq.cpu =raw_smp_processor_id (); - tq.tail = inc.tail; tq.next =NULL ;/* Check to see if we already hold the lock. */ if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) {/* The last holder left before queue formed, we hold lock. */ - tqhp->head_tkt =-1 ;return true; }@@ -251,16 +243,14 @@ tkt_q_init_contend(int i, arch_spinlock_t *asp, struct __raw_tickets inc)return tkt_q_do_spin (asp, inc); } +/* Point the queue at the lock and go spin on it. */ + tqhp->head_tkt = asold.tickets.head;/* The low-order bit in the head counter says "queued". */ asnew.tickets.head |=0x1 ; }while (cmpxchg (&asp->head_tail, asold.head_tail, asnew.head_tail) != asold.head_tail); -/* Point the queue at the lock and go spin on it. */ - tqhp->head_tkt = asold.tickets.head; -smp_mb ();/* Ensure head_tkt is set prior to queuers seeing tqhp. */ -ACCESS_ONCE (tqhp->ref) = asp;return tkt_q_do_spin (asp, inc); }@@ -282,14 +272,9 @@ bool tkt_q_start_contend(arch_spinlock_t *asp,struct __raw_tickets inc) * the lock with the corresponding queue. */ do { - /* - * Use 0x1 to mark the queue in use, but also avoiding - * any spinners trying to use it before we get it all - * initialized. - */ if (cmpxchg(&tkt_q_heads[i].ref, NULL, - (arch_spinlock_t *)0x1) == NULL) { + asp) == NULL) { /* Succeeded, now go initialize it. */Quoted 1 line
return tkt_q_init_contend(i, asp, inc);if (tkt_q_heads[i].ref == asp)
Quoted 10 lines · Paul E. McKenney
Quoted 5 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?
If this turns out to work for large machines, that means that distros will enable it, and distros tend to up the NR_CPUS, which is defined at compile time and is set regardless of if you are running with 2 CPUs or a 1000 CPUs.
For now it's fine to use NR_CPUS, but I always try to avoid it. Working in the ARM and POWER environment you are use to lots of kernels compiled specifically for the target. But in the x86 world, it is basically one kernel for all environments, where NR_CPUS does make a big difference.
-- Steve
RFC PATCH 1/1 v2 Auto-queued ticketlock
In reply to Paul E. McKenneyBreaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
arch/x86/include/asm/spinlock.h +38 -5
@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired
@@ -49,17 +64,15 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ; -cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)
@@ -70,17 +83,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);
arch/x86/include/asm/spinlock_types.h +12 -2
@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)
@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
include/linux/kernel.h +1 -0
@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))
kernel/Kconfig.locks +35 -0
@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.
kernel/Makefile +1 -0
@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.o
kernel/tktqlock.c new file +369 -0
369 lines not on this page · raw message
Quoted 9 lines · Paul E. McKenney
Quoted 6 lines
Quoted 4 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.Yeah, it can be allocated dynamically at boot.
But let's first demonstrate the need. Keep in mind that an early-boot deadlock would exercise this code.
I think an early-boot deadlock has more problems than this :-)
Now if we allocate this before other CPUs are enabled, there's no need to worry about accessing it before they are used. They can only be used on contention, and there would be no contention when we are only running on one CPU.
Quoted 3 lines
Yes, it is just a check for NULL,
but on the other hand I didn't get the impression that you thought that this code was too simple. ;-)
I wouldn't change the code that uses it. It should never be hit, and if it is triggered by an early boot deadlock, then I think this would actually be a plus. An early boot deadlock would cause the system to hang with no feedback whats so ever, causing the developer hours of crying for mommy and pulling out their hair because the system just stops doing anything except to show the developer a blinking cursor that blinks "haha, haha, haha".
But if an early boot deadlock were to cause this code to be triggered and do a NULL pointer dereference, then the system crashes. It would most likely produce a backtrace that will give a lot more information to the developer to see what is happening here. Sure, it may confuse them at first, but then they can say: "why is this code triggering before we have other CPUS? Oh, I have a deadlock here" and go fix the code in a matter of minutes instead of hours.
Note, I don't even see this triggering with an early boot deadlock. The only way that can happen is if the task tries to take a spinlock it already owns, or an interrupt goes off and grabs a spinlock that the task currently has but didn't disable interrupts. The ticket counter would be just 2, which is far below the threshold that triggers the queuing.
-- Steve
Quoted 5 lines · Paul E. McKenney
Another approach is to permanently associate queues with each lock, but that increases the size of the lock -- something that has raised concerns in the past. But if adding 32 bytes to each ticketlock was OK, this simplifies things quite a bit.
Yeah, no. The spinlocks need to be small. We have them in size-conscious data structures like "struct dentry" and "struct page", and they really must not be bigger than an "int" in the non-debug case.
In fact, I've occasionally thought about combining a spinlock with a refcounter if that could make things fit in 32 bits on smaller machines, because we also have ops like "atomic_dec_and_lock()" that could possibly be optimized if they fit in one word. That is probably not worth it, but spinlocks do need to remain small.
Linus
Quoted 18 lines · Steven Rostedt
Quoted 10 lines · Paul E. McKenney
Quoted 5 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?If this turns out to work for large machines, that means that distros will enable it, and distros tend to up the
NR_CPUS, which is defined at compile time and is set regardless of if you are running with 2 CPUs or a 1000 CPUs.
Quoted 4 lines
For now it's fine to use
NR_CPUS, but I always try to avoid it. Working in the ARM and POWER environment you are use to lots of kernels compiled specifically for the target. But in the x86 world, it is basically one kernel for all environments, whereNR_CPUSdoes make a big difference.
Fair point. Something to worry about should this ever be in danger of actually going upstream. ;-)
Thanx, Paul
Quoted 13 lines · Linus Torvalds
Quoted 5 lines · Paul E. McKenney
Another approach is to permanently associate queues with each lock, but that increases the size of the lock -- something that has raised concerns in the past. But if adding 32 bytes to each ticketlock was OK, this simplifies things quite a bit.
Yeah, no. The spinlocks need to be small. We have them in size-conscious data structures like "struct dentry" and "struct page", and they really must not be bigger than an "int" in the non-debug case.
Quoted 5 lines
In fact, I've occasionally thought about combining a spinlock with a refcounter if that could make things fit in 32 bits on smaller machines, because we also have ops like "
atomic_dec_and_lock()" that could possibly be optimized if they fit in one word. That is probably not worth it, but spinlocks do need to remain small.
I was afraid of that. On the other hand, I guess that this means that I sent out the correct patch of the two that I prepared. ;-)
Thanx, Paul
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
In reply to Paul E. McKenneyHmm. Something just struck me when reading this patch..
Our memory ordering semantics in our *current* locks are very very subtle. We have just a "barrier()" between the
inc.head = ACCESS_ONCE(lock->tickets.head);
if (inc.head == inc.tail)
break; /* success */and the inside of the locked region.
I think it's safe because of the new memory ordering semantics (loads are in-order, and stores only move *down*), but there's not even a comment about it.
So let's at least comment the current locks before making them even more complex and subtle..
Linus
Quoted 30 lines · Steven Rostedt
Quoted 22 lines
Quoted 22 lines
arch/x86/include/asm/spinlock_types.h +8
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2>= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2>= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2>= (unsigned int)((a) - (b)))Quoted 8 lines
arch/x86/include/asm/spinlock_types.h line 22
Quoted 1 line
arch/x86/include/asm/spinlock_types.h +0
-0#endif It is theoretically possible that a large number of CPUs (says 64 or more with CONFIG_NR_CPUS< 128) can acquire a ticket from the lock before the check for
TICKET_T_CMP_GE()intkt_spin_pass(). So the check will fail even when there is a large number of CPUs contending for the lock. The chance of this happening is, of course, extremely rare. This is not an error as the lock is still working as it should be without your change.
Quoted 3 lines
Can you explain this more. How can you acquire the ticket and update at the same time? If a queue has been set, then you can't acquire the ticket as the head has a 1 appended to it.
I am sorry if I confuse you. What I meant is queuing up at the tail of the ticket lock incrementing the tail number, not actually getting the lock.
Quoted 22 lines
Quoted 14 lines
Quoted 14 lines
arch/x86/include/asm/spinlock_types.h +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
arch/x86/include/asm/spinlock_types.h
Quoted 4 lines
arch/x86/include/asm/spinlock_types.h +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
We could add a limit, but in practice I'm not sure that would have any issue. I thought the same thing when I first saw this, but to hit most of the list, would require a large collision in the hash algorithm, would could probably be fixed with a better hash.
The current code will scan the whole table until either it gets a match or whole table scan is completed. I first thought that hitting a NULL entry can stop the search, but that is not true. It is entirely possible that an entry was used when a queue is created but become empty immediately after that. So we have to scan the whole table to be sure or unless we impose a limit on how many entries we scan.
Regards,
Longman
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
In reply to Paul E. McKenneyQuoted 22 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +15
-0+#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,15 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) {
arch/x86/include/asm/spinlock.h line 59
Quoted 10 lines
arch/x86/include/asm/spinlock.h +2
-4- registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ; -cpu_relax ();
Overheating the CPU are we ;-)
Keeping the cpu_relax() doesn't hurt, even when TICKET_LOCK_QUEUE is enabled. As the only latency to worry about is when tkt_spin_pass() returns true, where it breaks out of the loop anyway.
But if you really don't want the double call to cpu_relax(), we can probably remove the cpu_relax from tkt_spin_pass() and keep this one, or in the above tkt_spin_pass() where TICK_LOCK_QUEUED is not set, we can do:
static inline bool tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc)
{
cpu_relax();
return false;
}Honesty, I would say remove it from tkt_spin_pass() when returning false.
-- Steve
Quoted 6 lines
arch/x86/include/asm/spinlock.h +1
-1inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ }
Quoted 39 lines · Steven Rostedt
Quoted 9 lines · Paul E. McKenney
Quoted 6 lines
Quoted 4 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.Yeah, it can be allocated dynamically at boot.
But let's first demonstrate the need. Keep in mind that an early-boot deadlock would exercise this code.
I think an early-boot deadlock has more problems than this :-)
Now if we allocate this before other CPUs are enabled, there's no need to worry about accessing it before they are used. They can only be used on contention, and there would be no contention when we are only running on one CPU.
Quoted 3 lines
Yes, it is just a check for NULL,
but on the other hand I didn't get the impression that you thought that this code was too simple. ;-)I wouldn't change the code that uses it. It should never be hit, and if it is triggered by an early boot deadlock, then I think this would actually be a plus. An early boot deadlock would cause the system to hang with no feedback whats so ever, causing the developer hours of crying for mommy and pulling out their hair because the system just stops doing anything except to show the developer a blinking cursor that blinks "haha, haha, haha".
But if an early boot deadlock were to cause this code to be triggered and do a NULL pointer dereference, then the system crashes. It would most likely produce a backtrace that will give a lot more information to the developer to see what is happening here. Sure, it may confuse them at first, but then they can say: "why is this code triggering before we have other CPUS? Oh, I have a deadlock here" and go fix the code in a matter of minutes instead of hours.
Quoted 6 lines
Note, I don't even see this triggering with an early boot deadlock. The only way that can happen is if the task tries to take a spinlock it already owns, or an interrupt goes off and grabs a spinlock that the task currently has but didn't disable interrupts. The ticket counter would be just 2, which is far below the threshold that triggers the queuing.
Fair point. I suppose that the hapless kernel hacker could set the threshold really low for that case.
But we are talking about only 8192 bytes of memory here. Is that really enough to worry about on current systems?
Thanx, Paul
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
In reply to Linus TorvaldsQuoted 9 lines · Linus Torvalds
Hmm. Something just struck me when reading this patch..
Our memory ordering semantics in our *current* locks are very very subtle. We have just a "
barrier()" between theinc.head = ACCESS_ONCE(lock->tickets.head); if (inc.head == inc.tail) break; /* success */
Quoted 8 lines
and the inside of the locked region.
I think it's safe because of the new memory ordering semantics (loads are in-order, and stores only move *down*), but there's not even a comment about it.
So let's at least comment the current locks before making them even more complex and subtle..
Would it make sense to have something like an smp_tso() that was a compiler barrier for TSO systems (x86, s390, sparc, etc.) but that emitted the needed memory-barrier instruction for weakly ordered systems?
Seems to me to be easy to do, and helps describe the intent better.
Thanx, Paul
Quoted 10 lines · Linus Torvalds
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
Quoted 6 lines
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
I hate to be the bearer of bad news but I got some pretty bad aim7 performance numbers with this patch on an 8-socket (80 core) 256 Gb memory DL980 box against a vanilla 3.10-rc4 kernel:
* shared workload:
10-100 users is in the noise area.
100-2000 users: -13% throughput.
* high_systime workload:
10-700 users is in the noise area.
700-2000 users: -55% throughput.
* disk:
10-100 users -57% throughput.
100-1000 users: -25% throughput
1000-2000 users: +8% throughput (this patch only benefits when we have a lot of concurrency).
* custom:
10-100 users: -33% throughput.
100-2000 users: -46% throughput.
* alltests:
10-1000 users is in the noise area.
1000-2000 users: -10% throughput.
One notable exception is the short workload where we actually see positive numbers:
10-100 users: +40% throughput.
100-2000 users: +69% throughput.
Thanks,
Davidlohr
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
In reply to Steven RostedtQuoted 48 lines · Steven Rostedt
Quoted 22 lines · Paul E. McKenney
arch/x86/include/asm/spinlock.h +15
-0+#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,15 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) {arch/x86/include/asm/spinlock.h line 59
Quoted 10 lines
arch/x86/include/asm/spinlock.h +2
-4- registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ; -cpu_relax ();Overheating the CPU are we ;-)
Keeping the
cpu_relax()doesn't hurt, even whenTICKET_LOCK_QUEUEis enabled. As the only latency to worry about is whentkt_spin_pass()returns true, where it breaks out of the loop anyway.But if you really don't want the double call to
cpu_relax(), we can probably remove thecpu_relaxfromtkt_spin_pass()and keep this one, or in the abovetkt_spin_pass()whereTICK_LOCK_QUEUEDis not set, we can do:static inline bool
tkt_spin_pass(arch_spinlock_t *ap, struct __raw_tickets inc)
Quoted 7 lines
{ cpu_relax(); return false; }Honesty, I would say remove it from
tkt_spin_pass()when returning false.
Sold! I moved the cpu_relax() from tkt_spin_pass()'s false return to the spin loop in __ticket_spin_lock(). Misguided attempt on my part to minimize __ticket_spin_lock()'s size.
Thanx, Paul
Quoted 11 lines
-- Steve
Quoted 6 lines
arch/x86/include/asm/spinlock.h +1
-1inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ }
Quoted 15 lines · Davidlohr Bueso
Quoted 14 lines · Linus Torvalds
Quoted 3 lines · Steven Rostedt
OK, I haven't found a issue here yet, but youss are beiing trickssy! We don't like trickssy, and we must find precccciouss!!!
.. and I personally have my usual reservations. I absolutely hate papering over scalability issues, and historically whenever people have ever thought that we want complex spinlocks, the problem has always been that the locking sucks.
So reinforced by previous events, I really feel that code that needs this kind of spinlock is broken and needs to be fixed, rather than actually introduce tricky spinlocks.
Quoted 6 lines
Quoted 2 lines
So in order to merge something like this, I want (a) numbers for real loads and (b) explanations for why the spinlock users cannot be fixed.
I hate to be the bearer of bad news but I got some pretty bad aim7 performance numbers with this patch on an 8-socket (80 core) 256 Gb memory DL980 box against a vanilla 3.10-rc4 kernel:
Looks pretty ugly, sorry that it doesn't help in many of your situations.
Any info on what bottlenecks you are encountering?
Thanx, Paul
Quoted 30 lines
* shared workload:
10-100 users is in the noise area.
100-2000 users: -13% throughput.*
high_systimeworkload:
10-700 users is in the noise area.
700-2000 users: -55% throughput.* disk:
10-100 users -57% throughput.
100-1000 users: -25% throughput
1000-2000 users: +8% throughput (this patch only benefits when we have a lot of concurrency).* custom:
10-100 users: -33% throughput.
100-2000 users: -46% throughput.* alltests:
10-1000 users is in the noise area.
1000-2000 users: -10% throughput.One notable exception is the short workload where we actually see positive numbers:
10-100 users: +40% throughput.
100-2000 users: +69% throughput.Thanks,
Davidlohr
Quoted 3 lines · Davidlohr Bueso
I hate to be the bearer of bad news but I got some pretty bad aim7 performance numbers with this patch on an 8-socket (80 core) 256 Gb memory DL980 box against a vanilla 3.10-rc4 kernel:
This doesn't surprise me as the spin lock now contains a function call on any contention. Not to mention the added i$ pressure on the embedded spinlock code having to setup a function call.
Even if the queues are not used, it adds a slight overhead to all spinlocks, due to the code size increase as well as a function call on all contention, which will also have an impact on i$ and branch prediction.
Quoted 5 lines
* shared workload:
10-100 users is in the noise area.
100-2000 users: -13% throughput.
Quoted 8 lines
*
high_systimeworkload:
10-700 users is in the noise area.
700-2000 users: -55% throughput.* disk:
10-100 users -57% throughput.
100-1000 users: -25% throughput
1000-2000 users: +8% throughput (this patch only benefits when we have a
Perhaps this actually started using the queues?
Quoted 6 lines
lot of concurrency).
* custom:
10-100 users: -33% throughput.
100-2000 users: -46% throughput.
Quoted 8 lines
* alltests:
10-1000 users is in the noise area.
1000-2000 users: -10% throughput.One notable exception is the short workload where we actually see positive numbers:
10-100 users: +40% throughput.
100-2000 users: +69% throughput.
Perhaps short work loads have a cold cache, and the impact on cache is not as drastic?
It would be interesting to see what perf reports on these runs.
-- Steve
Quoted 6 lines · Steven Rostedt
Quoted 3 lines · Davidlohr Bueso
I hate to be the bearer of bad news but I got some pretty bad aim7 performance numbers with this patch on an 8-socket (80 core) 256 Gb memory DL980 box against a vanilla 3.10-rc4 kernel:
Quoted 8 lines
This doesn't surprise me as the spin lock now contains a function call on any contention. Not to mention the added i$ pressure on the embedded spinlock code having to setup a function call.
Even if the queues are not used, it adds a slight overhead to all spinlocks, due to the code size increase as well as a function call on all contention, which will also have an impact on i$ and branch prediction.
Agreed.
Quoted 31 lines
Quoted 5 lines
* shared workload:
10-100 users is in the noise area.
100-2000 users: -13% throughput.Quoted 8 lines
*
high_systimeworkload:
10-700 users is in the noise area.
700-2000 users: -55% throughput.* disk:
10-100 users -57% throughput.
100-1000 users: -25% throughput
1000-2000 users: +8% throughput (this patch only benefits when we have aPerhaps this actually started using the queues?
Quoted 6 lines
lot of concurrency).
* custom:
10-100 users: -33% throughput.
100-2000 users: -46% throughput.Quoted 8 lines
* alltests:
10-1000 users is in the noise area.
1000-2000 users: -10% throughput.One notable exception is the short workload where we actually see positive numbers:
10-100 users: +40% throughput.
100-2000 users: +69% throughput.
Quoted 4 lines
Perhaps short work loads have a cold cache, and the impact on cache is not as drastic?
It would be interesting to see what perf reports on these runs.
I didn't actually collect perf traces in this run but I will rerun it with that in mind. I'm also running some OLTP and data mining Oracle based benchmarks where I'm already collecting perf reports.
Will post when I have everything.
Thanks,
Davidlohr
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?
My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 22 lines
Quoted 14 lines
Quoted 14 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
+/*
+ * Start handling a period of high contention by finding a queue to associate
+ * with this lock. Returns true if successful (in which case we hold the
+ * lock) and false otherwise (in which case we do -not- hold the lock).
+ */
+bool tkt_q_start_contend(arch_spinlock_t *asp, struct __raw_tickets inc) +{
+ int i;
+ int start;
+
+ /* Hash the lock address to find a starting point. */
+ start = i = tkt_q_hash(asp);
+
+ /*
+ * Each pass through the following loop attempts to associate
+ * the lock with the corresponding queue.
+ */
+ do {
+ /*
+ * Use 0x1 to mark the queue in use, but also avoiding
+ * any spinners trying to use it before we get it all
+ * initialized.
+ */
+ if (cmpxchg(&tkt_q_heads[i].ref,
+ NULL,
+ (arch_spinlock_t *)0x1) == NULL) {
+
+ /* Succeeded, now go initialize it. */
+ return tkt_q_init_contend(i, asp, inc);
+ }
+
+ /* If someone beat us to it, go spin on their queue. */
+ if (ACCESS_ONCE(asp->tickets.head)& 0x1)
+ return tkt_q_do_spin(asp, inc);
+ } while ((i = tkt_q_next_slot(i)) != start);
+
+ /* All the queues are in use, revert to spinning on the ticket lock. */
+ return false;
+}
+
Unconditional cmpxchg() can be a source of high contention by itself. Considering that 16 threads may be doing cmpxchg() more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.
Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use cmpxchg128() for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Regards,
Longman
Quoted 6 lines · Steven Rostedt
Quoted 3 lines · Davidlohr Bueso
I hate to be the bearer of bad news but I got some pretty bad aim7 performance numbers with this patch on an 8-socket (80 core) 256 Gb memory DL980 box against a vanilla 3.10-rc4 kernel:
Quoted 8 lines
This doesn't surprise me as the spin lock now contains a function call on any contention. Not to mention the added i$ pressure on the embedded spinlock code having to setup a function call.
Even if the queues are not used, it adds a slight overhead to all spinlocks, due to the code size increase as well as a function call on all contention, which will also have an impact on i$ and branch prediction.
Was this system hyperthreaded? If so, it might be suffering from the misplaced cpu_relax(), which would mean that hardware threads spinning on the lock would fail to inform the CPU that it was not doing anything useful.
Thanx, Paul
Quoted 38 lines
Quoted 4 lines
* shared workload:
10-100 users is in the noise area.
100-2000 users: -13% throughput.Quoted 8 lines
*
high_systimeworkload:
10-700 users is in the noise area.
700-2000 users: -55% throughput.* disk:
10-100 users -57% throughput.
100-1000 users: -25% throughput
1000-2000 users: +8% throughput (this patch only benefits when we have aPerhaps this actually started using the queues?
Quoted 6 lines
lot of concurrency).
* custom:
10-100 users: -33% throughput.
100-2000 users: -46% throughput.Quoted 8 lines
* alltests:
10-1000 users is in the noise area.
1000-2000 users: -10% throughput.One notable exception is the short workload where we actually see positive numbers:
10-100 users: +40% throughput.
100-2000 users: +69% throughput.Perhaps short work loads have a cold cache, and the impact on cache is not as drastic?
It would be interesting to see what perf reports on these runs.
-- Steve
Quoted 87 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 22 lines
Quoted 14 lines
Quoted 14 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +
Quoted 5 lines
Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.
Good point, we already noticed good benefits in mutexes and rwsems when using test and cmpxchg techniques.
Thanks,
davidlohr
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
(quoted diff) +13
-0+/* + * Return a pointer to the queue header associated with the specified lock, + * or return NULL if there is no queue for the lock or if the lock's queue + * is in transition. + */ +static struct tkt_q_head *tkt_q_find_head(arch_spinlock_t *asp) +{ + int i; + int start; + + start = i = tkt_q_hash(asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
Review
Quoted 4 lines
(quoted diff) +4
-0+ return&tkt_q_heads[i]; + while ((i = tkt_q_next_slot(i)) != start); + return NULL; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
(quoted diff) +40
-0+/* + * Start handling a period of high contention by finding a queue to associate + * with this lock. Returns true if successful (in which case we hold the + * lock) and false otherwise (in which case we do -not- hold the lock). + */ +bool tkt_q_start_contend(arch_spinlock_t *asp, struct __raw_tickets inc) +{ + int i; + int start; + + /* Hash the lock address to find a starting point. */ + start = i = tkt_q_hash(asp); + + /* + * Each pass through the following loop attempts to associate + * the lock with the corresponding queue. + */ + do { + /* + * Use 0x1 to mark the queue in use, but also avoiding + * any spinners trying to use it before we get it all + * initialized. + */ + if (cmpxchg(&tkt_q_heads[i].ref, + NULL, + (arch_spinlock_t *)0x1) == NULL) { + + /* Succeeded, now go initialize it. */ + return tkt_q_init_contend(i, asp, inc); + } + + /* If someone beat us to it, go spin on their queue. */ + if (ACCESS_ONCE(asp->tickets.head)& 0x1) + return tkt_q_do_spin(asp, inc); + } while ((i = tkt_q_next_slot(i)) != start); + + /* All the queues are in use, revert to spinning on the ticket lock. */ + return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.
Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes cpu_relax().
2. Tests before doing cmpxchg().
3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)
Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
arch/x86/include/asm/spinlock.h +38 -4
@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired
@@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)
@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);
arch/x86/include/asm/spinlock_types.h +12 -2
@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)
@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
include/linux/kernel.h +1 -0
@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))
kernel/Kconfig.locks +35 -0
@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.
kernel/Makefile +1 -0
@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.o
kernel/tktqlock.c new file +369 -0
369 lines not on this page · raw message
kernel/tktqlock.c line 353
Quoted 10 lines · Paul E. McKenney
kernel/tktqlock.c new file +10
-0+bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ +
This looks better, but please add a comment, something to the likes of:
/*
* Only the TKT_Q_SWITCH waiter will set up the queue to prevent
* a thundering herd of setups to occur. It is still possible for
* more than one task to perform a setup if the lock is released
* after this check, a waiter coming in may also match this test. But
* that's covered by the cmpxchg() setup in tkt_q_start_contend. */
kernel/tktqlock.c line 354
Quoted 1 line
kernel/tktqlock.c new file +1
-0+ }else if (inc.tail -TKT_Q_SWITCH == inc.head) {
Also shouldn't this be:
} else if ((__ticket_t)(inc.tail - TKT_Q_SWITCH) == inc.head) {As TKT_Q_SWITCH doesn't have a type, I'm not sure how C will evaluate this. I always screw type conversions up, and just add in the type casts to be safe.
You could also give TKT_Q_SWITCH a type too.
-- Steve
Quoted 15 lines
kernel/tktqlock.c new file +15
-0+ +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass);
Quoted 670 lines · Paul E. McKenney
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes
cpu_relax().2. Tests before doing
cmpxchg().3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ]arch/x86/include/asm/spinlock.h +38
-4@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +319
-0@@ -0,0 +1,369 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (tkt_q_heads[i].ref == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ + do {
kernel/tktqlock.c line 329
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) {
Hi Paul,
Would it be better to do the check like this before the cmpxchg in order to keep the logic the same?
if (!tkt_q_heads[i].ref &&
cmpxchg(&tkt_q_heads[i].ref,
NULL,
(arch_spinlock_t *)0x1) == NULL)Thanks,
Jason
Quoted 23 lines · Steven Rostedt
kernel/tktqlock.c line 353
Quoted 10 lines · Paul E. McKenney
kernel/tktqlock.c new file +10
-0+bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ +This looks better, but please add a comment, something to the likes of:
/*
* Only theTKT_Q_SWITCHwaiter will set up the queue to prevent
* a thundering herd of setups to occur. It is still possible for
* more than one task to perform a setup if the lock is released
* after this check, a waiter coming in may also match this test. But
* that's covered by thecmpxchg()setup intkt_q_start_contend. */
Quoted 5 lines
kernel/tktqlock.c line 354
Quoted 1 line
kernel/tktqlock.c new file +1
-0+ }else if (inc.tail -TKT_Q_SWITCH == inc.head) {Also shouldn't this be:
} else if ((__ticket_t)(inc.tail - TKT_Q_SWITCH) == inc.head) {
Good points on the comment, here is what I currently have:
} else if (inc.tail - TKT_Q_SWITCH == inc.head) {
/*
* This lock has lots of spinners, but no queue. Go create
* a queue to spin on.
*
* In the common case, only the single task that
* sees the head and tail tickets being different by
* exactly TKT_Q_SWITCH will come here set up the queue,
* which prevents a "thundering herd" of queue setups.
* Although it is still possible for an unfortunate series
* of lock handoffs and newly arrived tasks to result
* in more than one task performing a queue setup, this
* is unlikely. Of course, this situation must still be
* handled correctly, which is the job of the cmpxchg()
* in tkt_q_start_contend().
*/
if (tkt_q_start_contend(ap, inc))
return true;
Does that help?
Quoted 5 lines
As
TKT_Q_SWITCHdoesn't have a type, I'm not sure how C will evaluate this. I always screw type conversions up, and just add in the type casts to be safe.You could also give
TKT_Q_SWITCHa type too.
This is an excellent point as well -- things might well get confused. My solution was to take your last suggestion and given TKT_Q_SWITCH the same type as inc.tail and inc.head, and also apply type-safety paranoia to TKT_Q_NQUEUES:
/*
* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a
* given ticket lock to motivate switching to spinning on a queue.
* The reason that it is twice the number is because the bottom bit of
* the ticket is reserved for the bit that indicates that a queue is
* associated with the lock.
*/
#define TKT_Q_SWITCH ((__ticket_t)(CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2))
/*
* TKT_Q_NQUEUES is the number of queues to maintain. Large systems
* might have multiple highly contended locks, so provide more queues for
* systems with larger numbers of CPUs.
*/
#define TKT_Q_NQUEUES (2 * DIV_ROUND_UP(NR_CPUS + ((int)TKT_Q_SWITCH) - 1, \ (int)TKT_Q_SWITCH))
Does that look OK? (The limits on the value of TKT_Q_SWITCH should avoid signed integer overflow.)
Thanx, Paul
Quoted 19 lines
-- Steve
Quoted 15 lines
kernel/tktqlock.c new file +15
-0+ +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass);
Quoted 685 lines · Jason Low
Quoted 670 lines · Paul E. McKenney
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes
cpu_relax().2. Tests before doing
cmpxchg().3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ]arch/x86/include/asm/spinlock.h +38
-4@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +319
-0@@ -0,0 +1,369 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (tkt_q_heads[i].ref == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ + do {kernel/tktqlock.c line 329
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) {Hi Paul,
Quoted 7 lines
Would it be better to do the check like this before the cmpxchg in order to keep the logic the same?
if (!tkt_q_heads[i].ref &&
cmpxchg(&tkt_q_heads[i].ref, NULL, (arch_spinlock_t *)0x1) == NULL)
Good point -- my approach was skipping the check for someone else having set the queue up. Fixed!
Thanx, Paul
Quoted 18 lines · Paul E. McKenney
/*
* This lock has lots of spinners, but no queue. Go create
* a queue to spin on.
*
* In the common case, only the single task that
* sees the head and tail tickets being different by
* exactlyTKT_Q_SWITCHwill come here set up the queue,
* which prevents a "thundering herd" of queue setups.
* Although it is still possible for an unfortunate series
* of lock handoffs and newly arrived tasks to result
* in more than one task performing a queue setup, this
* is unlikely. Of course, this situation must still be
* handled correctly, which is the job of thecmpxchg()
* intkt_q_start_contend().
*/
if (tkt_q_start_contend(ap, inc))
return true;
Quoted 1 line
Does that help?
Yes, very good.
Quoted 26 lines
Quoted 6 lines
As
TKT_Q_SWITCHdoesn't have a type, I'm not sure how C will evaluate this. I always screw type conversions up, and just add in the type casts to be safe.You could also give
TKT_Q_SWITCHa type too.This is an excellent point as well -- things might well get confused. My solution was to take your last suggestion and given
TKT_Q_SWITCHthe same type as inc.tail and inc.head, and also apply type-safety paranoia toTKT_Q_NQUEUES:/*
*TKT_Q_SWITCHis twice the number of CPUs that must be spinning on a
* given ticket lock to motivate switching to spinning on a queue.
* The reason that it is twice the number is because the bottom bit of
* the ticket is reserved for the bit that indicates that a queue is
* associated with the lock.
*/
#defineTKT_Q_SWITCH((__ticket_t)(CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2))/*
*TKT_Q_NQUEUESis the number of queues to maintain. Large systems
* might have multiple highly contended locks, so provide more queues for
* systems with larger numbers of CPUs.
*/
Quoted 6 lines
#define TKT_Q_NQUEUES (2 * DIV_ROUND_UP(NR_CPUS + ((int)TKT_Q_SWITCH) - 1, \ (int)TKT_Q_SWITCH))Does that look OK? (The limits on the value of
TKT_Q_SWITCHshould avoid signed integer overflow.)
Looks fine.
-- Steve
Quoted 22 lines · Paul E. McKenney
kernel/Kconfig.locks +22
-0+config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED
kernel/Kconfig.locks endif · line 258
Quoted 10 lines
kernel/Kconfig.locks +10
-0+ default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place.
Are you sure real-time wants low numbers? I would think that real-time would want this off. This is just a way to help prevent cache ping ponging, but it adds to non-deterministic behavior. As I mentioned before, even though you fixed the thundering herd on setup, once the queue is set, then we will get a thundering herd of tasks trying to queue itself, and the task that was spinning the longest could very well become the one at the end of the FIFO.
-- Steve
Quoted 3 lines
(quoted diff) +2
-0+ + Take the default if you are unsure.kernel/Makefile +0
-0
Quoted 35 lines · Steven Rostedt
Quoted 22 lines · Paul E. McKenney
kernel/Kconfig.locks +22
-0+config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUEDkernel/Kconfig.locks endif · line 258
Quoted 10 lines
kernel/Kconfig.locks +10
-0+ default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place.
Quoted 7 lines
Are you sure real-time wants low numbers? I would think that real-time would want this off. This is just a way to help prevent cache ping ponging, but it adds to non-deterministic behavior. As I mentioned before, even though you fixed the thundering herd on setup, once the queue is set, then we will get a thundering herd of tasks trying to queue itself, and the task that was spinning the longest could very well become the one at the end of the FIFO.
Me? I think that real-time just wants contention to remain low, so that this sort of thing isn't needed in the first place. And now that you mention it, I suppose that is one of the few things that real-time and real-fast workloads have in common.
But if you had some mixed workload on a large system that was mostly real-fast, but had a real-time component, and if the real-fast portion needed TICKET_LOCK_QUEUED=y, then I would guess that the real-time portion would want a relatively low number for TICKET_LOCK_QUEUED_SWITCH.
Thanx, Paul
Quoted 9 lines
-- Steve
Quoted 3 lines
(quoted diff) +2
-0+ + Take the default if you are unsure.kernel/Makefile +0
-0
Quoted 517 lines · Paul E. McKenney
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes
cpu_relax().2. Tests before doing
cmpxchg().3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ]arch/x86/include/asm/spinlock.h +38
-4@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +166
-0@@ -0,0 +1,369 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (tkt_q_heads[i].ref == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */
kernel/tktqlock.c line 176
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;
I guess a mb() is needed here for between read tqhp->ref and read tqhp->head_tkt.
you can move the above mb() to here.
Quoted 199 lines
kernel/tktqlock.c new file +193
-1+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 52 lines · Lai Jiangshan
Quoted 52 lines
kernel/tktqlock.c new file +52
-0+ +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; +
Quoted 8 lines
kernel/tktqlock.c line 176
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;I guess a
mb()is needed here for between readtqhp->refand readtqhp->head_tkt.
you can move the abovemb()to here.
Do we?
The only way to get into here is if you either set up the queue yourself, or you saw the LSB set in head.
If you were the one to set it up yourself, then there's nothing to worry about because you are also the one that set head_tkt.
If you didn't set up the queue, then someone else set the LSB in head, which is done with a cmpxchg() which is also a full mb. This would make head_tkt visible as well because it's set before cmpxchg is called.
Thus, to come into this function you must have seen head & 1 set, and the smp_mb() above will also make head_tkt visible.
The only thing I can see now is that it might not find tqhp because ref may not be set yet. If that's the case, then it will fall out back to the main loop. But if it finds ref, then I don't see how it can't see head_tkt up to date as well.
Maybe I'm missing something.
-- Steve
Quoted 200 lines
Quoted 200 lines
kernel/tktqlock.c new file +193
-1+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 78 lines · Steven Rostedt
Quoted 52 lines · Lai Jiangshan
Quoted 52 lines
kernel/tktqlock.c new file +52
-0+ +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; +Quoted 8 lines
kernel/tktqlock.c line 176
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;I guess a
mb()is needed here for between readtqhp->refand readtqhp->head_tkt.
you can move the abovemb()to here.Do we?
The only way to get into here is if you either set up the queue yourself, or you saw the LSB set in head.
If you were the one to set it up yourself, then there's nothing to worry about because you are also the one that set
head_tkt.If you didn't set up the queue, then someone else set the LSB in head, which is done with a
cmpxchg()which is also a full mb. This would makehead_tktvisible as well because it's set before cmpxchg is called.Thus, to come into this function you must have seen head & 1 set, and the
smp_mb()above will also makehead_tktvisible.
Quoted 6 lines
The only thing I can see now is that it might not find tqhp because ref may not be set yet. If that's the case, then it will fall out back to the main loop. But if it finds ref, then I don't see how it can't see
head_tktup to date as well.Maybe I'm missing something.
Hmmm... I need to look at this more carefully. Lai might well be right because if we are relying on the cmpxchg() for ordering, there needs to be a memory barrier on the read side to pair with the cmpxchg(). You are of course quite correct in the case where the CPU reading the ->head_tkt is the one that set it up.
Something to think about at the gym, I guess. ;-)
Thanx, Paul
Quoted 205 lines
-- Steve
Quoted 200 lines
Quoted 200 lines
kernel/tktqlock.c new file +193
-1+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 78 lines · Steven Rostedt
Quoted 52 lines · Lai Jiangshan
Quoted 52 lines
kernel/tktqlock.c new file +52
-0+ +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; +Quoted 8 lines
kernel/tktqlock.c line 176
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;I guess a
mb()is needed here for between readtqhp->refand readtqhp->head_tkt.
you can move the abovemb()to here.Do we?
The only way to get into here is if you either set up the queue yourself, or you saw the LSB set in head.
If you were the one to set it up yourself, then there's nothing to worry about because you are also the one that set
head_tkt.If you didn't set up the queue, then someone else set the LSB in head, which is done with a
cmpxchg()which is also a full mb. This would makehead_tktvisible as well because it's set before cmpxchg is called.Thus, to come into this function you must have seen head & 1 set, and the
smp_mb()above will also makehead_tktvisible.
Quoted 6 lines
The only thing I can see now is that it might not find tqhp because ref may not be set yet. If that's the case, then it will fall out back to the main loop. But if it finds ref, then I don't see how it can't see
head_tktup to date as well.Maybe I'm missing something.
No, you are right.
When I lay on the bed in the night, I was thinking about the V1, I wrongly considered the V2 has the same problem without deeper thought in this morning.
V2 has not such problem. sorry for the noisy.
Thanks,
Lai
Quoted 206 lines
-- Steve
Quoted 200 lines
Quoted 200 lines
kernel/tktqlock.c new file +193
-1+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Hi, Paul
I have some question about smp_mb().(searching smp_mb() can find all my question)
Thanks,
Lai
Quoted 487 lines · Paul E. McKenney
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes
cpu_relax().2. Tests before doing
cmpxchg().3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ]arch/x86/include/asm/spinlock.h +38
-4@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +136
-0@@ -0,0 +1,369 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (tkt_q_heads[i].ref == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */
kernel/tktqlock.c line 146
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */
I just try to find in which arch&case,
there will be wrong if I use barrier() instead.
Is barrier() enough if I want to reduce the overhead for X86?
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc)
kernel/tktqlock.c line 166
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */
Is barrier() enough if I want to reduce the overhead for X86?
Quoted 65 lines
kernel/tktqlock.c new file +65
-0+ +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ + if (cmpxchg(&tqhp->spin_tail, + &tq.next,
kernel/tktqlock.c line 241
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */
Is barrier() enough if I want to reduce the overhead for X86?
Quoted 27 lines
kernel/tktqlock.c new file +27
-0+ +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ;
kernel/tktqlock.c line 278
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(lock->head_tail); + asnew = asold; + if (asnew.tickets.head & 0x1) { + + /* Someone beat us to it, back out. */ + smp_mb();
Is this intention not to corrupt the next owner of tqhp by the above modification of tqhp?
Is smp_wmb() enough?
Quoted 97 lines
kernel/tktqlock.c new file +91
-1+ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 72 lines · Lai Jiangshan
Quoted 71 lines · Steven Rostedt
Quoted 52 lines · Lai Jiangshan
Quoted 52 lines
kernel/tktqlock.c new file +52
-0+ +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; +Quoted 8 lines
kernel/tktqlock.c line 176
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ;I guess a
mb()is needed here for between readtqhp->refand readtqhp->head_tkt.
you can move the abovemb()to here.Do we?
The only way to get into here is if you either set up the queue yourself, or you saw the LSB set in head.
If you were the one to set it up yourself, then there's nothing to worry about because you are also the one that set
head_tkt.
Quoted 6 lines
Quoted 6 lines
If you didn't set up the queue, then someone else set the LSB in head, which is done with a
cmpxchg()which is also a full mb. This would makehead_tktvisible as well because it's set before cmpxchg is called.Thus, to come into this function you must have seen head & 1 set, and the
smp_mb()above will also makehead_tktvisible.
Agreed, after looking again.
Quoted 7 lines
Quoted 6 lines
The only thing I can see now is that it might not find tqhp because ref may not be set yet. If that's the case, then it will fall out back to the main loop. But if it finds ref, then I don't see how it can't see
head_tktup to date as well.Maybe I'm missing something.
Quoted 7 lines
No, you are right.
When I lay on the bed in the night, I was thinking about the V1, I wrongly considered the V2 has the same problem without deeper thought in this morning.
V2 has not such problem. sorry for the noisy.
Not a problem -- you did cause me to spot a missing ACCESS_ONCE() in tkt_q_find_head(), which I have now added. I also added a comment to tkt_q_do_wake() noting that the caller's xadd() provides the needed memory ordering.
Thank you both for looking this over!
Thanx, Paul
Quoted 210 lines
Thanks,
LaiQuoted 206 lines
-- Steve
Quoted 200 lines
Quoted 200 lines
kernel/tktqlock.c new file +193
-1+ +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ +do { + asold.head_tail =ACCESS_ONCE (lock->head_tail); + asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb (); +ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 511 lines · Lai Jiangshan
Hi, Paul
I have some question about smp_mb().(searching
smp_mb()can find all my question)Thanks,
LaiQuoted 487 lines · Paul E. McKenney
Quoted 103 lines · Waiman Long
Quoted 10 lines · Paul E. McKenney
Quoted 6 lines
I am a bit concern about the size of the head queue table itself. RHEL6, for example, had defined
CONFIG_NR_CPUSto be 4096 which mean a table size of 256. Maybe it is better to dynamically allocate the table at init time depending on the actual number of CPUs in the system.But if your kernel is built for 4096 CPUs, the 32*256=8192 bytes of memory is way down in the noise. Systems that care about that small an amount of memory probably have a small enough number of CPUs that they can just turn off queueing at build time using
CONFIG_TICKET_LOCK_QUEUED=n, right?My concern is more about the latency on the table scan than the actual memory that was used.
Quoted 21 lines
Quoted 13 lines
Quoted 13 lines
kernel/tktqlock.c new file +13
-0+/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *asp) +{ +int i; +int start; + + start = i =tkt_q_hash (asp); + do + if (tkt_q_heads[i].ref == asp)Quoted 8 lines
kernel/tktqlock.c line 88
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +}With a table size of 256 and you have to scan the whole table to find the right head queue. This can be a significant overhead. I will suggest setting a limiting of how many entries it scans before it aborts rather than checking the whole table.
Quoted 4 lines
But it will scan 256 entries only if there are 256 other locks in queued mode, which is -very- unlikely, even given 4096 CPUs. That said, if you show me that this results in a real latency problem on a real system, I would be happy to provide a way to limit the search.
Looking at the code more carefully, the chance of actually scanning 256 entries is very small. However, I now have some concern on the way you set up the initial queue.
kernel/tktqlock.c new file +40
-0+/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *asp,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (asp); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, asp, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (asp->tickets.head)&0x1 ) +return tkt_q_do_spin (asp, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} +Unconditional
cmpxchg()can be a source of high contention by itself. Considering that 16 threads may be doingcmpxchg()more or less simultaneously on the same cache line, it can cause a lot of contention. It will be better if you check to see if tkt_q_heads[i] is NULL first before doing cmpxchg.Another point is that the 16 threads maybe setting up the queues in consecutive slots in the head table. This is both a source of contention and a waste of effort. One possible solution is to add one more field (set to cpuid + 1, for example) to indicate that that setup is being done with asp set to the target lock address immediately. We will need to use
cmpxchg128()for 64-bit machine, though. Another solution is to have only that thread with ticket number that is a fixed distance from head (e.g. 16*2) to do the queue setup while the rest wait until the setup is done before spinning on the queue.Quoted 3 lines
As my colleague Davidlohr had reported there are more regressions than performance improvement in the AIM7 benchmark. I believe that queue setup contention is likely a source of performance regression.
Please see below for a v3 patch that:
1. Fixes
cpu_relax().2. Tests before doing
cmpxchg().3. Reduces the number of CPUs attempting to set up the queue,
in the common case, to a single CPU. (Multiple CPUs can still be trying to set up the queue given unfortunate sequences of concurrent ticket-lock handoffs.)Please let me know how it goes!
Thanx, Paul
------------------------------------------------------------------------
ticketlock: Add queued-ticketlock capability
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ]arch/x86/include/asm/spinlock.h +38
-4@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,37 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; +#ifndef CONFIG_TICKET_LOCK_QUEUED new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + new.head_tail = old.head_tail + (2 <<TICKET_SHIFT ); +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ /* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +136
-0@@ -0,0 +1,369 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH (CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (DIV_ROUND_UP(NR_CPUS + TKT_Q_SWITCH - 1, TKT_Q_SWITCH) * 2) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (tkt_q_heads[i].ref == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* If the queue is still being set up, wait for it. */ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ kernel/tktqlock.c line 146
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ I just try to find in which arch&case,
there will be wrong if I usebarrier()instead.
Quoted 1 line
Is
barrier()enough if I want to reduce the overhead for X86?
It would for x86, but my intent is for this to be common code. Hence my question to Linus about his thoughts on an smp_mb_tso() that provided non-transitive ordering of everything but prior stores against later loads. If people are OK with this, I would implement it and use it here.
Quoted 14 lines
Quoted 14 lines
kernel/tktqlock.c new file +14
-0+ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp;
Quoted 8 lines
kernel/tktqlock.c line 166
Quoted 6 lines
kernel/tktqlock.c new file +6
-0+ +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ Is
barrier()enough if I want to reduce the overhead for X86?
Yep, but same desire for common code and thoughts of smp_mb_tso().
Quoted 69 lines
Quoted 69 lines
kernel/tktqlock.c new file +69
-0+ +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */
Quoted 8 lines
kernel/tktqlock.c line 241
Quoted 6 lines
kernel/tktqlock.c new file +6
-0+while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ Is
barrier()enough if I want to reduce the overhead for X86?
Yep, same desire for common code and smp_mb_tso().
Quoted 32 lines
Quoted 32 lines
kernel/tktqlock.c new file +32
-0+ +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(lock->head_tail);
Quoted 8 lines
kernel/tktqlock.c line 278
Quoted 5 lines
kernel/tktqlock.c new file +5
-0+ asnew = asold; +if (asnew.tickets.head &0x1 ) { + +/* Someone beat us to it, back out. */ +smp_mb ();Is this intention not to corrupt the next owner of tqhp by the above modification of tqhp?
Yep!
Quoted 1 line
Is
smp_wmb()enough?
Indeed it should be sufficient.
Quoted 98 lines
Quoted 97 lines
kernel/tktqlock.c new file +91
-1+ACCESS_ONCE (tqhp->ref) =NULL ; + +/* Spin on the queue element they set up. */ +return tkt_q_do_spin (lock, inc); + } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ; + }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +int i; +int start; + +/* Hash the lock address to find a starting point. */ + start = i =tkt_q_hash (lock); + +/* +* Each pass through the following loop attempts to associate +* the lock with the corresponding queue. +*/ +do { +/* +* Use 0x1 to mark the queue in use, but also avoiding +* any spinners trying to use it before we get it all +* initialized. +*/ +if (tkt_q_heads[i].ref) +continue ; +if (cmpxchg (&tkt_q_heads[i].ref, +NULL , + (arch_spinlock_t *)0x1 ) ==NULL ) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc); + }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. +* Go create a queue to spin on. +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
RFC PATCH 1/1 v3 Auto-queued ticketlock
In reply to Paul E. McKenneyBreaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
arch/x86/include/asm/spinlock.h +35 -5
@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired
@@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)
@@ -70,17 +84,33 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; - new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); + new.head_tail = old.head_tail + (__TKT_SPIN_INC <<TICKET_SHIFT );/* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);
arch/x86/include/asm/spinlock_types.h +12 -2
@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)
@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;
include/linux/kernel.h +1 -0
@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))
kernel/Kconfig.locks +35 -0
@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.
kernel/Makefile +1 -0
@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.o
kernel/tktqlock.c new file +383 -0
383 lines not on this page · raw message
Re: [PATCH RFC ticketlock] v3 Auto-queued ticketlock
Quoted 511 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ][ paulmck: __TKT_SPIN_INC for __ticket_spin_trylock() (Steffen Persvold). ][ paulmck: Type safety fixes (Steven Rostedt). ][ paulmck: Pre-check cmpxchg() value (Waiman Long). ][ paulmck: smp_mb() downgrade to smp_wmb() (Lai Jiangshan). ]arch/x86/include/asm/spinlock.h +35
-5@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,33 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; - new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); + new.head_tail = old.head_tail + (__TKT_SPIN_INC <<TICKET_SHIFT );/* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +286
-0@@ -0,0 +1,383 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH ((__ticket_t)(CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2)) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (2 * DIV_ROUND_UP(NR_CPUS + ((int)TKT_Q_SWITCH) - 1, \ + (int)TKT_Q_SWITCH)) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (ACCESS_ONCE (tkt_q_heads[i].ref) == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* +* If the queue is still being set up, wait for it. Note that +* the caller's xadd() provides the needed memory ordering. +*/ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(lock->head_tail); + asnew = asold; + if (asnew.tickets.head & 0x1) { + + /* Someone beat us to it, back out. */ + smp_wmb(); /* Ensure init before NULLing. */ + ACCESS_ONCE(tqhp->ref) = NULL; + + /* Spin on the queue element they set up. */ + return tkt_q_do_spin(lock, inc);
kernel/tktqlock.c line 296
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ } + +/* +* Record the head counter in case one of the spinning +* CPUs already holds the lock but doesn't realize it yet. +*/ + tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ;
if asold.tickets.head == inc.tail, we will quickly success in the next steps, we don't need to cancel&return directly to avoid unneeded redo from other cpu. but what if in tkt_q_start_contend() ... ?
Quoted 34 lines
kernel/tktqlock.c new file +34
-0+ }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend(arch_spinlock_t *lock, struct __raw_tickets inc) +{ + int i; + int start; + + /* Hash the lock address to find a starting point. */ + start = i = tkt_q_hash(lock); + + /* + * Each pass through the following loop attempts to associate + * the lock with the corresponding queue. + */ + do { + /* + * Use 0x1 to mark the queue in use, but also avoiding + * any spinners trying to use it before we get it all + * initialized. + */ + if (!tkt_q_heads[i].ref && + cmpxchg(&tkt_q_heads[i].ref,
kernel/tktqlock.c line 340
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ NULL, + (arch_spinlock_t *)0x1) == NULL) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc);
if (ACCESS_ONCE(lock->tickets.head) == inc.tail)
return true;
Quoted 49 lines
kernel/tktqlock.c new file +43
-1+ }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. Go create +* a queue to spin on. +* +* In the common case, only the single task that +* sees the head and tail tickets being different by +* exactly TKT_Q_SWITCH will come here set up the queue, +* which prevents a "thundering herd" of queue setups. +* Although it is still possible for an unfortunate series +* of lock handoffs and newly arrived tasks to result +* in more than one task performing a queue setup, this +* is unlikely. Of course, this situation must still be +* handled correctly, which is the job of the cmpxchg() +* in tkt_q_start_contend(). +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Re: [PATCH RFC ticketlock] v3 Auto-queued ticketlock
Quoted 519 lines · Lai Jiangshan
Quoted 517 lines · Paul E. McKenney
Breaking up locks is better than implementing high-contention locks, but if we must have high-contention locks, why not make them automatically switch between light-weight ticket locks at low contention and queued locks at high contention? After all, this would remove the need for the developer to predict which locks will be highly contended.
This commit allows ticket locks to automatically switch between pure ticketlock and queued-lock operation as needed. If too many CPUs are spinning on a given ticket lock, a queue structure will be allocated and the lock will switch to queued-lock operation. When the lock becomes free, it will switch back into ticketlock operation. The low-order bit of the head counter is used to indicate that the lock is in queued mode, which forces an unconditional mismatch between the head and tail counters. This approach means that the common-case code path under conditions of low contention is very nearly that of a plain ticket lock.
A fixed number of queueing structures is statically allocated in an array. The ticket-lock address is used to hash into an initial element, but if that element is already in use, it moves to the next element. If the entire array is already in use, continue to spin in ticket mode.
Signed-off-by Paul E. McKenney <[email protected]>[ paulmck: Eliminate duplicate code and update comments (Steven Rostedt). ][ paulmck: Address Eric Dumazet review feedback. ][ paulmck: Use Lai Jiangshan idea to eliminate smp_mb(). ][ paulmck: Expand ->head_tkt from s32 to s64 (Waiman Long). ][ paulmck: Move cpu_relax() to main spin loop (Steven Rostedt). ][ paulmck: Reduce queue-switch contention (Waiman Long). ][ paulmck: __TKT_SPIN_INC for __ticket_spin_trylock() (Steffen Persvold). ][ paulmck: Type safety fixes (Steven Rostedt). ][ paulmck: Pre-check cmpxchg() value (Waiman Long). ][ paulmck: smp_mb() downgrade to smp_wmb() (Lai Jiangshan). ]arch/x86/include/asm/spinlock.h +35
-5@@ -34,6 +34,21 @@# define UNLOCK_LOCK_PREFIX #endif +#ifdef CONFIG_TICKET_LOCK_QUEUED + +#define __TKT_SPIN_INC 2 +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc); + +#else /* #ifdef CONFIG_TICKET_LOCK_QUEUED */ + +#define __TKT_SPIN_INC 1 +static inline bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +return false; +} + +#endif /* #else #ifdef CONFIG_TICKET_LOCK_QUEUED */ +/* * Ticket locks are conceptually two parts, one indicating the current head of * the queue, and the other indicating the current tail. The lock is acquired @@ -49,17 +64,16 @@*/ static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock) { - registerstruct __raw_tickets inc = { .tail =1 }; + registerstruct __raw_tickets inc = { .tail = __TKT_SPIN_INC }; inc =xadd (&lock->tickets, inc); -for (;;) { -if (inc.head == inc.tail) +if (inc.head == inc.tail ||tkt_spin_pass (lock, inc))break ;cpu_relax (); inc.head =ACCESS_ONCE (lock->tickets.head); } -barrier ();/* make sure nothing creeps before the lock is taken */ +barrier ();/* Make sure nothing creeps in before the lock is taken. */ } static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)@@ -70,17 +84,33 @@ static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock)if (old.tickets.head != old.tickets.tail)return 0 ; - new.head_tail = old.head_tail + (1 <<TICKET_SHIFT ); + new.head_tail = old.head_tail + (__TKT_SPIN_INC <<TICKET_SHIFT );/* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg (&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail; } +#ifndef CONFIG_TICKET_LOCK_QUEUED + static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) {__add (&lock->tickets.head,1 ,UNLOCK_LOCK_PREFIX ); } +#else /* #ifndef CONFIG_TICKET_LOCK_QUEUED */ + +extern void tkt_q_do_wake (arch_spinlock_t *lock); + +static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock) +{ +__ticket_t head =2 ; + + head =xadd (&lock->tickets.head, head); +if (head &0x1 ) +tkt_q_do_wake (lock); +} +#endif /* #else #ifndef CONFIG_TICKET_LOCK_QUEUED */ +static inline int __ticket_spin_is_locked (arch_spinlock_t *lock) {struct __raw_tickets tmp =ACCESS_ONCE (lock->tickets);arch/x86/include/asm/spinlock_types.h +12
-2@@ -7,12 +7,18 @@#include <linux/types.h> -#if (CONFIG_NR_CPUS <256 ) +#if (CONFIG_NR_CPUS <128 )typedef u8 __ticket_t ;typedef u16 __ticketpair_t ; -#else +#define TICKET_T_CMP_GE (a, b) (UCHAR_MAX / 2 >= (unsigned char)((a) - (b))) +#elif (CONFIG_NR_CPUS <32768 )typedef u16 __ticket_t ;typedef u32 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (USHRT_MAX / 2 >= (unsigned short)((a) - (b))) +#else +typedef u32 __ticket_t ; +typedef u64 __ticketpair_t ; +#define TICKET_T_CMP_GE (a, b) (UINT_MAX / 2 >= (unsigned int)((a) - (b)))#endif #define TICKET_SHIFT (sizeof(__ticket_t) * 8)@@ -21,7 +27,11 @@ typedef struct arch_spinlock {union {__ticketpair_t head_tail;struct __raw_tickets { +#ifdef __BIG_ENDIAN__ +__ticket_t tail, head; +#else /* #ifdef __BIG_ENDIAN__ */ __ticket_t head, tail; +#endif /* #else #ifdef __BIG_ENDIAN__ */ } tickets; }; } arch_spinlock_t;include/linux/kernel.h +1
-0@@ -15,6 +15,7 @@#include <asm/byteorder.h> #include <uapi/linux/kernel.h> +#define UCHAR_MAX ((u8)(~0U))#define USHRT_MAX ((u16)(~0U))#define SHRT_MAX ((s16)(USHRT_MAX>>1))#define SHRT_MIN ((s16)(-SHRT_MAX - 1))kernel/Kconfig.locks +35
-0@@ -223,3 +223,38 @@ endif config MUTEX_SPIN_ON_OWNER def_bool y depends on SMP && !DEBUG_MUTEXES + +config TICKET_LOCK_QUEUED + bool "Dynamically switch between ticket and queued locking" + depends on SMP + default n + ---help--- + Enable dynamic switching between ticketlock and queued locking + on a per-lock basis. This option will slow down low-contention + acquisition and release very slightly (additional conditional + in release path), but will provide more efficient operation at + high levels of lock contention. High-contention operation will + not be quite as efficient as would be a pure queued lock, but + this dynamic approach consumes less memory than queud locks + and also runs faster at low levels of contention. + + Say "Y" if you are running on a large system with a workload + that is likely to result in high levels of contention. + + Say "N" if you are unsure. + +config TICKET_LOCK_QUEUED_SWITCH + int "When to switch from ticket to queued locking" + depends on TICKET_LOCK_QUEUED + default 8 + range 3 32 + ---help--- + Specify how many tasks should be spinning on the lock before + switching to queued mode. Systems with low-latency memory/cache + interconnects will prefer larger numbers, while extreme low-latency + and real-time workloads will prefer a smaller number. Of course, + extreme real-time workloads would be even happier if contention + on the locks were reduced to the point that there was never any + need for queued locking in the first place. + + Take the default if you are unsure.kernel/Makefile +1
-0@@ -51,6 +51,7 @@ endif obj-$(CONFIG_SMP) += spinlock.o obj-$(CONFIG_DEBUG_SPINLOCK) += spinlock.o obj-$(CONFIG_PROVE_LOCKING) += spinlock.o +obj-$(CONFIG_TICKET_LOCK_QUEUED) += tktqlock.o obj-$(CONFIG_UID16) += uid16.o obj-$(CONFIG_MODULES) += module.o obj-$(CONFIG_MODULE_SIG) += module_signing.o modsign_pubkey.o modsign_certificate.okernel/tktqlock.c new file +292
-0@@ -0,0 +1,383 @@ +/* +* Queued ticket spinlocks. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +* +* Copyright IBM Corporation, 2013 +* +* Authors: Paul E. McKenney <[email protected]> +*/ +#include <linux/types.h> +#include <linux/kernel.h> +#include <linux/spinlock.h> +#include <linux/smp.h> +#include <linux/percpu.h> + +struct tkt_q { +int cpu; +__ticket_t tail; +struct tkt_q *next; +}; + +struct tkt_q_head { +arch_spinlock_t *ref;/* Pointer to spinlock if in use. */ +s64 head_tkt;/* Head ticket when started queuing. */ +struct tkt_q *spin;/* Head of queue. */ +struct tkt_q **spin_tail;/* Tail of queue. */ +}; + +/* +* TKT_Q_SWITCH is twice the number of CPUs that must be spinning on a +* given ticket lock to motivate switching to spinning on a queue. +* The reason that it is twice the number is because the bottom bit of +* the ticket is reserved for the bit that indicates that a queue is +* associated with the lock. +*/ +#define TKT_Q_SWITCH ((__ticket_t)(CONFIG_TICKET_LOCK_QUEUED_SWITCH * 2)) + +/* +* TKT_Q_NQUEUES is the number of queues to maintain. Large systems +* might have multiple highly contended locks, so provide more queues for +* systems with larger numbers of CPUs. +*/ +#define TKT_Q_NQUEUES (2 * DIV_ROUND_UP(NR_CPUS + ((int)TKT_Q_SWITCH) - 1, \ + (int)TKT_Q_SWITCH)) + +/* The queues themselves. */ +struct tkt_q_head tkt_q_heads[TKT_Q_NQUEUES ]; + +/* Advance to the next queue slot, wrapping around to the beginning. */ +static int tkt_q_next_slot (int i) +{ +return (++i <TKT_Q_NQUEUES ) ? i :0 ; +} + +/* Very crude hash from lock address to queue slot number. */ +static unsigned long tkt_q_hash (arch_spinlock_t *lock) +{ +return (((unsigned long )lock) >>8 ) %TKT_Q_NQUEUES ; +} + +/* +* Return a pointer to the queue header associated with the specified lock, +* or return NULL if there is no queue for the lock or if the lock's queue +* is in transition. +*/ +static struct tkt_q_head *tkt_q_find_head (arch_spinlock_t *lock) +{ +int i; +int start; + + start = i =tkt_q_hash (lock); +do +if (ACCESS_ONCE (tkt_q_heads[i].ref) == lock) +return &tkt_q_heads[i]; +while ((i =tkt_q_next_slot (i)) != start); +return NULL ; +} + +/* +* Try to stop queuing, reverting back to normal ticket-lock operation. +* We can only stop queuing when the queue is empty, which means that +* we need to correctly handle races where someone shows up in the queue +* just as we are trying to dispense with the queue. They win, we lose. +*/ +static bool tkt_q_try_unqueue (arch_spinlock_t *lock,struct tkt_q_head *tqhp) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; + +/* Pick up the ticket values. */ + asold =ACCESS_ONCE (*lock); +if ((asold.tickets.head & ~0x1 ) == asold.tickets.tail) { + +/* Attempt to mark the lock as not having a queue. */ + asnew = asold; + asnew.tickets.head &= ~0x1 ; +if (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) == asold.head_tail) { + +/* Succeeded, mark the queue as unused. */ +ACCESS_ONCE (tqhp->ref) =NULL ; +return true; + } + } + +/* Failed, tell the caller there is still a queue to pass off to. */ +return false; +} + +/* +* Hand the lock off to the first CPU on the queue. +*/ +void tkt_q_do_wake (arch_spinlock_t *lock) +{ +struct tkt_q_head *tqhp; +struct tkt_q *tqp; + +/* +* If the queue is still being set up, wait for it. Note that +* the caller's xadd() provides the needed memory ordering. +*/ +while ((tqhp =tkt_q_find_head (lock)) ==NULL ) +cpu_relax (); + +for (;;) { + +/* Find the first queue element. */ + tqp =ACCESS_ONCE (tqhp->spin); +if (tqp !=NULL ) +break ;/* Element exists, hand off lock. */ +if (tkt_q_try_unqueue (lock, tqhp)) +return ;/* No element, successfully removed queue. */ +cpu_relax (); + } +if (ACCESS_ONCE (tqhp->head_tkt) !=-1 ) +ACCESS_ONCE (tqhp->head_tkt) =-1 ; +smp_mb ();/* Order pointer fetch and assignment against handoff. */ +ACCESS_ONCE (tqp->cpu) =-1 ; +} +EXPORT_SYMBOL (tkt_q_do_wake); + +/* +* Given a lock that already has a queue associated with it, spin on +* that queue. Return false if there was no queue (which means we do not +* hold the lock) and true otherwise (meaning we -do- hold the lock). +*/ +bool tkt_q_do_spin (arch_spinlock_t *lock,struct __raw_tickets inc) +{ +struct tkt_q **oldtail; +struct tkt_q tq; +struct tkt_q_head *tqhp; + +/* +* Ensure that accesses to queue header happen after sensing +* the lock's have-queue bit. +*/ +smp_mb ();/* See above block comment. */ + +/* If there no longer is a queue, leave. */ + tqhp =tkt_q_find_head (lock); +if (tqhp ==NULL ) +return false; + +/* Initialize our queue element. */ + tq.cpu =raw_smp_processor_id (); + tq.tail = inc.tail; + tq.next =NULL ; + +/* Check to see if we already hold the lock. */ +if (ACCESS_ONCE (tqhp->head_tkt) == inc.tail) { +/* The last holder left before queue formed, we hold lock. */ + tqhp->head_tkt =-1 ; +return true; + } + +/* +* Add our element to the tail of the queue. Note that if the +* queue is empty, the ->spin_tail pointer will reference +* the queue's head pointer, namely ->spin. +*/ + oldtail =xchg (&tqhp->spin_tail, &tq.next); +ACCESS_ONCE (*oldtail) = &tq; + +/* Spin until handoff. */ +while (ACCESS_ONCE (tq.cpu) !=-1 ) +cpu_relax (); + +/* +* Remove our element from the queue. If the queue is now empty, +* update carefully so that the next acquisition will enqueue itself +* at the head of the list. Of course, the next enqueue operation +* might be happening concurrently, and this code needs to handle all +* of the possible combinations, keeping in mind that the enqueue +* operation happens in two stages: (1) update the tail pointer and +* (2) update the predecessor's ->next pointer. With this in mind, +* the following code needs to deal with three scenarios: +* +* 1. tq is the last entry. In this case, we use cmpxchg to +* point the list tail back to the list head (->spin). If +* the cmpxchg fails, that indicates that we are instead +* in scenario 2 below. If the cmpxchg succeeds, the next +* enqueue operation's tail-pointer exchange will enqueue +* the next element at the queue head, because the ->spin_tail +* pointer now references the queue head. +* +* 2. tq is the last entry, and the next entry has updated the +* tail pointer but has not yet updated tq.next. In this +* case, tq.next is NULL, the cmpxchg will fail, and the +* code will wait for the enqueue to complete before completing +* removal of tq from the list. +* +* 3. tq is not the last pointer. In this case, tq.next is non-NULL, +* so the following code simply removes tq from the list. +*/ +if (tq.next ==NULL ) { + +/* Mark the queue empty. */ + tqhp->spin =NULL ; + +/* Try to point the tail back at the head. */ +if (cmpxchg (&tqhp->spin_tail, + &tq.next, + &tqhp->spin) == &tq.next) +return true;/* Succeeded, queue is now empty. */ + +/* Failed, if needed, wait for the enqueue to complete. */ +while (tq.next ==NULL ) +cpu_relax (); + +/* The following code will repair the head. */ + } +smp_mb ();/* Force ordering between handoff and critical section. */ + +/* +* Advance list-head pointer. This same task will be the next to +* access this when releasing the lock, so no need for a memory +* barrier after the following assignment. +*/ +ACCESS_ONCE (tqhp->spin) = tq.next; +return true; +} + +/* +* Given a lock that does not have a queue, attempt to associate the +* i-th queue with it, returning true if successful (meaning we hold +* the lock) or false otherwise (meaning we do -not- hold the lock). +* Note that the caller has already filled in ->ref with 0x1, so we +* own the queue. +*/ +static bool +tkt_q_init_contend (int i,arch_spinlock_t *lock,struct __raw_tickets inc) +{ +arch_spinlock_t asold; +arch_spinlock_t asnew; +struct tkt_q_head *tqhp; + +/* Initialize the i-th queue header. */ + tqhp = &tkt_q_heads[i]; + tqhp->spin =NULL ; + tqhp->spin_tail = &tqhp->spin; + +/* Each pass through this loop attempts to mark the lock as queued. */ + do { + asold.head_tail = ACCESS_ONCE(lock->head_tail); + asnew = asold; + if (asnew.tickets.head & 0x1) { + + /* Someone beat us to it, back out. */ + smp_wmb(); /* Ensure init before NULLing. */ + ACCESS_ONCE(tqhp->ref) = NULL; + + /* Spin on the queue element they set up. */ + return tkt_q_do_spin(lock, inc); + } + + /* + * Record the head counter in case one of the spinning + * CPUs already holds the lock but doesn't realize it yet. + */
Quoted 8 lines
kernel/tktqlock.c line 296
Quoted 4 lines
kernel/tktqlock.c new file +4
-0+ tqhp->head_tkt = asold.tickets.head; + +/* The low-order bit in the head counter says "queued". */ + asnew.tickets.head |=0x1 ;if asold.tickets.head == inc.tail, we will quickly success in the next steps, we don't need to cancel&return directly to avoid unneeded redo from other cpu. but what if in
tkt_q_start_contend()... ?
I am not completely sure that I understand what you are asking.
The straightforward answer is that ->head_tkt handles this case, and it is checked before we start spinning on our queue element.
Another possible answer is that the current version tends to avoid having multiple CPUs attempting to concurrently switch a given lock to queued mode.
So what was the real question? ;-)
Quoted 46 lines
Quoted 34 lines
kernel/tktqlock.c new file +34
-0+ }while (cmpxchg (&lock->head_tail, + asold.head_tail, + asnew.head_tail) != asold.head_tail); + +/* Point the queue at the lock and go spin on it. */ +ACCESS_ONCE (tqhp->ref) = lock; +return tkt_q_do_spin (lock, inc); +} + +/* +* Start handling a period of high contention by finding a queue to associate +* with this lock. Returns true if successful (in which case we hold the +* lock) and false otherwise (in which case we do -not- hold the lock). +*/ +bool tkt_q_start_contend(arch_spinlock_t *lock, struct __raw_tickets inc) +{ + int i; + int start; + + /* Hash the lock address to find a starting point. */ + start = i = tkt_q_hash(lock); + + /* + * Each pass through the following loop attempts to associate + * the lock with the corresponding queue. + */ + do { + /* + * Use 0x1 to mark the queue in use, but also avoiding + * any spinners trying to use it before we get it all + * initialized. + */ + if (!tkt_q_heads[i].ref && + cmpxchg(&tkt_q_heads[i].ref,kernel/tktqlock.c line 340
Quoted 10 lines
kernel/tktqlock.c new file +10
-0+ NULL, + (arch_spinlock_t *)0x1) == NULL) { + +/* Succeeded, now go initialize it. */ +return tkt_q_init_contend (i, lock, inc); + } + +/* If someone beat us to it, go spin on their queue. */ +if (ACCESS_ONCE (lock->tickets.head) &0x1 ) +return tkt_q_do_spin (lock, inc);if (
ACCESS_ONCE(lock->tickets.head)== inc.tail)
Quoted 1 line
return true;
Is this really a good change? The downside is that we fail to put the lock into queued mode when it needs it. Sooner or later, someone else should do so, but we will be suffering from memory contention in the meantime. It is not clear to me that this is a win.
Quoted 50 lines
Quoted 49 lines
kernel/tktqlock.c new file +43
-1+ }while ((i =tkt_q_next_slot (i)) != start); + +/* All the queues are in use, revert to spinning on the ticket lock. */ +return false; +} + +bool tkt_spin_pass (arch_spinlock_t *ap,struct __raw_tickets inc) +{ +if (unlikely (inc.head &0x1 )) { + +/* This lock has a queue, so go spin on the queue. */ +if (tkt_q_do_spin (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + + }else if (inc.tail -TKT_Q_SWITCH == inc.head) { + +/* +* This lock has lots of spinners, but no queue. Go create +* a queue to spin on. +* +* In the common case, only the single task that +* sees the head and tail tickets being different by +* exactly TKT_Q_SWITCH will come here set up the queue, +* which prevents a "thundering herd" of queue setups. +* Although it is still possible for an unfortunate series +* of lock handoffs and newly arrived tasks to result +* in more than one task performing a queue setup, this +* is unlikely. Of course, this situation must still be +* handled correctly, which is the job of the cmpxchg() +* in tkt_q_start_contend(). +*/ +if (tkt_q_start_contend (ap, inc)) +return true; + +/* Get here if the queue is in transition: Retry next time. */ + } + +/* Either no need for a queue or the queue is in transition. Spin. */ +return false; +} +EXPORT_SYMBOL (tkt_spin_pass); --To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
Quoted 4 lines · Steven Rostedt
Perhaps short work loads have a cold cache, and the impact on cache is not as drastic?
It would be interesting to see what perf reports on these runs.
After running the aim7 workloads on Paul's v3 patch (same 80 core, 8 socket box - HT off) the results are quite similar to the v1. One difference is that the five_sec workload benefited with +15% throughput after 500 users.
Taking a further look at each workload:
* five_sec: spends a large amount of time in the newish mcs style lock at the spin on owner for the inode->i_mutex:
24.13% 315655 reaim [kernel.kallsyms] [k] mspin_lock
|
--- mspin_lock
|
|--99.76%-- __mutex_lock_killable_slowpath
| mutex_lock_killable
| vfs_readdir
| SyS_getdents
| system_call_fastpath
| __getdents64With this patch:
23.56% 310531 reaim [kernel.kallsyms] [k] mspin_lock
|
--- mspin_lock
|
|--99.78%-- __mutex_lock_killable_slowpath
| mutex_lock_killable
| vfs_readdir
| SyS_getdents
| system_call
| __getdents64* custom: Got a -33% throughput regression with this patch with 10-100 users and -46% with 100 users and up. It spends most kernel space time dealing trying to take the inode->i_mutex and the ext4 ->s_orphan_lock (note that all runs are performed on ramdisks with ext4):
3.12% 137131 reaim [kernel.kallsyms] [k] mspin_lock
|
--- mspin_lock
|
|--82.98%-- __mutex_lock_killable_slowpath
| mutex_lock_killable
| vfs_readdir
| SyS_getdents
| system_call_fastpath
| __getdents64
|
|--16.97%-- __mutex_lock_slowpath
| mutex_lock
| |
| |--47.65%-- ext4_orphan_del
| |--45.01%-- ext4_orphan_addWith this patch:
2.14% 109982 reaim [kernel.kallsyms] [k] mspin_lock
|
--- mspin_lock
|
|--68.67%-- __mutex_lock_killable_slowpath
| mutex_lock_killable
| vfs_readdir
| SyS_getdents
| system_call
| __getdents64
|
|--31.24%-- __mutex_lock_slowpath
| mutex_lock
| |
| |--40.36%-- ext4_orphan_del* short: is the big winner for this patch, +69% throughput improvement with 100-2000 users. This makes a lot of sense since the workload spends a ridiculous amount of time trying to acquire the d_lock:
84.86% 1569902 reaim [kernel.kallsyms] [k] _raw_spin_lock
|
--- _raw_spin_lock
|
|--49.96%-- dget_parent
| __fsnotify_parent
|--49.71%-- dput
| |
| |--99.98%-- __fsnotify_parentWith this patch:
70.65% 467422 reaim [kernel.kallsyms] [k] tkt_q_do_spin
|
--- tkt_q_do_spin
|
|--100.00%-- tkt_spin_pass
| |
| |--100.00%-- _raw_spin_lock
| | |
| | |--50.07%-- dget_parent
| | | __fsnotify_parent
| | |--49.93%-- dput
| | | __fsnotify_parent* disk: This patch benefits when adding more concurrency. Got -57% with 10-100 users, -25% with 100-1000 users and +8% with over 1000 users. Spends a good amount of time dealing with the wait_queue lock. The perf traces are with 80 users, where we see the worst numbers:
22.34% 20400 reaim [kernel.kallsyms] [k] _raw_spin_lock_irqsave
|
--- _raw_spin_lock_irqsave
|
|--50.28%-- __wake_up
| |
| |--99.10%-- __wake_up_bit
| | wake_up_bit
| | unlock_buffer
|
|--33.73%-- prepare_to_wait_exclusive
| __wait_on_bit_lock
| out_of_line_wait_on_bit_lock
| __lock_buffer
| do_get_write_access
| jbd2_journal_get_write_access
| __ext4_journal_get_write_access
|--14.76%-- finish_wait
| |
| |--98.93%-- __wait_on_bit_lock
| | out_of_line_wait_on_bit_lock
| | __lock_buffer
| | do_get_write_access
| | jbd2_journal_get_write_access
| | __ext4_journal_get_write_accessWith this patch the the time spent in the mentioned spinlocks considerably reduced:
8.09% 6237 reaim [kernel.kallsyms] [k] __read_lock_failed
|
--- __read_lock_failed
_raw_read_lock
|
|--99.08%-- start_this_handle
| jbd2__journal_start
| __ext4_journal_start_sb
1.48% 1032 reaim [kernel.kallsyms] [k] _raw_spin_lock_irqsave
|
--- _raw_spin_lock_irqsave
|
|--50.77%-- prepare_to_wait
| |
| |--72.61%-- jbd2_log_wait_commit
| | jbd2_complete_transaction
|--21.76%-- prepare_to_wait_exclusive
| __wait_on_bit_lock
| out_of_line_wait_on_bit_lock
| __lock_buffer
| do_get_write_access
| jbd2_journal_get_write_access
|--11.46%-- __wake_up
| |
| |--44.21%-- ftrace_define_fields_jbd2_run_stats
| | __ext4_journal_stop
|--10.39%-- finish_wait
| |
| |--53.18%-- __wait_on_bit_lock
| | out_of_line_wait_on_bit_lock
| | __lock_buffer
| | do_get_write_access
| | jbd2_journal_get_write_access
| | __ext4_journal_get_write_accessThanks,
Davidlohr
Quoted 5 lines · Davidlohr Bueso
* short: is the big winner for this patch, +69% throughput improvement with 100-2000 users. This makes a lot of sense since the workload spends a ridiculous amount of time trying to acquire the
d_lock:
Quoted 7 lines
84.86% 1569902 reaim [kernel.kallsyms] [k]
_raw_spin_lock| --- _raw_spin_lock | |--49.96%-- dget_parent | __fsnotify_parent |--49.71%-- dput
Ugh. Do you have any idea what the heck that thing actually does?
Normally, we shouldn't see lots of dget contention, since the dcache these days does everything but the last path component locklessly.
But there's a few exceptions, like symlinks (act as "last component" in the middle). And obviously, if some crazy threaded program opens the *same* file concurrently over and over again, then that "last component" will hammer on the dentry lock of that particular path. But that "open the same file concurrently" seems totally unrealistic - although maybe that's what AIM does..
Anybody know the AIM subtests?
Also, we *may* actually be able to optimize this by making dentry->d_count atomic, which will allow us to often do dget_parent and put() without taking the dcache lock at all. That's what it used to be, but the RCU patches actually made it be protected by the d_lock. It made sense at the time, as a step in the sequence, and many of the dentry d_count accesses are under the lock, but now that the remaining hot-paths are dget_parent and dput and many of the dentry d_count increments are gone from the hot-paths, we might want to re-visit that decision. It could go either way.
Al, comments?
Linus