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 · 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 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 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 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 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 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 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 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 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
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 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 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
Quoted 15 lines · Linus Torvalds
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
Quoted 1 line
Ugh. Do you have any idea what the heck that thing actually does?
Waiman's dcache patchet were actually an attempt to address these exact
issues: http://lkml.org/lkml/2013/5/22/716
According to him:
"the short workload calls security functions like getpwnam(), getpwuid(), getgrgid() a couple of times. These functions open the /etc/passwd or /etc/group files, read their content and close the files.
It is the intensive open/read/close sequence from multiple threads that is causing 80%+ contention in the d_lock on a system with large number of cores. The MIT's MOSBench paper also outlined dentry reference counting as a scalability roadblock for its Apache and Exim tests."
Quoted 13 lines
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?
Quoted 9 lines
Also, we *may* actually be able to optimize this by making
dentry->d_countatomic, which will allow us to often dodget_parentandput()without taking the dcache lock at all. That's what it used to be, but the RCU patches actually made it be protected by thed_lock. It made sense at the time, as a step in the sequence, and many of the dentryd_countaccesses are under the lock, but now that the remaining hot-paths aredget_parentand dput and many of the dentryd_countincrements are gone from the hot-paths, we might want to re-visit that decision. It could go either way.
I did a quick attempt at this (patch attached). For the short workload, we now have:
76.90% 928688 reaim [kernel.kallsyms] [k] _raw_spin_lock
|
--- _raw_spin_lock
|
|--99.69%-- dget_parent
| __fsnotify_parent
| |
| |--20.23%-- fsnotify_access
| | vfs_read
| |--20.13%-- __fput
| | ____fput
| | task_work_run
| |--20.07%-- security_file_permission
| | rw_verify_area
| | vfs_read
| |--19.97%-- do_sys_open
| | SyS_open
| --19.60%-- security_file_open
| do_dentry_openStill 76%!!! Throughput wise we do have a very nice boost when compared to the vanilla kernel:
10-100 users: +47%
100-1000 users: +76%
1000-2000 users: +76%
Thanks,
Davidlohr
fs/autofs4/expire.c +4 -4
@@ -109,7 +109,7 @@ cont:spin_lock_nested (&q->d_lock,DENTRY_D_LOCK_NESTED );/* Already gone or negative dentry (under construction) - try next */ -if (q->d_count ==0 || !simple_positive (q)) { +if (atomic_read (&q->d_count) ==0 || !simple_positive (q)) {spin_unlock (&q->d_lock); next = q->d_u.d_child.next; goto cont;
@@ -267,7 +267,7 @@ static int autofs4_tree_busy(struct vfsmount *mnt,else ino_count++; -if (p->d_count > ino_count) { +if (atomic_read (&p->d_count) > ino_count) { top_ino->last_used = jiffies;dput (p);return 1 ;
@@ -409,7 +409,7 @@ struct dentry *autofs4_expire_indirect(struct super_block *sb,if (!exp_leaves) {/* Path walk currently on this dentry? */ ino_count =atomic_read (&ino->count) +1 ; -if (dentry->d_count > ino_count) +if (atomic_read (&dentry->d_count) > ino_count) goto next;if (!autofs4_tree_busy (mnt, dentry, timeout, do_now)) {
@@ -423,7 +423,7 @@ struct dentry *autofs4_expire_indirect(struct super_block *sb, }else {/* Path walk currently on this dentry? */ ino_count =atomic_read (&ino->count) +1 ; -if (dentry->d_count > ino_count) +if (atomic_read (&dentry->d_count) > ino_count) goto next; expired =autofs4_check_leaves (mnt, dentry, timeout, do_now);
fs/autofs4/root.c +3 -4
@@ -176,12 +176,11 @@ static struct dentry *autofs4_lookup_active(struct dentry *dentry) ino = list_entry(p, struct autofs_info, active); active = ino->dentry; -spin_lock (&active->d_lock); -/* Already gone? */ -if (active->d_count ==0 ) - goto next; +if (atomic_read (&active->d_count) ==0 ) +continue ; +spin_lock (&active->d_lock); qstr = &active->d_name;if (active->d_name.hash != hash)
fs/ceph/inode.c +2 -2
@@ -903,8 +903,8 @@ static struct dentry *splice_dentry(struct dentry *dn, struct inode *in, }else if (realdn) {dout ("dn %p (%d) spliced with %p (%d) " "inode %p ino %llx.%llx\n" , - dn, dn->d_count, - realdn, realdn->d_count, + dn,atomic_read (&dn->d_count), + realdn,atomic_read (&realdn->d_count), realdn->d_inode,ceph_vinop (realdn->d_inode));dput (dn); dn = realdn;
fs/coda/dir.c +1 -1
@@ -560,7 +560,7 @@ static int coda_dentry_revalidate(struct dentry *de, unsigned int flags)if (cii->c_flags &C_FLUSH )coda_flag_inode_children (inode,C_FLUSH ); -if (de->d_count >1 ) +if (atomic_read (&de->d_count) >1 )/* pretend it's valid, but don't change the flags */ goto out;
fs/configfs/dir.c +1 -1
@@ -387,7 +387,7 @@ static void remove_dir(struct dentry * d)if (d->d_inode)simple_rmdir (parent->d_inode,d); -pr_debug (" o %s removing done (%d)\n" ,d->d_name.name, d->d_count); +pr_debug (" o %s removing done (%d)\n" ,d->d_name.name,atomic_read (&d->d_count));dput (parent); }
fs/dcache.c +31 -38
221 lines not on this page · raw message
fs/ecryptfs/inode.c +1 -1
@@ -358,7 +358,7 @@ static int ecryptfs_lookup_interpose(struct dentry *dentry, lower_mnt =mntget (ecryptfs_dentry_to_lower_mnt (dentry->d_parent));fsstack_copy_attr_atime (dir_inode, lower_dentry->d_parent->d_inode); -BUG_ON (!lower_dentry->d_count); +BUG_ON (!atomic_read (&lower_dentry->d_count));ecryptfs_set_dentry_private (dentry, dentry_info);ecryptfs_set_dentry_lower (dentry, lower_dentry);
fs/locks.c +1 -1
@@ -1351,7 +1351,7 @@ int generic_add_lease(struct file *filp, long arg, struct file_lock **flp)if ((arg ==F_RDLCK ) && (atomic_read (&inode->i_writecount) >0 )) goto out;if ((arg ==F_WRLCK ) - && ((dentry->d_count >1 ) + && ((atomic_read (&dentry->d_count) >1 ) || (atomic_read (&inode->i_count) >1 ))) goto out;
fs/namei.c +6 -5
@@ -536,8 +536,8 @@ static int unlazy_walk(struct nameidata *nd, struct dentry *dentry)* a reference at this point. */ BUG_ON (!IS_ROOT (dentry) && dentry->d_parent != parent); -BUG_ON (!parent->d_count); - parent->d_count++; +BUG_ON (!atomic_read (&parent->d_count)); +atomic_inc (&parent->d_count);spin_unlock (&dentry->d_lock); }spin_unlock (&parent->d_lock);
@@ -3279,10 +3279,11 @@ SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)void dentry_unhash (struct dentry *dentry) {shrink_dcache_parent (dentry); -spin_lock (&dentry->d_lock); -if (dentry->d_count ==1 ) +if (atomic_read (&dentry->d_count) ==1 ) { +spin_lock (&dentry->d_lock);__d_drop (dentry); -spin_unlock (&dentry->d_lock); +spin_unlock (&dentry->d_lock); + } }int vfs_rmdir (struct inode *dir,struct dentry *dentry)
fs/nfs/dir.c +3 -3
@@ -1721,7 +1721,7 @@ int nfs_unlink(struct inode *dir, struct dentry *dentry) dir->i_ino, dentry->d_name.name);spin_lock (&dentry->d_lock); -if (dentry->d_count >1 ) { +if (atomic_read (&dentry->d_count) >1 ) {spin_unlock (&dentry->d_lock);/* Start asynchronous writeout of the inode */ write_inode_now (dentry->d_inode,0 );
@@ -1870,7 +1870,7 @@ int nfs_rename(struct inode *old_dir, struct dentry *old_dentry,dfprintk (VFS ,"NFS: rename(%s/%s -> %s/%s, ct=%d)\n" , old_dentry->d_parent->d_name.name, old_dentry->d_name.name, new_dentry->d_parent->d_name.name, new_dentry->d_name.name, - new_dentry->d_count); +atomic_read (&new_dentry->d_count));/* * For non-directories, check whether the target is busy and if so,
@@ -1888,7 +1888,7 @@ int nfs_rename(struct inode *old_dir, struct dentry *old_dentry, rehash = new_dentry; } -if (new_dentry->d_count >2 ) { +if (atomic_read (&new_dentry->d_count) >2 ) {int err;/* copy the target dentry's name */
fs/nfs/unlink.c +1 -1
@@ -479,7 +479,7 @@ nfs_sillyrename(struct inode *dir, struct dentry *dentry)dfprintk (VFS ,"NFS: silly-rename(%s/%s, ct=%d)\n" , dentry->d_parent->d_name.name, dentry->d_name.name, - dentry->d_count); +atomic_read (&dentry->d_count));nfs_inc_stats (dir,NFSIOS_SILLYRENAME );/*
fs/nilfs2/super.c +1 -1
@@ -973,7 +973,7 @@ static int nilfs_attach_snapshot(struct super_block *s, __u64 cno,static int nilfs_tree_was_touched (struct dentry *root_dentry) { -return root_dentry->d_count >1 ; +return atomic_read (&root_dentry->d_count) >1 ; }/**
include/linux/dcache.h +3 -3
@@ -112,7 +112,7 @@ struct dentry {unsigned char d_iname[DNAME_INLINE_LEN ];/* small names */ /* Ref lookup also touches following */ -unsigned int d_count;/* protected by d_lock */ +atomic_t d_count;spinlock_t d_lock;/* per dentry lock */ const struct dentry_operations *d_op;struct super_block *d_sb;/* The root of the dentry tree */
@@ -319,7 +319,7 @@ static inline int __d_rcu_to_refcount(struct dentry *dentry, unsigned seq)assert_spin_locked (&dentry->d_lock);if (!read_seqcount_retry (&dentry->d_seq, seq)) { ret =1 ; - dentry->d_count++; +atomic_inc (&dentry->d_count); }return ret;
@@ -352,7 +352,7 @@ extern char *dentry_path(struct dentry *, char *, int);static inline struct dentry *dget_dlock (struct dentry *dentry) {if (dentry) - dentry->d_count++; +atomic_inc (&dentry->d_count);return dentry; }
Quoted 7 lines · Davidlohr Bueso
According to him:
"the short workload calls security functions like
getpwnam(),getpwuid(),getgrgid()a couple of times. These functions open the/etc/passwdor/etc/groupfiles, read their content and close the files.
Ahh, ok. So yeah, it's multiple threads all hitting the same file.
I guess that /etc/passwd case is historically interesting, but I'm not sure we really want to care too deeply..
Quoted 1 line
I did a quick attempt at this (patch attached).
Yeah, that's wrong, although it probably approximates the dget() case (but incorrectly).
One of the points behind using an atomic d_count is that then dput() should do
if (!atomic_dec_and_lock(&dentry->d_count, &dentry->d_count))
return;
at the very top of the function. It can avoid taking the lock entirely if the count doesn't go down to zero, which would be a common case if you have lots of users opening the same file. While still protecting d_count from ever going to zero while the lock is held.
Your
+ if (atomic_read(&dentry->d_count) > 1) {
+ atomic_dec(&dentry->d_count);
+ return;
+ }
+ spin_lock(&dentry->d_lock);
pattern is fundamentally racy, but it's what "atomic_dec_and_lock()" should do race-free.
For similar reasons, I think you need to still maintain the d_lock in d_prune_aliases etc. That's a slow-path, so the fact that we add an atomic sequence there doesn't much matter.
However, one optimization missing from your patch is obvious in the profile. "dget_parent()" also needs to be optimized - you still have that as 99% of the spin-lock case. I think we could do something like
rcu_read_lock();
parent = ACCESS_ONCE(dentry->d_parent);
if (atomic_inc_nonzero(&parent->d_count))
return parent;
.. get d_lock and do it the slow way ...
rcu_read_unlock();to locklessly get the parent pointer. We know "parent" isn't going away (dentries are rcu-free'd and we hold the rcu read lock), and I think that we can optimistically take *any* parent dentry that happened to be valid at one point. As long as the refcount didn't go down to zero. Al?
With dput and dget_parent() both being lockless for the common case, you might get rid of the d_lock contention entirely for that load. I dunno. And I should really think more about that dget_parent() thing a bit more, but I cannot imagine how it could not be right (because even with the current d_lock model, the lock is gotten *within* dget_parent(), so the caller can never know if it gets a new or an old parent, so there is no higher-level serialization going on - and we might as well return *either* the new or the old as such).
I really want Al to double-check me if we decide to try going down this hole. But the above two fixes to your patch should at least approximate the d_lock changes, even if I'd have to look more closely at the other details of your patch..
Linus
Quoted 11 lines · Linus Torvalds
Quoted 7 lines · Davidlohr Bueso
According to him:
"the short workload calls security functions like
getpwnam(),getpwuid(),getgrgid()a couple of times. These functions open the/etc/passwdor/etc/groupfiles, read their content and close the files.Ahh, ok. So yeah, it's multiple threads all hitting the same file.
Quoted 7 lines
I guess that
/etc/passwdcase is historically interesting, but I'm not sure we really want to care too deeply..Quoted 1 line
I did a quick attempt at this (patch attached).
Yeah, that's wrong, although it probably approximates the
dget()case (but incorrectly).
Indeed, it was only a proof of concept.
Quoted 4 lines
One of the points behind using an atomic
d_countis that thendput()should doif (!atomic_dec_and_lock(&dentry->d_count, &dentry->d_count)) return;
noted.
Quoted 45 lines
at the very top of the function. It can avoid taking the lock entirely if the count doesn't go down to zero, which would be a common case if you have lots of users opening the same file. While still protecting
d_countfrom ever going to zero while the lock is held.Your
fs/dcache.c +5
-0+if (atomic_read (&dentry->d_count) >1 ) { +atomic_dec (&dentry->d_count); +return ; + } +spin_lock (&dentry->d_lock);pattern is fundamentally racy, but it's what "
atomic_dec_and_lock()" should do race-free.
For similar reasons, I think you need to still maintain thed_lockind_prune_aliasesetc. That's a slow-path, so the fact that we add an atomic sequence there doesn't much matter.However, one optimization missing from your patch is obvious in the profile. "
dget_parent()" also needs to be optimized - you still have that as 99% of the spin-lock case. I think we could do something likercu_read_lock(); parent = ACCESS_ONCE(dentry->d_parent); if (atomic_inc_nonzero(&parent->d_count)) return parent; .. get d_lock and do it the slow way ... rcu_read_unlock();to locklessly get the parent pointer. We know "parent" isn't going away (dentries are rcu-free'd and we hold the rcu read lock), and I think that we can optimistically take *any* parent dentry that happened to be valid at one point. As long as the refcount didn't go down to zero. Al?
With dput and
dget_parent()both being lockless for the common case, you might get rid of thed_lockcontention entirely for that load. I dunno. And I should really think more about thatdget_parent()thing a bit more, but I cannot imagine how it could not be right (because even with the currentd_lockmodel, the lock is gotten *within*dget_parent(), so the caller can never know if it gets a new or an old parent, so there is no higher-level serialization going on - and we might as well return *either* the new or the old as such).
Quoted 4 lines
I really want Al to double-check me if we decide to try going down this hole. But the above two fixes to your patch should at least approximate the
d_lockchanges, even if I'd have to look more closely at the other details of your patch..
Ok, I'll try to rerun and send a more conscious patch. Thanks for the tips.
Davidlohr
Quoted 10 lines · Linus Torvalds
Quoted 7 lines · Davidlohr Bueso
According to him:
"the short workload calls security functions like
getpwnam(),getpwuid(),getgrgid()a couple of times. These functions open the/etc/passwdor/etc/groupfiles, read their content and close the files.Ahh, ok. So yeah, it's multiple threads all hitting the same file
If that's the case and it's a bunch of reads, shouldn't they act concurrently anyway?
I mean it's not like dentries are being changed or added or removed in this case.
Quoted 70 lines
I guess that
/etc/passwdcase is historically interesting, but I'm not sure we really want to care too deeply..Quoted 1 line
I did a quick attempt at this (patch attached).
Yeah, that's wrong, although it probably approximates the
dget()case (but incorrectly).One of the points behind using an atomic
d_countis that thendput()should doif (!atomic_dec_and_lock(&dentry->d_count, &dentry->d_count)) return;at the very top of the function. It can avoid taking the lock entirely if the count doesn't go down to zero, which would be a common case if you have lots of users opening the same file. While still protecting
d_countfrom ever going to zero while the lock is held.Your
fs/dcache.c +5
-0+if (atomic_read (&dentry->d_count) >1 ) { +atomic_dec (&dentry->d_count); +return ; + } +spin_lock (&dentry->d_lock);pattern is fundamentally racy, but it's what "
atomic_dec_and_lock()" should do race-free.For similar reasons, I think you need to still maintain the
d_lockind_prune_aliasesetc. That's a slow-path, so the fact that we add an atomic sequence there doesn't much matter.However, one optimization missing from your patch is obvious in the profile. "
dget_parent()" also needs to be optimized - you still have that as 99% of the spin-lock case. I think we could do something likercu_read_lock(); parent = ACCESS_ONCE(dentry->d_parent); if (atomic_inc_nonzero(&parent->d_count)) return parent; .. get d_lock and do it the slow way ... rcu_read_unlock();to locklessly get the parent pointer. We know "parent" isn't going away (dentries are rcu-free'd and we hold the rcu read lock), and I think that we can optimistically take *any* parent dentry that happened to be valid at one point. As long as the refcount didn't go down to zero. Al?
With dput and
dget_parent()both being lockless for the common case, you might get rid of thed_lockcontention entirely for that load. I dunno. And I should really think more about thatdget_parent()thing a bit more, but I cannot imagine how it could not be right (because even with the currentd_lockmodel, the lock is gotten *within*dget_parent(), so the caller can never know if it gets a new or an old parent, so there is no higher-level serialization going on - and we might as well return *either* the new or the old as such).I really want Al to double-check me if we decide to try going down this hole. But the above two fixes to your patch should at least approximate the
d_lockchanges, even if I'd have to look more closely at the other details of your patch..Linus
--
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/
Al Viro
maintainerQuoted 15 lines · Linus Torvalds
For similar reasons, I think you need to still maintain the
d_lockind_prune_aliasesetc. That's a slow-path, so the fact that we add an atomic sequence there doesn't much matter.However, one optimization missing from your patch is obvious in the profile. "
dget_parent()" also needs to be optimized - you still have that as 99% of the spin-lock case. I think we could do something likercu_read_lock(); parent = ACCESS_ONCE(dentry->d_parent); if (atomic_inc_nonzero(&parent->d_count)) return parent; .. get d_lock and do it the slow way ... rcu_read_unlock();
Quoted 5 lines
to locklessly get the parent pointer. We know "parent" isn't going away (dentries are rcu-free'd and we hold the rcu read lock), and I think that we can optimistically take *any* parent dentry that happened to be valid at one point. As long as the refcount didn't go down to zero. Al?
What will you do with __d_rcu_to_refcount()? Any such scheme has to hold d_lock from zero->non-zero d_count changes, or atomic_dec_and_lock in dput() won't help at all. As it is, both comlete_walk() and unlazy_walk() are grabbing ->d_lock on the dentry we'd reached, so they can call that sucker. And that'll give you ->d_lock contention when a bunch of threads are hitting the same file; I don't see how atomics would avoid that one...
Quoted 19 lines · Al Viro
Quoted 12 lines · Linus Torvalds
However, one optimization missing from your patch is obvious in the profile. "
dget_parent()" also needs to be optimized - you still have that as 99% of the spin-lock case. I think we could do something likercu_read_lock(); parent = ACCESS_ONCE(dentry->d_parent); if (atomic_inc_nonzero(&parent->d_count)) return parent; .. get d_lock and do it the slow way ... rcu_read_unlock();Quoted 5 lines
to locklessly get the parent pointer. We know "parent" isn't going away (dentries are rcu-free'd and we hold the rcu read lock), and I think that we can optimistically take *any* parent dentry that happened to be valid at one point. As long as the refcount didn't go down to zero. Al?
Quoted 3 lines
What will you do with
__d_rcu_to_refcount()? Any such scheme has to holdd_lockfrom zero->non-zerod_countchanges, oratomic_dec_and_lockindput()won't help at all.
I'd actually suggest we do *not* remove any existing d_lock usage outside of the particular special cases we want to optimize, which at least from Davidlohr's profile is just dput() (which has shown up a lot before) and dget_parent() (which I'm not sure why it happens so much on his load, but it really seems trivially safe to optimistically do under just the RCU lock).
Quoted 5 lines
As it is, both
complete_walk()andunlazy_walk()
are grabbing ->d_lock on the dentry we'd reached, so they can call that sucker. And that'll give you ->d_lock contention when a bunch of threads are hitting the same file; I don't see how atomics would avoid that one...
I'd love to get rid of complete_walk() using the dcache lock too, but if we really can't get rid of it, I won't cry.
That said, I do wonder if we could do something like "atomic_inc_not_zero()" on the d_count, and only if it is zero (which won't be horribly unusual, since for leaf dentries that nobody else is using) we'd do the whole locking sequence.
But my first reaction is to not even bother until it shows up on some profile. Of course, maybe it immediately does.
There's a real downside to making d_count an "atomic_t", and there might be loads where it actually bites us. But even in the absense of contention, atomics tend to be sufficiently faster than spinlocks that even if we end up adding two or even three atomics for each d_lock lock we get rid of, we should be ok even for single-thread. On the contention case, we'll obviously win almost regardless of how many atomics we add.
Of course, that assumes we get rid of any locking at all for the normal case. with dput() having to take the lock when the refcount goes to zero, and most single-threaded file opens using the final path component with a dentry that isn't otherwise used, doing an atomic d_count might hurt the single-thread case without getting any spinlock advantage at all. dget_parent() would be the only case that helps even there, and that should normally only happen for "..", I think.
So who knows.. Are we willing to take a hit on the single-thread case (*if* that is the case) if it helps scalability a lot? If it was a common scalability issue, sure. But just for AIM7 looking up /etc/passwd? Maybe that's not a good idea.
Of course, for the mostly non-contended case, it's quite possible that the extra atomic isn't even noticeable. As long as the dentry is dirty in the cache (which it would be, normally), the atomic cost is just in the 10-20 cycles.
End result: I think it would be interesting to try this all out, and it could be a noticeable win under some cases, but it *definitely* needs a lot of timing and testing to see which ways it goes..
Linus
Al Viro
maintainerQuoted 6 lines · Linus Torvalds
I'd actually suggest we do *not* remove any existing
d_lockusage outside of the particular special cases we want to optimize, which at least from Davidlohr's profile is justdput()(which has shown up a lot before) anddget_parent()(which I'm not sure why it happens so much on his load, but it really seems trivially safe to optimistically do under just the RCU lock).
Actually, dget_parent() change might be broken; the thing is, the assumptions are more subtle than "zero -> non-zero only happens under ->d_lock". It's actually "new references are grabbed by somebody who's either already holding one on the same dentry _or_ holding ->d_lock". That's what d_invalidate() check for ->d_count needs for correctness - caller holds one reference, so comparing ->d_count with 2 under ->d_lock means checking that there's no other holders _and_ there won't be any new ones appearing.
Consider the following situation:
X is dentry of a/b
Y is dentry of a/b/c
Z is dentry of d/e
A holds a reference to Y and enters dget_parent(Y)
B holds a reference to X and enters d_invalidate(X)
A picks the value of Y->d_parent (== X)
C moves Y to Z
B grabs ->d_lock on X
B checks X->d_count; it's 1, we deduce that no other references exist or are going to appear
A does atomic_inc_not_zero(&X->d_count). And since it's not zero (it's 1, actually), we've just grabbed an extra reference on X that was not going to appear according to B...
Quoted 4 lines
That said, I do wonder if we could do something like "
atomic_inc_not_zero()" on thed_count, and only if it is zero (which won't be horribly unusual, since for leaf dentries that nobody else is using) we'd do the whole locking sequence.
Same correctness issue as above, I'm afraid...
Quoted 3 lines
End result: I think it would be interesting to try this all out, and it could be a noticeable win under some cases, but it *definitely* needs a lot of timing and testing to see which ways it goes..
*nod*
What's more, we need the underlying assumptions documented very clearly for any such change; it's _not_ as simple as "protect transitions from zero to non-zero and we are done" ;-/
Quoted 8 lines · Al Viro
Actually,
dget_parent()change might be broken; the thing is, the assumptions are more subtle than "zero -> non-zero only happens under ->d_lock". It's actually "new references are grabbed by somebody who's either already holding one on the same dentry _or_ holding ->d_lock". That's whatd_invalidate()check for ->d_count needs for correctness - caller holds one reference, so comparing ->d_count with 2 under ->d_lock means checking that there's no other holders _and_ there won't be any new ones appearing.
For the particular case of dget_parent() maybe dget_parent() should just double-check the original dentry->d_parent pointer after getting the refcount on it (and if the parent has changed, drop the refcount again and go to the locked version). That might be a good idea anyway, and should fix the possible race (which would be with another cpu having to first rename the child to some other parent, and the d_invalidate() the original parent)
That said, the case we'd really want to fix isn't dget_parent(), but just the normal RCU lookup finishing touches (the__d_rcu_to_refcount() case you already mentioned) . *If* we could do that without ever taking the d_lock on the target, that would be lovely. But it would seem to have the exact same issue. Although maybe the dentry_rcuwalk_barrier() thing ends up solving it (ie if we had a lookup at a bad time, we know it will fail the sequence count test, so we're ok).
Subtle, subtle.
Linus
Al Viro
maintainerQuoted 10 lines · Linus Torvalds
Quoted 8 lines · Al Viro
Actually,
dget_parent()change might be broken; the thing is, the assumptions are more subtle than "zero -> non-zero only happens under ->d_lock". It's actually "new references are grabbed by somebody who's either already holding one on the same dentry _or_ holding ->d_lock". That's whatd_invalidate()check for ->d_count needs for correctness - caller holds one reference, so comparing ->d_count with 2 under ->d_lock means checking that there's no other holders _and_ there won't be any new ones appearing.
Quoted 7 lines
For the particular case of
dget_parent()maybedget_parent()should just double-check the originaldentry->d_parentpointer after getting the refcount on it (and if the parent has changed, drop the refcount again and go to the locked version). That might be a good idea anyway, and should fix the possible race (which would be with another cpu having to first rename the child to some other parent, and thed_invalidate()the original parent)
Yes, but... Then we'd need to dput() that sucker if we decide we shouldn't have grabbed that reference, after all, which would make dget_parent() potentially blocking.
Quoted 8 lines
That said, the case we'd really want to fix isn't
dget_parent(), but just the normal RCU lookup finishing touches (the__d_rcu_to_refcount()case you already mentioned) . *If* we could do that without ever taking thed_lockon the target, that would be lovely. But it would seem to have the exact same issue. Although maybe thedentry_rcuwalk_barrier()thing ends up solving it (ie if we had a lookup at a bad time, we know it will fail the sequence count test, so we're ok).
Maybe, but that would require dentry_rcuwalk_barrier() between any such check and corresponding grabbing of ->d_lock done for it, so it's not just d_invalidate().
Quoted 1 line
Subtle, subtle.
Yes ;-/ The current variant is using ->d_lock as a brute-force mechanism for avoiding all that fun, and I'm not sure that getting rid of it would buy us enough to make it worth the trouble. I'm absolutely sure that if we go for that, we _MUST_ document the entire scheme as explicitly as possible, or we'll end up with the shitload of recurring bugs in that area. Preferably with the formal proof of correctness spelled out somewhere...
Quoted 10 lines · Al Viro
Quoted 8 lines · Linus Torvalds
For the particular case of
dget_parent()maybedget_parent()should just double-check the originaldentry->d_parentpointer after getting the refcount on it (and if the parent has changed, drop the refcount again and go to the locked version). That might be a good idea anyway, and should fix the possible race (which would be with another cpu having to first rename the child to some other parent, and thed_invalidate()the original parent)
Quoted 3 lines
Yes, but... Then we'd need to
dput()that sucker if we decide we shouldn't have grabbed that reference, after all, which would makedget_parent()potentially blocking.
Ho humm.. interesting. I was talking about wanting to mix atomics and spinlocks earlier in this thread due to space constraints, and it strikes me that that would actually help this case a lot. Having the dentry count mix d_lock and the count in one word would allow for atomic ops like "increment if not locked", and we'd avoid this whole race entirely..
Something like "low bit of count is the lock bit" would end up being lovely for this case. Of course, that's not how our spinlocks work ..
Linus
Quoted 19 lines · Linus Torvalds
Quoted 11 lines · Al Viro
Quoted 7 lines · Linus Torvalds
For the particular case of
dget_parent()maybedget_parent()should just double-check the originaldentry->d_parentpointer after getting the refcount on it (and if the parent has changed, drop the refcount again and go to the locked version). That might be a good idea anyway, and should fix the possible race (which would be with another cpu having to first rename the child to some other parent, and thed_invalidate()the original parent)Yes, but... Then we'd need to
dput()that sucker if we decide we shouldn't have grabbed that reference, after all, which would makedget_parent()potentially blocking.Ho humm.. interesting. I was talking about wanting to mix atomics and spinlocks earlier in this thread due to space constraints, and it strikes me that that would actually help this case a lot. Having the dentry count mix
d_lockand the count in one word would allow for atomic ops like "increment if not locked", and we'd avoid this whole race entirely..
Quoted 4 lines
Something like "low bit of count is the lock bit" would end up being lovely for this case. Of course, that's not how our spinlocks work ..
Linus
I have created another patch to do exactly the "increment if not locked" operation as suggested. It did help a lot. See the patch below for more information. Any additional comment will be appreciated.
Regards,
Longman
-------------------------------------------------------------------------------------------------------------------
The current code takes the dentry's d_lock lock whenever the d_count reference count is being updated. In reality, nothing big really happens until d_count goes to 0 in dput(). So it is not necessary to take the lock if the reference count won't go to 0. On the other hand, there are cases where d_count should not be updated or was not expected to be updated while d_lock was taken by other functions.
To try to locklessly update the d_count while d_lock is not taken by others, the 32-bit d_count and 32-bit d_lock (when no debugging code is turned on) can be combined into a single 64-bit word to be updated atomically whenever the following conditions happen:
1. The lock is not taken, i.e. spin_can_lock() returns true.
2. For increment, the original d_count must be > 0, or
3. for decrement, the original d_count must be > 1.
To maximize the chance of doing lockless update, the new code calls spin_unlock_wait() before trying to do the update.
The new code also attempts to do lockless atomic update twice before falling back to the old code path of taking a lock before doing the update. It is because there will still be some fair amount of contention with only one attempt.
The offsets of the d_count/d_lock duple are at byte 72 and 88 for 32-bit and 64-bit systems respectively. In both cases, they are 8-byte aligned and their combination into a single 8-byte word will not introduce a hole that increase the size of the dentry structure.
This patch has a particular big impact on the short workload of the AIM7 benchmark with ramdisk filesystem. The table below show the performance improvement to the JPM (jobs per minutes) throughput due to this patch on an 8-socket 80-core x86-64 system with a 3.10-rc4 kernel in a 1/2/4/8 node configuration by using numactl to restrict the execution of the workload on certain nodes.
+-----------------+----------------+-----------------+----------+ | Configuration | Mean JPM | Mean JPM | % Change | | | Rate w/o patch | Rate with patch | | +-----------------+---------------------------------------------+ | | User Range 10 - 100 | +-----------------+---------------------------------------------+ | 8 nodes, HT off | 1596798 | 4748981 | +197.4% | | 4 nodes, HT off | 1653817 | 4845590 | +193.0% | | 2 nodes, HT off | 3802258 | 3832017 | +0.8% | | 1 node , HT off | 2403297 | 2386401 | -0.7% | +-----------------+---------------------------------------------+ | | User Range 200 - 1000 | +-----------------+---------------------------------------------+ | 8 nodes, HT off | 1070992 | 6060457 | +465.9% | | 4 nodes, HT off | 1367668 | 6799978 | +397.2% | | 2 nodes, HT off | 4554370 | 4609893 | +1.2% | | 1 node , HT off | 2534826 | 2526519 | -0.3% | +-----------------+---------------------------------------------+ | | User Range 1100 - 2000 | +-----------------+---------------------------------------------+ | 8 nodes, HT off | 1061322 | 6435537 | +506.37 | | 4 nodes, HT off | 1365111 | 6589983 | +382.7% | | 2 nodes, HT off | 4583947 | 4648464 | +1.4% | | 1 node , HT off | 2563721 | 2566229 | +0.1% | +-----------------+----------------+-----------------+----------+
It can be seen that with 40 CPUs (4 nodes) or more, this patch can significantly improve the short workload performance. With only 1 or 2 nodes, the performance is similar with or without the patch. The short workload also scales pretty well up to 4 nodes with this patch.
A perf call-graph report of the short workload at 1500 users without the patch on the same 8-node machine indicates that about 78% of the workload's total time were spent in the _raw_spin_lock() function. Almost all of which can be attributed to the following 2 kernel functions:
1. dget_parent (49.91%)
2. dput (49.89%)
The relevant perf report lines are:
+ 78.37% reaim [kernel.kallsyms] [k] _raw_spin_lock + 0.09% reaim [kernel.kallsyms] [k] dput + 0.05% reaim [kernel.kallsyms] [k] _raw_spin_lock_irq + 0.00% reaim [kernel.kallsyms] [k] dget_parent
With this patch installed, the new perf report lines are:
+ 13.28% reaim [kernel.kallsyms] [k] _raw_spin_lock_irqsave + 7.33% reaim [kernel.kallsyms] [k] _raw_spin_lock + 2.93% reaim [kernel.kallsyms] [k] dget_parent + 1.32% reaim [kernel.kallsyms] [k] dput
- 7.33% reaim [kernel.kallsyms] [k] _raw_spin_lock
- _raw_spin_lock
+ 41.96% d_path
+ 41.68% sys_getcwd
+ 2.67% prepend_path
+ 1.66% complete_walk
+ 0.86% unlazy_walk
+ 0.74% sem_lock
+ 0.72% do_anonymous_page
+ 0.69% dget_parent
+ 0.67% dput
+ 0.55% process_backlog
+ 0.52% enqueue_to_backlog
The dput used up only 0.67% of the _raw_spin_lock time while dget_parent used only 0.69%. The time spent on dput and dget_parent did increase because of busy waiting for unlock as well as the overhead of doing cmpxchg operations.
This impact of this patch on other AIM7 workloads were much more modest. The table below show the mean %change due to this patch on the same 8-socket system with a 3.10-rc4 kernel.
+--------------+---------------+----------------+-----------------+ | Workload | mean % change | mean % change | mean % change | | | 10-100 users | 200-1000 users | 1100-2000 users | +--------------+---------------+----------------+-----------------+ | alltests | 0.0% | -0.3% | -0.3% | | five_sec | -4.6% | +6.5% | +3.1% | | fserver | -1.2% | -4.0% | -3.4% | | high_systime | -0.1% | +1.7% | +7.2% | | new_fserver | -2.8% | -3.3% | -2.1% | | shared | -0.6% | -0.2% | +0.2% | +--------------+---------------+----------------+-----------------+
There are slight drops in performance for fsever and new_fserver workloads, but slight increase in the high_systime and five_sec workloads.
--- fs/dcache.c | 14 ++++++- include/linux/dcache.h | 102 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 4 deletions(-)
fs/dcache.c +12 -2
@@ -515,6 +515,8 @@ void dput(struct dentry *dentry) repeat:if (dentry->d_count ==1 )might_sleep (); +if (__dput_unless_lt2_or_locked (dentry)) +return ;spin_lock (&dentry->d_lock);BUG_ON (!dentry->d_count);if (dentry->d_count >1 ) {
@@ -611,6 +613,8 @@ static inline void __dget_dlock(struct dentry *dentry)static inline void __dget (struct dentry *dentry) { +if (__dget_unless_zero_or_locked (dentry)) +return ;spin_lock (&dentry->d_lock);__dget_dlock (dentry);spin_unlock (&dentry->d_lock);
@@ -620,17 +624,23 @@ struct dentry *dget_parent(struct dentry *dentry) {struct dentry *ret; +rcu_read_lock (); + ret =rcu_dereference (dentry->d_parent); +if (__dget_unless_zero_or_locked (ret)) { +rcu_read_unlock (); +return ret; + } repeat:/* * Don't need rcu_dereference because we re-check it was correct under * the lock. */ -rcu_read_lock (); - ret = dentry->d_parent; + ret =ACCESS_ONCE (dentry->d_parent);spin_lock (&ret->d_lock);if (unlikely (ret != dentry->d_parent)) {spin_unlock (&ret->d_lock);rcu_read_unlock (); +rcu_read_lock (); goto repeat; }rcu_read_unlock ();
include/linux/dcache.h +20 -2
@@ -112,8 +112,13 @@ struct dentry {unsigned char d_iname[DNAME_INLINE_LEN ];/* small names */ /* Ref lookup also touches following */ -unsigned int d_count;/* protected by d_lock */ -spinlock_t d_lock;/* per dentry lock */ +union { +struct { +unsigned int d_count;/* protected by d_lock */ +spinlock_t d_lock;/* per dentry lock */ + }; +u64 d_cnt_lock;/* combined count & lock */ + };const struct dentry_operations *d_op;struct super_block *d_sb;/* The root of the dentry tree */ unsigned long d_time;/* used by d_revalidate */
@@ -132,6 +137,19 @@ struct dentry { };/* +* The compiler does not allow named union in struct dentry without adding +* a named field. The union definition is repeated below to allow functions +* to reference it. +*/ +union _d_cnt_lock { +struct { +unsigned int d_count;/* protected by d_lock */ +spinlock_t d_lock;/* per dentry lock */ + }; +u64 d_cnt_lock;/* combined count & lock */ +}; + +/* * dentry->d_lock spinlock nesting subclasses: * * 0: normal
@@ -325,6 +343,84 @@ static inline int __d_rcu_to_refcount(struct dentry *dentry
, unsigned seq)
return ret;
}
+/**
+ * __dget_unless_zero_or_locked - atomically inc d_count if != 0 and not locked
+ * @dentry: dentry to work on
+ * Return: 0 on failure, else 1
+ *
+ * __dget_if_notzero_and_locked tries to atomically increment d_count without
+ * taking a lock as long as the count is not 0 and d_lock is not taken.
+ */
+static inline int __dget_unless_zero_or_locked(struct dentry *dentry) +{
+ if (sizeof(union _d_cnt_lock) == sizeof(dentry->d_cnt_lock)) {
+ union _d_cnt_lock old, new;
+
+ spin_unlock_wait(&dentry->d_lock);
+ old.d_cnt_lock = ACCESS_ONCE(dentry->d_cnt_lock);
+ if ((old.d_count > 0) && spin_can_lock(&old.d_lock)) {
+ new.d_cnt_lock = old.d_cnt_lock;
+ new.d_count++;
+ if (cmpxchg(&dentry->d_cnt_lock, old.d_cnt_lock,+ new.d_cnt_lock) == old.d_cnt_lock)
+ return 1;
+ cpu_relax();
+ /*
+ * Try one more time
+ */
+ old.d_cnt_lock = ACCESS_ONCE(dentry->d_cnt_lock);
+ if ((old.d_count > 0) && spin_can_lock(&old.d_lock)) {
+ new.d_cnt_lock = old.d_cnt_lock; + new.d_count++; + if (cmpxchg(&dentry->d_cnt_lock, old.d_cnt_lock,
+ new.d_cnt_lock) == old.d_cnt_lock)
+ return 1;
+ cpu_relax();
+ }
+ }
+ }
+ return 0;
+}
+
+/**
+ * __dput_unless_lt2_or_locked - atomically dec d_count if >= 1 and not locked
+ * @dentry: dentry to work on
+ * Return: 0 on failure, else 1
+ *
+ * __dput_unless_leone_or_locked tries to atomically decrement d_count without
+ * taking a lock as long as the count is bigger than 1 and d_lock is not taken.
+ */
+static inline int __dput_unless_lt2_or_locked(struct dentry *dentry) +{
+ if (sizeof(union _d_cnt_lock) == sizeof(dentry->d_cnt_lock)) {
+ union _d_cnt_lock old, new;
+
+ spin_unlock_wait(&dentry->d_lock);
+ old.d_cnt_lock = ACCESS_ONCE(dentry->d_cnt_lock);
+ if ((old.d_count > 1) && spin_can_lock(&old.d_lock)) {
+ new.d_cnt_lock = old.d_cnt_lock;
+ new.d_count--;
+ if (cmpxchg(&dentry->d_cnt_lock, old.d_cnt_lock,+ new.d_cnt_lock) == old.d_cnt_lock)
+ return 1;
+ cpu_relax();
+ /*
+ * Try one more time
+ */
+ old.d_cnt_lock = ACCESS_ONCE(dentry->d_cnt_lock);
+ if ((old.d_count > 1) && spin_can_lock(&old.d_lock)) {
+ new.d_cnt_lock = old.d_cnt_lock; + new.d_count--; + if (cmpxchg(&dentry->d_cnt_lock, old.d_cnt_lock,
+ new.d_cnt_lock) == old.d_cnt_lock)
+ return 1;
+ cpu_relax();
+ }
+ }
+ }
+ return 0;
+}
+
/* validate "insecure" dentry pointer */
extern int d_validate(struct dentry *, struct dentry *);
@@ -359,6 +455,8 @@ static inline struct dentry *dget_dlock(struct dentry *dentr
y)
static inline struct dentry *dget(struct dentry *dentry)
{
if (dentry) {
+ if (__dget_unless_zero_or_locked(dentry))
+ return dentry;
spin_lock(&dentry->d_lock);
dget_dlock(dentry);
spin_unlock(&dentry->d_lock);Signature · 1 line
1.7.1
Quoted 12 lines · Waiman Long
Quoted 11 lines · Linus Torvalds
Ho humm.. interesting. I was talking about wanting to mix atomics and spinlocks earlier in this thread due to space constraints, and it strikes me that that would actually help this case a lot. Having the dentry count mix
d_lockand the count in one word would allow for atomic ops like "increment if not locked", and we'd avoid this whole race entirely..Something like "low bit of count is the lock bit" would end up being lovely for this case. Of course, that's not how our spinlocks work ..
Quoted 6 lines
Quoted 1 line
Linus
I have created another patch to do exactly the "increment if not locked" operation as suggested. It did help a lot. See the patch below for more information. Any additional comment will be appreciated.
Hmm. This is interesting and proves the concept, and the numbers look very promising.
The patch is not mergable, though, since it clearly depends on the spinlock/d_count fitting in a u64, which is normally true, but not the case of debugging locks etc, we'd need to generalize and fix the whole concept of "refcount+lock".
Generalizing it might be a good idea anyway, since there are other cases of "atomic_dec_and_lock()" etc behaviours where we might want to have these kinds of extended lock+count shenanigans.
I also do wonder if we could perhaps fit both in 32-bits, and just not use the "real" spinlocks at all, but use a bitlock in the low (or high) bit of the refcount. We do that in some other places - we'd potentially lose lockdep etc, and we'd lose some of the other good parts of spinlocks (fairness yadda yadda), but *if* we can reduce contention enough that it works out, maybe it would be worth it.
So this doesn't look like 3.11 material, but the numbers certainly make it look very promising, so with some more work on it ...
Linus
Quoted 17 lines · Linus Torvalds
Quoted 8 lines · Waiman Long
Quoted 7 lines · Linus Torvalds
Ho humm.. interesting. I was talking about wanting to mix atomics and spinlocks earlier in this thread due to space constraints, and it strikes me that that would actually help this case a lot. Having the dentry count mix
d_lockand the count in one word would allow for atomic ops like "increment if not locked", and we'd avoid this whole race entirely..Quoted 8 lines
Quoted 4 lines
Something like "low bit of count is the lock bit" would end up being lovely for this case. Of course, that's not how our spinlocks work ..
Linus
I have created another patch to do exactly the "increment if not locked" operation as suggested. It did help a lot. See the patch below for more information. Any additional comment will be appreciated.
Quoted 7 lines
Hmm. This is interesting and proves the concept, and the numbers look very promising.
The patch is not mergable, though, since it clearly depends on the spinlock/d_count fitting in a u64, which is normally true, but not the case of debugging locks etc, we'd need to generalize and fix the whole concept of "refcount+lock".
With some minor changes, the current patch can be modified to support debugging lock for 32-bit system. For 64-bit system, we can apply a similar concept for debugging lock with cmpxchg_double. However, for architecture that does not have cmpxchg_double support, it will be out of luck and we probably couldn't support the same feature in debugging mode. It will have to fall back to taking the lock.
I was thinking about generalizing the fix, but one issue that I was aware of was that the d_lock member of dentry had more than 300 references throughout the filesystem code. A general fix will require d_lock to be accessed in a different way. So it will be a pretty massive patch touching quite a lot of files even though the changes will be pretty straightforward in most cases.
Quoted 3 lines
Generalizing it might be a good idea anyway, since there are other cases of "
atomic_dec_and_lock()" etc behaviours where we might want to have these kinds of extended lock+count shenanigans.
The patch can certainly be generalized. I will see what I can do in a week of two.
Quoted 6 lines
I also do wonder if we could perhaps fit both in 32-bits, and just not use the "real" spinlocks at all, but use a bitlock in the low (or high) bit of the refcount. We do that in some other places - we'd potentially lose lockdep etc, and we'd lose some of the other good parts of spinlocks (fairness yadda yadda), but *if* we can reduce contention enough that it works out, maybe it would be worth it.
As the dentry is such an important data structure for the filesystem layer, losing the fairness attribute and the ability to debug may be too high a price to pay. For other niche cases, such a combined data type can certainly be used.
Quoted 4 lines
So this doesn't look like 3.11 material, but the numbers certainly make it look very promising, so with some more work on it ...
Linus
When will be the deadline for stable patch that can be considered to be merged in 3.11?
Regards,
Longman
Quoted 7 lines · Waiman Long
With some minor changes, the current patch can be modified to support debugging lock for 32-bit system. For 64-bit system, we can apply a similar concept for debugging lock with
cmpxchg_double. However, for architecture that does not havecmpxchg_doublesupport, it will be out of luck and we probably couldn't support the same feature in debugging mode. It will have to fall back to taking the lock.
That means only x86_64 and s390 would benefit from it ... I'm sure we can do better :-)
Cheers,
Ben.
Quoted 11 lines · Benjamin Herrenschmidt
Quoted 6 lines · Waiman Long
With some minor changes, the current patch can be modified to support debugging lock for 32-bit system. For 64-bit system, we can apply a similar concept for debugging lock with
cmpxchg_double. However, for architecture that does not havecmpxchg_doublesupport, it will be out of luck and we probably couldn't support the same feature in debugging mode. It will have to fall back to taking the lock.That means only
x86_64and s390 would benefit from it ... I'm sure we can do better :-)Cheers,
Ben.
On second thought, using cmpxchg_double may not be such a good idea after all as it requires a 16-byte alignment, at least for x86-64. Another possible alternative is to integrate the reference count directly into the spinlock_t data structure immediately after arch_spinlock_t for this special case. If CONFIG_GENERIC_LOCKBREAK is not defined, there will be a 4-byte hole that can be used. Otherwise, the spinlock_t structure will have an 8 byte size increase. I suppose that others won't be too upset for an 8-byte increase in size when spinlock debugging is turned on.
Regards,
Longman
Quoted 3 lines · Davidlohr Bueso
Waiman's dcache patchet were actually an attempt to address these exact
issues: http://lkml.org/lkml/2013/5/22/716
Ok, looking at that patch-set, I think it has the same race with not atomically getting the d_lock spinlock and d_count going down to zero in dput(). And Waiman clearly didn't know about "atomic_inc_not_zero()" or "atomic_dec_and_lock()" that are designed for exactly the "increment if already nonzero" and "decrement without taking the lock if we're not going down to zero" cases.
As outlined, I'm also not at all sure that the whole seqrw-lock thing that Waiman did is really necessary - I think the optimistic dget_parent() might be sufficient.
Linus
Quoted 10 lines · Davidlohr Bueso
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_secworkload benefited with +15% throughput after 500 users.
Thanks,
Quoted 18 lines
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 theinode->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:
Quoted 10 lines
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
| __getdents64Note, the mspin_lock is not interesting, as its not affected by this patch.
Quoted 23 lines
* 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_mutexand 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_add
Quoted 2 lines
With this patch:
2.14% 109982 reaim [kernel.kallsyms] [k]mspin_lock
Less time in the mspin_lock as it's probably now in the real spin lock somewhere.
Quoted 31 lines
| --- 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:
Quoted 12 lines
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_parentThis looks to be where the patch helps. The run without the patch is hammering away at the cacheline of the d_lock, which I'm sure shares the cache of other items in the dentry, such as the d_count. With the patch, the spin is on a separate cacheline and doesn't affect the owner so much.
Quoted 63 lines
* 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_queuelock. 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
Quoted 8 lines
|--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_accessInteresting that this trace doesn't show it going into patch code at all. I wonder if adding the slight overhead to the spin lock itself shifts things enough to get a benefit by avoiding contention?
-- Steve
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 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 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 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 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 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 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 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
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 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 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 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 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
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
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 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
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 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 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 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 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 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 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 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/
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/
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 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 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
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
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 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
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
Quoted 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
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. */ }
Re: [PATCH RFC ticketlock] v2 Auto-queued ticketlock
Quoted 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. */ }