From: Alex Kogan <hidden> Date: 2020-01-15 04:15:18
Minor changes from v8 based on feedback from Longman:
-----------------------------------------------------
- Add __init to cna_configure_spin_lock_slowpath().
- Fix the comment for cna_scan_main_queue().
- Change the type of intra_node_handoff_threshold to unsigned int.
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
CNA is a NUMA-aware version of the MCS lock. Spinning threads are
organized in two queues, a main queue for threads running on the same
node as the current lock holder, and a secondary queue for threads
running on other nodes. Threads store the ID of the node on which
they are running in their queue nodes. After acquiring the MCS lock and
before acquiring the spinlock, the lock holder scans the main queue
looking for a thread running on the same node (pre-scan). If found (call
it thread T), all threads in the main queue between the current lock
holder and T are moved to the end of the secondary queue. If such T
is not found, we make another scan of the main queue after acquiring
the spinlock when unlocking the MCS lock (post-scan), starting at the
node where pre-scan stopped. If both scans fail to find such T, the
MCS lock is passed to the first thread in the secondary queue. If the
secondary queue is empty, the MCS lock is passed to the next thread in the
main queue. To avoid starvation of threads in the secondary queue, those
threads are moved back to the head of the main queue after a certain
number of intra-node lock hand-offs.
More details are available at https://arxiv.org/abs/1810.05600.
The series applies on top of v5.5.0-rc6, commit b3a987b026.
Performance numbers are available in previous revisions
of the series.
Further comments are welcome and appreciated.
Alex Kogan (5):
locking/qspinlock: Rename mcs lock/unlock macros and make them more
generic
locking/qspinlock: Refactor the qspinlock slow path
locking/qspinlock: Introduce CNA into the slow path of qspinlock
locking/qspinlock: Introduce starvation avoidance into CNA
locking/qspinlock: Introduce the shuffle reduction optimization into
CNA
.../admin-guide/kernel-parameters.txt | 18 +
arch/arm/include/asm/mcs_spinlock.h | 6 +-
arch/x86/Kconfig | 20 +
arch/x86/include/asm/qspinlock.h | 4 +
arch/x86/kernel/alternative.c | 4 +
include/asm-generic/mcs_spinlock.h | 4 +-
kernel/locking/mcs_spinlock.h | 20 +-
kernel/locking/qspinlock.c | 82 +++-
kernel/locking/qspinlock_cna.h | 399 ++++++++++++++++++
kernel/locking/qspinlock_paravirt.h | 2 +-
10 files changed, 536 insertions(+), 23 deletions(-)
create mode 100644 kernel/locking/qspinlock_cna.h
--
2.21.0 (Apple Git-122.2)
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-15 04:15:16
Move some of the code manipulating the spin lock into separate functions.
This would allow easier integration of alternative ways to manipulate
that lock.
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
kernel/locking/qspinlock.c | 38 ++++++++++++++++++++++++++++++++++++--
1 file changed, 36 insertions(+), 2 deletions(-)
From: Alex Kogan <hidden> Date: 2020-01-15 04:15:16
In CNA, spinning threads are organized in two queues, a main queue for
threads running on the same node as the current lock holder, and a
secondary queue for threads running on other nodes. After acquiring the
MCS lock and before acquiring the spinlock, the lock holder scans the
main queue looking for a thread running on the same node (pre-scan). If
found (call it thread T), all threads in the main queue between the
current lock holder and T are moved to the end of the secondary queue.
If such T is not found, we make another scan of the main queue when
unlocking the MCS lock (post-scan), starting at the position where
pre-scan stopped. If both scans fail to find such T, the MCS lock is
passed to the first thread in the secondary queue. If the secondary queue
is empty, the lock is passed to the next thread in the main queue.
For more details, see https://arxiv.org/abs/1810.05600.
Note that this variant of CNA may introduce starvation by continuously
passing the lock to threads running on the same node. This issue
will be addressed later in the series.
Enabling CNA is controlled via a new configuration option
(NUMA_AWARE_SPINLOCKS). By default, the CNA variant is patched in at the
boot time only if we run on a multi-node machine in native environment and
the new config is enabled. (For the time being, the patching requires
CONFIG_PARAVIRT_SPINLOCKS to be enabled as well. However, this should be
resolved once static_call() is available.) This default behavior can be
overridden with the new kernel boot command-line option
"numa_spinlock=on/off" (default is "auto").
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
.../admin-guide/kernel-parameters.txt | 10 +
arch/x86/Kconfig | 20 ++
arch/x86/include/asm/qspinlock.h | 4 +
arch/x86/kernel/alternative.c | 4 +
kernel/locking/mcs_spinlock.h | 2 +-
kernel/locking/qspinlock.c | 39 ++-
kernel/locking/qspinlock_cna.h | 318 ++++++++++++++++++
7 files changed, 392 insertions(+), 5 deletions(-)
create mode 100644 kernel/locking/qspinlock_cna.h
@@ -3190,6 +3190,16 @@ nox2apic [X86-64,APIC] Do not enable x2APIC mode.+ numa_spinlock= [NUMA, PV_OPS] Select the NUMA-aware variant+ of spinlock. The options are:+ auto - Enable this variant if running on a multi-node+ machine in native environment.+ on - Unconditionally enable this variant.+ off - Unconditionally disable this variant.++ Not specifying this option is equivalent to+ numa_spinlock=auto.+ cpu0_hotplug [X86] Turn on CPU0 hotplug feature when CONFIG_BOOTPARAM_HOTPLUG_CPU0 is off. Some features depend on CPU0. Known dependencies are:
@@ -1562,6 +1562,26 @@ config NUMAOtherwise,youshouldsayN.+configNUMA_AWARE_SPINLOCKS+bool"Numa-aware spinlocks"+depends onNUMA+depends onQUEUED_SPINLOCKS+depends on64BIT+# For now, we depend on PARAVIRT_SPINLOCKS to make the patching work.+# This is awkward, but hopefully would be resolved once static_call()+# is available.+depends onPARAVIRT_SPINLOCKS+defaulty+help+IntroduceNUMA(NonUniformMemoryAccess)awarenessinto+theslowpathofspinlocks.++Inthisvariantofqspinlock,thekernelwilltrytokeepthelock+onthesamenode,thusreducingthenumberofremotecachemisses,+whiletradingsomeoftheshorttermfairnessforbetterperformance.++SayNifyouwantabsolutefirstcomefirstservefairness.+configAMD_NUMAdef_boolyprompt"Old style AMD Opteron NUMA detection"
@@ -0,0 +1,318 @@+/* SPDX-License-Identifier: GPL-2.0 */+#ifndef _GEN_CNA_LOCK_SLOWPATH+#error "do not include this file"+#endif++#include<linux/topology.h>++/*+*ImplementaNUMA-awareversionofMCS(akaCNA,orcompactNUMA-awarelock).+*+*InCNA,spinningthreadsareorganizedintwoqueues,amainqueuefor+*threadsrunningonthesameNUMAnodeasthecurrentlockholder,anda+*secondaryqueueforthreadsrunningonothernodes.Schematically,it+*lookslikethis:+*+*cna_node+*+----------++--------++--------++*|mcs:next|->|mcs:next|->...|mcs:next|->NULL[Mainqueue]+*|mcs:locked|-++--------++--------++*+----------+|+*+----------------------++*\/+*+--------++--------++*|mcs:next|->...|mcs:next|[Secondaryqueue]+*+--------++--------++*^|+*+--------------------++*+*N.B.locked=1ifsecondaryqueueisabsent.Othewrise,itcontainsthe+*encodedpointertothetailofthesecondaryqueue,whichisorganizedasa+*circularlist.+*+*AfteracquiringtheMCSlockandbeforeacquiringthespinlock,thelock+*holderscansthemainqueuelookingforathreadrunningonthesamenode+*(pre-scan).Iffound(callitthreadT),allthreadsinthemainqueue+*betweenthecurrentlockholderandTaremovedtotheendofthesecondary+*queue.IfsuchTisnotfound,wemakeanotherscanofthemainqueuewhen+*unlockingtheMCSlock(post-scan),startingatthenodewherepre-scan+*stopped.IfbothscansfailtofindsuchT,theMCSlockispassedtothe+*firstthreadinthesecondaryqueue.Ifthesecondaryqueueisempty,the+*lockispassedtothenextthreadinthemainqueue.+*+*Formoredetails,seehttps://arxiv.org/abs/1810.05600.+*+*Authors:AlexKogan<alex.kogan@oracle.com>+*DaveDice<dave.dice@oracle.com>+*/++structcna_node{+structmcs_spinlockmcs;+intnuma_node;+u32encoded_tail;+u32pre_scan_result;/* encoded tail or enum val */+};++enum{+LOCAL_WAITER_FOUND=2,/* 0 and 1 are reserved for @locked */+MIN_ENCODED_TAIL+};++staticvoid__initcna_init_nodes_per_cpu(unsignedintcpu)+{+structmcs_spinlock*base=per_cpu_ptr(&qnodes[0].mcs,cpu);+intnuma_node=cpu_to_node(cpu);+inti;++for(i=0;i<MAX_NODES;i++){+structcna_node*cn=(structcna_node*)grab_mcs_node(base,i);++cn->numa_node=numa_node;+cn->encoded_tail=encode_tail(cpu,i);+/*+*makesure@encoded_tailisnotconfusedwithothervalid+*valuesfor@locked(0or1)orwithdesignatedvaluesfor+*@pre_scan_result+*/+WARN_ON(cn->encoded_tail<MIN_ENCODED_TAIL);+}+}++staticint__initcna_init_nodes(void)+{+unsignedintcpu;++/*+*thiswillbreakon32bitarchitectures,sowerestrict+*theuseofCNAto64bitonly(seearch/x86/Kconfig)+*/+BUILD_BUG_ON(sizeof(structcna_node)>sizeof(structqnode));+/* we store an ecoded tail word in the node's @locked field */+BUILD_BUG_ON(sizeof(u32)>sizeof(unsignedint));++for_each_possible_cpu(cpu)+cna_init_nodes_per_cpu(cpu);++return0;+}+early_initcall(cna_init_nodes);++/* this function is called only when the primary queue is empty */+staticinlineboolcna_try_change_tail(structqspinlock*lock,u32val,+structmcs_spinlock*node)+{+structmcs_spinlock*head_2nd,*tail_2nd;+u32new;++/* If the secondary queue is empty, do what MCS does. */+if(node->locked<=1)+return__try_clear_tail(lock,val,node);++/*+*Trytoupdatethetailvaluetothelastnodeinthesecondaryqueue.+*Ifsuccessful,passthelocktothefirstthreadinthesecondary+*queue.Doingthosetwoactionseffectivelymovesallnodesfromthe+*secondaryqueueintothemainone.+*/+tail_2nd=decode_tail(node->locked);+head_2nd=tail_2nd->next;+new=((structcna_node*)tail_2nd)->encoded_tail+_Q_LOCKED_VAL;++if(atomic_try_cmpxchg_relaxed(&lock->val,&val,new)){+/*+*Trytoreset@nextintail_2ndtoNULL,butnoneedtocheck+*theresult-iffailed,anewsuccessorhasupdatedit.+*/+cmpxchg_relaxed(&tail_2nd->next,head_2nd,NULL);+arch_mcs_pass_lock(&head_2nd->locked,1);+returntrue;+}++returnfalse;+}++/*+*cna_splice_tail--splicenodesinthemainqueuebetween[first,last]+*ontothesecondaryqueue.+*/+staticvoidcna_splice_tail(structmcs_spinlock*node,+structmcs_spinlock*first,+structmcs_spinlock*last)+{+/* remove [first,last] */+node->next=last->next;++/* stick [first,last] on the secondary queue tail */+if(node->locked<=1){/* if secondary queue is empty */+/* create secondary queue */+last->next=first;+}else{+/* add to the tail of the secondary queue */+structmcs_spinlock*tail_2nd=decode_tail(node->locked);+structmcs_spinlock*head_2nd=tail_2nd->next;++tail_2nd->next=first;+last->next=head_2nd;+}++node->locked=((structcna_node*)last)->encoded_tail;+}++/*+*cna_scan_main_queue-scanthemainwaitingqueuelookingforthefirst+*threadrunningonthesameNUMAnodeasthelockholder.Iffound(callit+*threadT),moveallthreadsinthemainqueuebetweenthelockholderand+*TtotheendofthesecondaryqueueandreturnLOCAL_WAITER_FOUND;+*otherwise,returntheencodedpointerofthelastscannednodeinthe+*primaryqueue(soasubsequentscancanberesumedfromthatnode).+*+*Schematically,thismaylooklikethefollowing(nnstandsfornuma_nodeand+*etstandsforencoded_tail).+*+*whencna_scan_main_queue()iscalled(thesecondaryqueueisempty):+*+*A+------------+B+--------+C+--------+T+--------++*|mcs:next|->|mcs:next|->|mcs:next|->|mcs:next|->NULL+*|mcs:locked=1||cna:nn=0||cna:nn=2||cna:nn=1|+*|cna:nn=1|+--------++--------++--------++*+-----------++*+*whencna_scan_main_queue()returns(thesecondaryqueuecontainsBandC):+*+*A+----------------+T+--------++*|mcs:next|->|mcs:next|->NULL+*|mcs:locked=C.et|-+|cna:nn=1|+*|cna:nn=1||+--------++*+---------------++-----++*\/+*B+--------+C+--------++*|mcs:next|->|mcs:next|-++*|cna:nn=0||cna:nn=2||+*+--------++--------+|+*^|+*+---------------------++*+*TheworstcasecomplexityofthescanisO(n),wherenisthenumber+*ofcurrentwaiters.However,theamortizedcomplexityisclosetoO(1),+*astheimmediatesuccessorislikelytoberunningonthesamenodeonce+*threadsfromothernodesaremovedtothesecondaryqueue.+*+*@node:PointertotheMCSnodeofthelockholder+*@pred_start:PointertotheMCSnodeofthewaiterwhosesuccessorshouldbe+*thefirstnodeinthescan+*Return:LOCAL_WAITER_FOUNDorencodedtailofthelastscannedwaiter+*/+staticu32cna_scan_main_queue(structmcs_spinlock*node,+structmcs_spinlock*pred_start)+{+structcna_node*cn=(structcna_node*)node;+structcna_node*cni=(structcna_node*)READ_ONCE(pred_start->next);+structcna_node*last;+intmy_numa_node=cn->numa_node;++/* find any next waiter on 'our' NUMA node */+for(last=cn;+cni&&cni->numa_node!=my_numa_node;+last=cni,cni=(structcna_node*)READ_ONCE(cni->mcs.next))+;++/* if found, splice any skipped waiters onto the secondary queue */+if(cni){+if(last!=cn)/* did we skip any waiters? */+cna_splice_tail(node,node->next,+(structmcs_spinlock*)last);+returnLOCAL_WAITER_FOUND;+}++returnlast->encoded_tail;+}++__always_inlineu32cna_pre_scan(structqspinlock*lock,+structmcs_spinlock*node)+{+structcna_node*cn=(structcna_node*)node;++cn->pre_scan_result=cna_scan_main_queue(node,node);++return0;+}++staticinlinevoidcna_pass_lock(structmcs_spinlock*node,+structmcs_spinlock*next)+{+structcna_node*cn=(structcna_node*)node;+structmcs_spinlock*next_holder=next,*tail_2nd;+u32val=1;++u32scan=cn->pre_scan_result;++/*+*checkifasuccessorfromthesamenumanodehasnotbeenfoundin+*pre-scan,andifso,trytofinditinpost-scanstartingfromthe+*nodewherepre-scanstopped(storedin@pre_scan_result)+*/+if(scan>=MIN_ENCODED_TAIL)+scan=cna_scan_main_queue(node,decode_tail(scan));++if(scan==LOCAL_WAITER_FOUND){+next_holder=node->next;+/*+*weunlocksuccessorbypassinganon-zerovalue,+*soset@valto1iff@lockedis0,whichwillhappen+*ifweacquiredtheMCSlockwhenitsqueuewasempty+*/+val=node->locked?node->locked:1;+}elseif(node->locked>1){/* if secondary queue is not empty */+/* next holder will be the first node in the secondary queue */+tail_2nd=decode_tail(node->locked);+/* @tail_2nd->next points to the head of the secondary queue */+next_holder=tail_2nd->next;+/* splice the secondary queue onto the head of the main queue */+tail_2nd->next=next;+}++arch_mcs_pass_lock(&next_holder->locked,val);+}++/*+*Constant(boot-paramconfigurable)flagselectingtheNUMA-awarevariant+*ofspinlock.Possiblevalues:-1(off)/0(auto,default)/1(on).+*/+staticintnuma_spinlock_flag;++staticint__initnuma_spinlock_setup(char*str)+{+if(!strcmp(str,"auto")){+numa_spinlock_flag=0;+return1;+}elseif(!strcmp(str,"on")){+numa_spinlock_flag=1;+return1;+}elseif(!strcmp(str,"off")){+numa_spinlock_flag=-1;+return1;+}++return0;+}+__setup("numa_spinlock=",numa_spinlock_setup);++void__cna_queued_spin_lock_slowpath(structqspinlock*lock,u32val);++/*+*SwitchtotheNUMA-friendlyslowpathforspinlockswhenwehave+*multipleNUMAnodesinnativeenvironment,unlesstheuserhas+*overriddenthisdefaultbehaviorbysettingthenuma_spinlockflag.+*/+void__initcna_configure_spin_lock_slowpath(void)+{+if((numa_spinlock_flag==1)||+(numa_spinlock_flag==0&&nr_node_ids>1&&+pv_ops.lock.queued_spin_lock_slowpath==+native_queued_spin_lock_slowpath)){+pv_ops.lock.queued_spin_lock_slowpath=+__cna_queued_spin_lock_slowpath;++pr_info("Enabling CNA spinlock\n");+}+}
--
2.21.0 (Apple Git-122.2)
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-15 04:15:18
Keep track of the number of intra-node lock handoffs, and force
inter-node handoff once this number reaches a preset threshold.
The default value for the threshold can be overridden with
the new kernel boot command-line option "numa_spinlock_threshold".
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
.../admin-guide/kernel-parameters.txt | 8 ++++
kernel/locking/qspinlock.c | 3 ++
kernel/locking/qspinlock_cna.h | 41 ++++++++++++++++++-
3 files changed, 51 insertions(+), 1 deletion(-)
@@ -3200,6 +3200,14 @@ Not specifying this option is equivalent to numa_spinlock=auto.+ numa_spinlock_threshold= [NUMA, PV_OPS]+ Set the threshold for the number of intra-node+ lock hand-offs before the NUMA-aware spinlock+ is forced to be passed to a thread on another NUMA node.+ Valid values are in the [0..31] range. Smaller values+ result in a more fair, but less performant spinlock, and+ vice versa. The default value is 16.+ cpu0_hotplug [X86] Turn on CPU0 hotplug feature when CONFIG_BOOTPARAM_HOTPLUG_CPU0 is off. Some features depend on CPU0. Known dependencies are:
@@ -51,13 +51,25 @@ struct cna_node {intnuma_node;u32encoded_tail;u32pre_scan_result;/* encoded tail or enum val */+u32intra_count;};enum{LOCAL_WAITER_FOUND=2,/* 0 and 1 are reserved for @locked */+FLUSH_SECONDARY_QUEUE=3,MIN_ENCODED_TAIL};+/*+*Controlsthethresholdforthenumberofintra-nodelockhand-offsbefore+*theNUMA-awarevariantofspinlockisforcedtobepassedtoathreadon+*anotherNUMAnode.Bydefault,thechosenvalueprovidesreasonable+*long-termfairnesswithoutsacrificingperformancecomparedtoalock+*thatdoesnothaveanyfairnessguarantees.Thedefaultsettingcan+*bechangedwiththe"numa_spinlock_threshold"bootoption.+*/+unsignedintintra_node_handoff_threshold__ro_after_init=1<<16;+staticvoid__initcna_init_nodes_per_cpu(unsignedintcpu){structmcs_spinlock*base=per_cpu_ptr(&qnodes[0].mcs,cpu);
@@ -97,6 +109,11 @@ static int __init cna_init_nodes(void)}early_initcall(cna_init_nodes);+static__always_inlinevoidcna_init_node(structmcs_spinlock*node)+{+((structcna_node*)node)->intra_count=0;+}+/* this function is called only when the primary queue is empty */staticinlineboolcna_try_change_tail(structqspinlock*lock,u32val,structmcs_spinlock*node)
@@ -262,6 +281,9 @@ static inline void cna_pass_lock(struct mcs_spinlock *node,*ifweacquiredtheMCSlockwhenitsqueuewasempty*/val=node->locked?node->locked:1;+/* inc @intra_count if the secondary queue is not empty */+((structcna_node*)next_holder)->intra_count=+cn->intra_count+(node->locked>1);}elseif(node->locked>1){/* if secondary queue is not empty *//* next holder will be the first node in the secondary queue */tail_2nd=decode_tail(node->locked);
@@ -316,3 +338,20 @@ void __init cna_configure_spin_lock_slowpath(void)pr_info("Enabling CNA spinlock\n");}}++staticint__initnuma_spinlock_threshold_setup(char*str)+{+intnew_threshold_param;++if(get_option(&str,&new_threshold_param)){+/* valid value is between 0 and 31 */+if(new_threshold_param<0||new_threshold_param>31)+return0;++intra_node_handoff_threshold=1<<new_threshold_param;+return1;+}++return0;+}+__setup("numa_spinlock_threshold=",numa_spinlock_threshold_setup);
--
2.21.0 (Apple Git-122.2)
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-15 04:15:20
This performance optimization reduces the probability threads will be
shuffled between the main and secondary queues when the secondary queue
is empty. It is helpful when the lock is only lightly contended.
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
kernel/locking/qspinlock_cna.h | 46 ++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
@@ -57,6 +58,7 @@ struct cna_node {enum{LOCAL_WAITER_FOUND=2,/* 0 and 1 are reserved for @locked */FLUSH_SECONDARY_QUEUE=3,+PASS_LOCK_IMMEDIATELY=4,MIN_ENCODED_TAIL};
From: Alex Kogan <hidden> Date: 2020-01-15 04:16:24
The mcs unlock macro (arch_mcs_pass_lock) should accept the value to be
stored into the lock argument as another argument. This allows using the
same macro in cases where the value to be stored when passing the lock is
different from 1.
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
arch/arm/include/asm/mcs_spinlock.h | 6 +++---
include/asm-generic/mcs_spinlock.h | 4 ++--
kernel/locking/mcs_spinlock.h | 18 +++++++++---------
kernel/locking/qspinlock.c | 4 ++--
kernel/locking/qspinlock_paravirt.h | 2 +-
5 files changed, 17 insertions(+), 17 deletions(-)
Hi Alex,
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune. As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock? This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this email, I try to provide some formal analysis to address this
question. Let's assume the probability for the lock to stay on the
same socket is *at least* p, which corresponds to the probability for
the function probably(unsigned int num_bits) in the patch to return *false*,
where SHUFFLE_REDUCTION_PROB_ARG is passed as the value of num_bits to the
function.
I noticed that the default value of p in the patch is 1/2^7 = 0.01, which is
somewhat counter-intuitive to me. If we switch sockets 99 times out of 100,
then fairness should be obvious. What I expected is a (much) higher value of
p, which would likely result in better performance, while having some degree
of fairness guarantee. Have you run some experiments by setting a lower
SHUFFLE_REDUCTION_PROB_ARG instead of the default value 7? It would be very
helpful to know the performance numbers.
Now let's do some analysis:
1. What is the probability P for the first thread on a different socket to
acquire the lock after *at most* N consecutive local lock handovers?
Note: N corresponds to the variable intra_node_handoff_threshold in the
patch, which is set to value 1 << numa_spinlock_threshold. Default value
is 1 << 16 = 64K.
Assuming mutual independence [1], we have P is equal to 1 - p^N, where p^N is
the probability of N consecutive threads running on the socket where the lock
was most recently acquired.
If p is 0.99, the probabilities of switching to a different socket after
N local lock handovers are as follows:
63.4% (N = 100)
86.6% (N = 200)
99.3% (N = 500)
99.996% (N = 1000)
99.99999999999933% (N = 64K)
2. We can ask the same question as above for the k-th thread on a different
socket from the lock holder. That is, what is the probability P for the k-th
thread on a different socket to acquire the lock after *at most* N
consecutive local lock handovers, assuming all these k threads in the queue
are running on different sockets (the worst case scenario). The analysis is
as follows (the case when k = 1 reduces to Question 1 above):
The total probability P is the sum of Pi for i = 0, 1, ..., N, where Pi is
the probability of having i *total* local lock handovers before the k-th
thread on a different socket can acquire the lock.
Pi can be calculated using formula Pi = B_i_k * (p^i) * (1 - p)^k, where
-- B_i_k is the number of ways to put i balls into k buckets, representing
all possible ways the i local handovers occurred in k different sockets.
B_i_k is a multiset number and equal to (i + k - 1)! / (i! * (k-1)!) [2]
-- p^i is the probability of i local lock handovers
-- (1 - p)^k is the probability of k socket switchings
I've written a simple Python script to calculate the value of P.
Let's look at some concrete examples and numbers.
When p = 0.99, k = 3 (e.g. a 4-socket system), P is equal to:
8.5% (N = 100)
33.2% (N = 200)
87.9% (N = 500)
99.7% (N = 1000)
99.99999999999937% (N = 64K)
When p = 0.99, k = 7 (e.g. an 8-socket system), the values of P are:
0.01% (N = 100)
0.52% (N = 200)
24.7% (N = 500)
87.5% (N = 1000)
99.3% (N = 1500)
99.99999999999871% (N = 64K)
I think this mathematical analysis would help users better understand the
fairness property of the CNA qspinlock. One can use it to plot a graph with
different values of p and N to tune the qspinlock for different platforms
and workloads.
Based on the analysis above, it may be useful to have
SHUFFLE_REDUCTION_PROB_ARG as a tunable parameter as well. Setting
SHUFFLE_REDUCTION_PROB_ARG to a lower value results in a higher value of p,
which would likely increase the performance. Then we can set
intra_node_handoff_threshold to have a bounded degree of fairness.
For instance, a user may want P to be around 90% for N = 100 on a 8-core
system. So they can set p = 0.9 and intra_node_handoff_threshold = ~150,
based on our analysis that P = 91.9% for N = 100, and 99.99% for N = 200,
when k = 7.
I hope this helps and please let me know if you have any comments or
if you spot any mistakes in our analysis.
Best,
Lihao.
References:
[1] https://en.wikipedia.org/wiki/Independence_(probability_theory)#More_than_two_events
[2] Theorem 2, https://en.wikipedia.org/wiki/Stars_and_bars_(combinatorics)
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Waiman Long <longman@redhat.com> Date: 2020-01-22 17:25:10
On 1/22/20 6:45 AM, Lihao Liang wrote:
Hi Alex,
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune. As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock? This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this email, I try to provide some formal analysis to address this
question. Let's assume the probability for the lock to stay on the
same socket is *at least* p, which corresponds to the probability for
the function probably(unsigned int num_bits) in the patch to return *false*,
where SHUFFLE_REDUCTION_PROB_ARG is passed as the value of num_bits to the
function.
That is not strictly true from my understanding of the code. The
probably() function does not come into play if a secondary queue is
present. Also calling cna_scan_main_queue() doesn't guarantee that a
waiter in the same node can be found. So the simple mathematical
analysis isn't that applicable in this case. One will have to do an
actual simulation to find out what the actual behavior will be.
The comment in the code states that:
/*
* Controls the probability for enabling the scan of the main queue when
* the secondary queue is empty. The chosen value reduces the amount of
* unnecessary shuffling of threads between the two waiting queues when
* the contention is low, while responding fast enough and enabling
* the shuffling when the contention is high.
*/
#define SHUFFLE_REDUCTION_PROB_ARG (7)
Cheers,
Longman
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-22 19:30:52
Hi, Lihao.
On Jan 22, 2020, at 6:45 AM, Lihao Liang [off-list ref] wrote:
Hi Alex,
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune.
This has been the case in the first versions of the series, but is not true anymore.
That is, the long-term fairness is achieved deterministically (and you are correct
that it is done through the numa_spinlock_threshold parameter).
As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock?
The SHUFFLE_REDUCTION_PROB_ARG parameter is intended for performance
optimization only, and *does not* affect the long-term fairness (or, at the
very least, does not make it any worse). As Longman correctly pointed out in
his response to this email, the shuffle reduction optimization is relevant only
when the secondary queue is empty. In that case, CNA hands-off the lock
exactly as MCS does, i.e., in the FIFO order. Note that when the secondary
queue is not empty, we do not call probably().
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
Hopefully, it addresses your concern. Let me know if you have any further
questions.
Best regards,
— Alex
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Peter Zijlstra <peterz@infradead.org> Date: 2020-01-23 09:27:18
On Tue, Jan 14, 2020 at 10:59:18PM -0500, Alex Kogan wrote:
+/* this function is called only when the primary queue is empty */
+static inline bool cna_try_change_tail(struct qspinlock *lock, u32 val,
+ struct mcs_spinlock *node)
+{
+ struct mcs_spinlock *head_2nd, *tail_2nd;
+ u32 new;
+
+ /* If the secondary queue is empty, do what MCS does. */
+ if (node->locked <= 1)
+ return __try_clear_tail(lock, val, node);
+
+ /*
+ * Try to update the tail value to the last node in the secondary queue.
+ * If successful, pass the lock to the first thread in the secondary
+ * queue. Doing those two actions effectively moves all nodes from the
+ * secondary queue into the main one.
+ */
+ tail_2nd = decode_tail(node->locked);
+ head_2nd = tail_2nd->next;
+ new = ((struct cna_node *)tail_2nd)->encoded_tail + _Q_LOCKED_VAL;
+
+ if (atomic_try_cmpxchg_relaxed(&lock->val, &val, new)) {
+ /*
+ * Try to reset @next in tail_2nd to NULL, but no need to check
+ * the result - if failed, a new successor has updated it.
+ */
I think you actually have an ordering bug here; the load of head_2nd
*must* happen before the atomic_try_cmpxchg(), otherwise it might
observe the new next and clear a valid next pointer.
What would be the best fix for that; I'm thinking:
head_2nd = smp_load_acquire(&tail_2nd->next);
Will?
From: Peter Zijlstra <peterz@infradead.org> Date: 2020-01-23 10:07:05
On Thu, Jan 23, 2020 at 10:26:58AM +0100, Peter Zijlstra wrote:
On Tue, Jan 14, 2020 at 10:59:18PM -0500, Alex Kogan wrote:
quoted
+/* this function is called only when the primary queue is empty */
+static inline bool cna_try_change_tail(struct qspinlock *lock, u32 val,
+ struct mcs_spinlock *node)
+{
+ struct mcs_spinlock *head_2nd, *tail_2nd;
+ u32 new;
+
+ /* If the secondary queue is empty, do what MCS does. */
+ if (node->locked <= 1)
+ return __try_clear_tail(lock, val, node);
+
+ /*
+ * Try to update the tail value to the last node in the secondary queue.
+ * If successful, pass the lock to the first thread in the secondary
+ * queue. Doing those two actions effectively moves all nodes from the
+ * secondary queue into the main one.
+ */
+ tail_2nd = decode_tail(node->locked);
+ head_2nd = tail_2nd->next;
+ new = ((struct cna_node *)tail_2nd)->encoded_tail + _Q_LOCKED_VAL;
+
+ if (atomic_try_cmpxchg_relaxed(&lock->val, &val, new)) {
+ /*
+ * Try to reset @next in tail_2nd to NULL, but no need to check
+ * the result - if failed, a new successor has updated it.
+ */
I think you actually have an ordering bug here; the load of head_2nd
*must* happen before the atomic_try_cmpxchg(), otherwise it might
observe the new next and clear a valid next pointer.
What would be the best fix for that; I'm thinking:
head_2nd = smp_load_acquire(&tail_2nd->next);
Will?
Hmm, given we've not passed the lock around yet; why wouldn't something
like this work:
smp_store_release(&tail_2nd->next, NULL);
if (!atomic_try_cmpxchg_relaxed(&lock, &val, new)) {
tail_2nd->next = head_2nd;
return false;
}
The whole second queue is only ever modified by the lock owner, and that
is us, so we can pre-terminate the secondary queue (break the circular
link), try the cmpxchg and fix it back up when it fails.
From: Peter Zijlstra <peterz@infradead.org> Date: 2020-01-23 10:17:06
On Thu, Jan 23, 2020 at 11:06:35AM +0100, Peter Zijlstra wrote:
On Thu, Jan 23, 2020 at 10:26:58AM +0100, Peter Zijlstra wrote:
quoted
On Tue, Jan 14, 2020 at 10:59:18PM -0500, Alex Kogan wrote:
quoted
+/* this function is called only when the primary queue is empty */
+static inline bool cna_try_change_tail(struct qspinlock *lock, u32 val,
+ struct mcs_spinlock *node)
+{
+ struct mcs_spinlock *head_2nd, *tail_2nd;
+ u32 new;
+
+ /* If the secondary queue is empty, do what MCS does. */
+ if (node->locked <= 1)
+ return __try_clear_tail(lock, val, node);
+
+ /*
+ * Try to update the tail value to the last node in the secondary queue.
+ * If successful, pass the lock to the first thread in the secondary
+ * queue. Doing those two actions effectively moves all nodes from the
+ * secondary queue into the main one.
+ */
+ tail_2nd = decode_tail(node->locked);
+ head_2nd = tail_2nd->next;
+ new = ((struct cna_node *)tail_2nd)->encoded_tail + _Q_LOCKED_VAL;
+
+ if (atomic_try_cmpxchg_relaxed(&lock->val, &val, new)) {
+ /*
+ * Try to reset @next in tail_2nd to NULL, but no need to check
+ * the result - if failed, a new successor has updated it.
+ */
I think you actually have an ordering bug here; the load of head_2nd
*must* happen before the atomic_try_cmpxchg(), otherwise it might
observe the new next and clear a valid next pointer.
What would be the best fix for that; I'm thinking:
head_2nd = smp_load_acquire(&tail_2nd->next);
Will?
Hmm, given we've not passed the lock around yet; why wouldn't something
like this work:
smp_store_release(&tail_2nd->next, NULL);
Argh, make that:
tail_2nd->next = NULL;
smp_wmb();
if (!atomic_try_cmpxchg_relaxed(&lock, &val, new)) {
tail_2nd->next = head_2nd;
return false;
}
The whole second queue is only ever modified by the lock owner, and that
is us, so we can pre-terminate the secondary queue (break the circular
link), try the cmpxchg and fix it back up when it fails.
From: Will Deacon <will@kernel.org> Date: 2020-01-23 11:22:59
On Thu, Jan 23, 2020 at 11:16:49AM +0100, Peter Zijlstra wrote:
On Thu, Jan 23, 2020 at 11:06:35AM +0100, Peter Zijlstra wrote:
quoted
On Thu, Jan 23, 2020 at 10:26:58AM +0100, Peter Zijlstra wrote:
quoted
On Tue, Jan 14, 2020 at 10:59:18PM -0500, Alex Kogan wrote:
quoted
+/* this function is called only when the primary queue is empty */
+static inline bool cna_try_change_tail(struct qspinlock *lock, u32 val,
+ struct mcs_spinlock *node)
+{
+ struct mcs_spinlock *head_2nd, *tail_2nd;
+ u32 new;
+
+ /* If the secondary queue is empty, do what MCS does. */
+ if (node->locked <= 1)
+ return __try_clear_tail(lock, val, node);
+
+ /*
+ * Try to update the tail value to the last node in the secondary queue.
+ * If successful, pass the lock to the first thread in the secondary
+ * queue. Doing those two actions effectively moves all nodes from the
+ * secondary queue into the main one.
+ */
+ tail_2nd = decode_tail(node->locked);
+ head_2nd = tail_2nd->next;
+ new = ((struct cna_node *)tail_2nd)->encoded_tail + _Q_LOCKED_VAL;
+
+ if (atomic_try_cmpxchg_relaxed(&lock->val, &val, new)) {
+ /*
+ * Try to reset @next in tail_2nd to NULL, but no need to check
+ * the result - if failed, a new successor has updated it.
+ */
I think you actually have an ordering bug here; the load of head_2nd
*must* happen before the atomic_try_cmpxchg(), otherwise it might
observe the new next and clear a valid next pointer.
What would be the best fix for that; I'm thinking:
head_2nd = smp_load_acquire(&tail_2nd->next);
Will?
Hmm, given we've not passed the lock around yet; why wouldn't something
like this work:
smp_store_release(&tail_2nd->next, NULL);
Argh, make that:
tail_2nd->next = NULL;
smp_wmb();
quoted
if (!atomic_try_cmpxchg_relaxed(&lock, &val, new)) {
... or could you drop the smp_wmb() and make this
atomic_try_cmpxchg_release()?
To be honest, I've failed to understand the code prior to your changes
in this area: it appears to reply on a control-dependency from the two
cmpxchg_relaxed() calls (which isn't sufficient to order the store parts
afaict) and I also don't get how we deal with a transiently circular primary
queue.
Will
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Will Deacon <will@kernel.org> Date: 2020-01-23 11:35:56
Hi folks,
(I think Lihao is travelling at the moment, so he may be delayed in his
replies)
On Wed, Jan 22, 2020 at 12:24:58PM -0500, Waiman Long wrote:
On 1/22/20 6:45 AM, Lihao Liang wrote:
quoted
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune. As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock? This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this email, I try to provide some formal analysis to address this
question. Let's assume the probability for the lock to stay on the
same socket is *at least* p, which corresponds to the probability for
the function probably(unsigned int num_bits) in the patch to return *false*,
where SHUFFLE_REDUCTION_PROB_ARG is passed as the value of num_bits to the
function.
That is not strictly true from my understanding of the code. The
probably() function does not come into play if a secondary queue is
present. Also calling cna_scan_main_queue() doesn't guarantee that a
waiter in the same node can be found. So the simple mathematical
analysis isn't that applicable in this case. One will have to do an
actual simulation to find out what the actual behavior will be.
It's certainly true that the analysis is based on the worst-case scenario,
but I think it's still worth considering. For example, the secondary queue
does not exist initially so it seems a bit odd that we only instantiate it
with < 1% probability.
That said, my real concern with any of this is that it makes formal
modelling and analysis of the qspinlock considerably more challenging. I
would /really/ like to see an update to the TLA+ model we have of the
current implementation [1] and preferably also the userspace version I
hacked together [2] so that we can continue to test and validate changes
to the code outside of the usual kernel stress-testing.
Will
[1] https://git.kernel.org/pub/scm/linux/kernel/git/cmarinas/kernel-tla.git/
[2] https://mirrors.edge.kernel.org/pub/linux/kernel/people/will/spinbench/
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Peter Zijlstra <peterz@infradead.org> Date: 2020-01-23 13:18:02
On Thu, Jan 23, 2020 at 11:22:51AM +0000, Will Deacon wrote:
quoted
Argh, make that:
tail_2nd->next = NULL;
smp_wmb();
quoted
if (!atomic_try_cmpxchg_relaxed(&lock, &val, new)) {
... or could you drop the smp_wmb() and make this
atomic_try_cmpxchg_release()?
My current code has the smp_wmb(), because most _releases end up being
an smp_mb() (except for powerpc where it is of equal cost to wmb and
arm64, where I have no idea of the costs).
To be honest, I've failed to understand the code prior to your changes
in this area: it appears to reply on a control-dependency from the two
cmpxchg_relaxed() calls (which isn't sufficient to order the store parts
afaict) and I also don't get how we deal with a transiently circular primary
queue.
Ha!, yes, so this little piece took me a while too. Let me attempt an
explanation.
+ * cna_node
+ * +----------+ +--------+ +--------+
+ * |mcs:next | --> |mcs:next| --> ... |mcs:next| --> NULL [Primary queue]
+ * |mcs:locked| -. +--------+ +--------+
+ * +----------+ |
+ * `----------------------.
+ * v
+ * +--------+ +--------+
+ * |mcs:next| --> ... |mcs:next| [Secondary queue]
+ * +--------+ +--------+
+ * ^ |
+ * `--------------------'
So @node is the current lock holder, node->next == NULL (primary queue
is empty) and we're going to try and splice the secondary queue to the
head of the primary.
+ tail_2nd = decode_tail(node->locked);
+ head_2nd = tail_2nd->next;
this gets the secondary head and tail, so far so simple
+ new = ((struct cna_node *)tail_2nd)->encoded_tail + _Q_LOCKED_VAL;
this encodes the new primary tail (as kept in lock->val), still simple
+ if (atomic_try_cmpxchg_relaxed(&lock->val, &val, new)) {
if this here succeeds, we've got the primary tail pointing at the
secondary tail. This is safe because only the lock holder (us) ever
modifies the secondary queue.
+ /*
+ * Try to reset @next in tail_2nd to NULL, but no need to check
+ * the result - if failed, a new successor has updated it.
+ */
+ cmpxchg_relaxed(&tail_2nd->next, head_2nd, NULL);
This is (broken, as per the prior argument) breaking the circular link
the secondary queue has. The trick here is that since we're the lock
holder, nothing will actually iterate the primary ->next chain, so a
bogus value in there is of no concern.
_However_ a new waiter might at this point do:
old = xchg_tail(lock, node);
if (old) {
prev = decode_tail(old);
WRITE_ONCE(prev->next, node);
...
}
which then results in conflicting stores to the one ->next variable.
The cmpxchg() is attempting to terminate the list, while the new waiter
is extending the list, it is therefore paramount the new waiter always
wins this. To that end they're employing the cmpxchg, but it very much
relies on the @head_2nd load to have happened before we exposed the
secondary tail as primary tail, otherwise it can have loaded the new
->next pointer and overwriten it.
+ arch_mcs_pass_lock(&head_2nd->locked, 1);
+ return true;
+ }
+
+ return false;
Did that help, or just make it worse?
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Waiman Long <longman@redhat.com> Date: 2020-01-23 14:16:09
On 1/14/20 10:59 PM, Alex Kogan wrote:
+static int __init cna_init_nodes(void)
+{
+ unsigned int cpu;
+
+ /*
+ * this will break on 32bit architectures, so we restrict
+ * the use of CNA to 64bit only (see arch/x86/Kconfig)
+ */
+ BUILD_BUG_ON(sizeof(struct cna_node) > sizeof(struct qnode));
+ /* we store an ecoded tail word in the node's @locked field */
+ BUILD_BUG_ON(sizeof(u32) > sizeof(unsigned int));
+
+ for_each_possible_cpu(cpu)
+ cna_init_nodes_per_cpu(cpu);
+
+ return 0;
+}
+early_initcall(cna_init_nodes);
+
I just realized that you shouldn't call cna_init_nodes as an
early_initcall. Instead,
+/*
+ * Switch to the NUMA-friendly slow path for spinlocks when we have
+ * multiple NUMA nodes in native environment, unless the user has
+ * overridden this default behavior by setting the numa_spinlock flag.
+ */
+void __init cna_configure_spin_lock_slowpath(void)
+{
+ if ((numa_spinlock_flag == 1) ||
+ (numa_spinlock_flag == 0 && nr_node_ids > 1 &&
+ pv_ops.lock.queued_spin_lock_slowpath ==
+ native_queued_spin_lock_slowpath)) {
+ pv_ops.lock.queued_spin_lock_slowpath =
+ __cna_queued_spin_lock_slowpath;
+
+ pr_info("Enabling CNA spinlock\n");
+ }
+}
call it when it is sure that CNA spinlock is going to be used. At this
point, the system is still in UP mode and the slowpath will not be called.
Cheers,
Longman
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Waiman Long <longman@redhat.com> Date: 2020-01-23 15:25:24
On 1/23/20 6:35 AM, Will Deacon wrote:
Hi folks,
(I think Lihao is travelling at the moment, so he may be delayed in his
replies)
On Wed, Jan 22, 2020 at 12:24:58PM -0500, Waiman Long wrote:
quoted
On 1/22/20 6:45 AM, Lihao Liang wrote:
quoted
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune. As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock? This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this email, I try to provide some formal analysis to address this
question. Let's assume the probability for the lock to stay on the
same socket is *at least* p, which corresponds to the probability for
the function probably(unsigned int num_bits) in the patch to return *false*,
where SHUFFLE_REDUCTION_PROB_ARG is passed as the value of num_bits to the
function.
That is not strictly true from my understanding of the code. The
probably() function does not come into play if a secondary queue is
present. Also calling cna_scan_main_queue() doesn't guarantee that a
waiter in the same node can be found. So the simple mathematical
analysis isn't that applicable in this case. One will have to do an
actual simulation to find out what the actual behavior will be.
It's certainly true that the analysis is based on the worst-case scenario,
but I think it's still worth considering. For example, the secondary queue
does not exist initially so it seems a bit odd that we only instantiate it
with < 1% probability.
That said, my real concern with any of this is that it makes formal
modelling and analysis of the qspinlock considerably more challenging. I
would /really/ like to see an update to the TLA+ model we have of the
current implementation [1] and preferably also the userspace version I
hacked together [2] so that we can continue to test and validate changes
to the code outside of the usual kernel stress-testing.
I do agree that the current CNA code is hard to model. The CNA lock
behaves like a regular qspinlock in many cases. If the lock becomes
fairly contended with waiters from different nodes, it will
opportunistically switch to CNA mode where preference is given to
waiters in the same node.
Cheers,
Longman
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Peter Zijlstra <peterz@infradead.org> Date: 2020-01-23 15:29:54
On Thu, Jan 23, 2020 at 09:15:55AM -0500, Waiman Long wrote:
On 1/14/20 10:59 PM, Alex Kogan wrote:
quoted
+static int __init cna_init_nodes(void)
+{
+ unsigned int cpu;
+
+ /*
+ * this will break on 32bit architectures, so we restrict
+ * the use of CNA to 64bit only (see arch/x86/Kconfig)
+ */
+ BUILD_BUG_ON(sizeof(struct cna_node) > sizeof(struct qnode));
+ /* we store an ecoded tail word in the node's @locked field */
+ BUILD_BUG_ON(sizeof(u32) > sizeof(unsigned int));
+
+ for_each_possible_cpu(cpu)
+ cna_init_nodes_per_cpu(cpu);
+
+ return 0;
+}
+early_initcall(cna_init_nodes);
+
I just realized that you shouldn't call cna_init_nodes as an
early_initcall. Instead,
quoted
+/*
+ * Switch to the NUMA-friendly slow path for spinlocks when we have
+ * multiple NUMA nodes in native environment, unless the user has
+ * overridden this default behavior by setting the numa_spinlock flag.
+ */
+void __init cna_configure_spin_lock_slowpath(void)
+{
+ if ((numa_spinlock_flag == 1) ||
+ (numa_spinlock_flag == 0 && nr_node_ids > 1 &&
+ pv_ops.lock.queued_spin_lock_slowpath ==
+ native_queued_spin_lock_slowpath)) {
+ pv_ops.lock.queued_spin_lock_slowpath =
+ __cna_queued_spin_lock_slowpath;
+
+ pr_info("Enabling CNA spinlock\n");
+ }
+}
call it when it is sure that CNA spinlock is going to be used. At this
point, the system is still in UP mode and the slowpath will not be called.
From: Waiman Long <longman@redhat.com> Date: 2020-01-23 19:08:41
On 1/23/20 10:25 AM, Waiman Long wrote:
On 1/23/20 6:35 AM, Will Deacon wrote:
quoted
Hi folks,
(I think Lihao is travelling at the moment, so he may be delayed in his
replies)
On Wed, Jan 22, 2020 at 12:24:58PM -0500, Waiman Long wrote:
quoted
On 1/22/20 6:45 AM, Lihao Liang wrote:
quoted
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune. As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock? This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this email, I try to provide some formal analysis to address this
question. Let's assume the probability for the lock to stay on the
same socket is *at least* p, which corresponds to the probability for
the function probably(unsigned int num_bits) in the patch to return *false*,
where SHUFFLE_REDUCTION_PROB_ARG is passed as the value of num_bits to the
function.
That is not strictly true from my understanding of the code. The
probably() function does not come into play if a secondary queue is
present. Also calling cna_scan_main_queue() doesn't guarantee that a
waiter in the same node can be found. So the simple mathematical
analysis isn't that applicable in this case. One will have to do an
actual simulation to find out what the actual behavior will be.
It's certainly true that the analysis is based on the worst-case scenario,
but I think it's still worth considering. For example, the secondary queue
does not exist initially so it seems a bit odd that we only instantiate it
with < 1% probability.
That said, my real concern with any of this is that it makes formal
modelling and analysis of the qspinlock considerably more challenging. I
would /really/ like to see an update to the TLA+ model we have of the
current implementation [1] and preferably also the userspace version I
hacked together [2] so that we can continue to test and validate changes
to the code outside of the usual kernel stress-testing.
I do agree that the current CNA code is hard to model. The CNA lock
behaves like a regular qspinlock in many cases. If the lock becomes
fairly contended with waiters from different nodes, it will
opportunistically switch to CNA mode where preference is given to
waiters in the same node.
BTW, I added the attached draft lock_event patch on top of the v9 CNA
patch series to observe the behavior of the CNA lock. Using a 2-socket
96-thread x86-64 server, the lock event output after boot up was:
cna_intra_max=1942
cna_mainscan_hit=134
cna_merge_queue=73
cna_prescan_hit=16662
cna_prescan_miss=268
cna_splice_new=352
cna_splice_old=2415
lock_pending=130090
lock_slowpath=191868
lock_use_node2=135
After resetting the counts and running a 96-thread lock stress test for
10s, I got
cna_intra_max=65536
cna_mainscan_hit=46
cna_merge_queue=661
cna_prescan_hit=42486841
cna_prescan_miss=68
cna_splice_new=676
cna_splice_old=402
lock_pending=11012
lock_slowpath=44332335
lock_use_node2=57203
So the cna_intra_max does go to the maximum of 64k.
Cheers,
Longman
From: Waiman Long <longman@redhat.com> Date: 2020-01-23 19:55:37
On 1/14/20 10:59 PM, Alex Kogan wrote:
quoted hunk
Keep track of the number of intra-node lock handoffs, and force
inter-node handoff once this number reaches a preset threshold.
The default value for the threshold can be overridden with
the new kernel boot command-line option "numa_spinlock_threshold".
Signed-off-by: Alex Kogan <redacted>
Reviewed-by: Steve Sistare <redacted>
Reviewed-by: Waiman Long <longman@redhat.com>
---
.../admin-guide/kernel-parameters.txt | 8 ++++
kernel/locking/qspinlock.c | 3 ++
kernel/locking/qspinlock_cna.h | 41 ++++++++++++++++++-
3 files changed, 51 insertions(+), 1 deletion(-)
@@ -3200,6 +3200,14 @@ Not specifying this option is equivalent to numa_spinlock=auto.+ numa_spinlock_threshold= [NUMA, PV_OPS]+ Set the threshold for the number of intra-node+ lock hand-offs before the NUMA-aware spinlock+ is forced to be passed to a thread on another NUMA node.+ Valid values are in the [0..31] range. Smaller values+ result in a more fair, but less performant spinlock, and+ vice versa. The default value is 16.+ cpu0_hotplug [X86] Turn on CPU0 hotplug feature when CONFIG_BOOTPARAM_HOTPLUG_CPU0 is off. Some features depend on CPU0. Known dependencies are:
@@ -51,13 +51,25 @@ struct cna_node {intnuma_node;u32encoded_tail;u32pre_scan_result;/* encoded tail or enum val */+u32intra_count;};enum{LOCAL_WAITER_FOUND=2,/* 0 and 1 are reserved for @locked */+FLUSH_SECONDARY_QUEUE=3,MIN_ENCODED_TAIL};+/*+*Controlsthethresholdforthenumberofintra-nodelockhand-offsbefore+*theNUMA-awarevariantofspinlockisforcedtobepassedtoathreadon+*anotherNUMAnode.Bydefault,thechosenvalueprovidesreasonable+*long-termfairnesswithoutsacrificingperformancecomparedtoalock+*thatdoesnothaveanyfairnessguarantees.Thedefaultsettingcan+*bechangedwiththe"numa_spinlock_threshold"bootoption.+*/+unsignedintintra_node_handoff_threshold__ro_after_init=1<<16;+staticvoid__initcna_init_nodes_per_cpu(unsignedintcpu){structmcs_spinlock*base=per_cpu_ptr(&qnodes[0].mcs,cpu);
@@ -97,6 +109,11 @@ static int __init cna_init_nodes(void)}early_initcall(cna_init_nodes);+static__always_inlinevoidcna_init_node(structmcs_spinlock*node)+{+((structcna_node*)node)->intra_count=0;+}+/* this function is called only when the primary queue is empty */staticinlineboolcna_try_change_tail(structqspinlock*lock,u32val,structmcs_spinlock*node)
@@ -262,6 +281,9 @@ static inline void cna_pass_lock(struct mcs_spinlock *node,*ifweacquiredtheMCSlockwhenitsqueuewasempty*/val=node->locked?node->locked:1;+/* inc @intra_count if the secondary queue is not empty */+((structcna_node*)next_holder)->intra_count=+cn->intra_count+(node->locked>1);
Playing with lock event counts, I would like you to change the meaning
intra_count parameter that you are tracking. Instead of tracking the
number of times a lock is passed to a waiter of the same node
consecutively, I would like you to track the number of times the head
waiter in the secondary queue has given up its chance to acquire the
lock because a later waiter has jumped the queue and acquire the lock
before it. This value determines the worst case latency that a secondary
queue waiter can experience. So
mcs_spinlock *node,
*/
val = node->locked ? node->locked : 1;
- /* inc @intra_count if the secondary queue is not empty */
- next_cn->intra_count = cn->intra_count + (node->locked > 1);
+ /*
+ * inc @intra_count and pass it down if the secondary queue
+ * is not empty
+ */
+ if (node->locked > 1)
+ next_cn->intra_count = cn->intra_count + 1;
} else if (node->locked > 1) { /* if secondary queue is not
empty */
/* next holder will be the first node in the secondary
queue */
Maybe rename it to jump_count or some other more meaningful name. With
that change, we could probably reduce the default threshold from 64k to
maybe 256 or 512.
I changed the threshold to 256 and run a 96-thread locking stress test
for 10s, the lock event counts:
cna_flush_queue=15687
cna_intra_max=256
cna_mainscan_hit=13
cna_merge_queue=15691
cna_prescan_hit=4344037
cna_prescan_miss=21
cna_splice_new=15701
cna_splice_old=1289
lock_pending=4384
lock_slowpath=47998292
lock_use_node2=16778
Of the prescan hits, only about 0.4% of that resulted in a queue flush
which I thought is reasonable. I didn't see any noticeable degradation
in the performance of the stress test by reducing the threshold from 64k
to 256.
Cheers,
Longman
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Waiman Long <longman@redhat.com> Date: 2020-01-23 20:39:12
On 1/23/20 2:55 PM, Waiman Long wrote:
Playing with lock event counts, I would like you to change the meaning
intra_count parameter that you are tracking. Instead of tracking the
number of times a lock is passed to a waiter of the same node
consecutively, I would like you to track the number of times the head
waiter in the secondary queue has given up its chance to acquire the
lock because a later waiter has jumped the queue and acquire the lock
before it. This value determines the worst case latency that a secondary
queue waiter can experience. So
Well, that is not strictly true as a a waiter in the middle of the
secondary queue can go back and fro between the queues for a number of
times. Of course, if we can ensure that when a FLUSH_SECONDARY_QUEUE is
issued, those waiters that were in the secondary queue won't be put back
into the secondary queue again. The parameter will then really determine
the worst case latency.
One way to do it is to store the tail of the secondary queue into the
CNA node and passed it down the queue until it matches the current
encoded tail. That will require changing both numa_node and intra_count
into u16 to squeeze out space for another u32.
That will also make the code a bit easier to analyze.
Cheers,
Longman
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-23 23:40:34
On Jan 23, 2020, at 3:39 PM, Waiman Long [off-list ref] wrote:
On 1/23/20 2:55 PM, Waiman Long wrote:
quoted
Playing with lock event counts, I would like you to change the meaning
intra_count parameter that you are tracking. Instead of tracking the
number of times a lock is passed to a waiter of the same node
consecutively, I would like you to track the number of times the head
waiter in the secondary queue has given up its chance to acquire the
lock because a later waiter has jumped the queue and acquire the lock
before it.
Isn’t that the same thing? Note that we keep track of the number of
intra-node lock transfers only when the secondary queue is not empty.
quoted
This value determines the worst case latency that a secondary
queue waiter can experience. So
Well, that is not strictly true as a a waiter in the middle of the
secondary queue can go back and fro between the queues for a number of
times. Of course, if we can ensure that when a FLUSH_SECONDARY_QUEUE is
issued, those waiters that were in the secondary queue won't be put back
into the secondary queue again.
This will not work as intended when we have more than 2 nodes. That is, if we
have threads from node A & B in the secondary queue, and then the queue
is flushed and its head (say, from node A) gets the lock, we want to push
threads from node B back into the secondary queue, to keep the lock on node A.
And if we have only 2 nodes, a waiter in the middle of the secondary queue will
never go back into the secondary queue, even if the threshold is small.
This is because we flush the secondary queue by putting all its waiters in
the front of the main queue, and the secondary queue will remain empty at least
until we reach a thread from another node.
Regards,
— Alex
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: "Paul E. McKenney" <paulmck@kernel.org> Date: 2020-01-24 22:24:36
On Tue, Jan 14, 2020 at 10:59:15PM -0500, Alex Kogan wrote:
Minor changes from v8 based on feedback from Longman:
-----------------------------------------------------
- Add __init to cna_configure_spin_lock_slowpath().
- Fix the comment for cna_scan_main_queue().
- Change the type of intra_node_handoff_threshold to unsigned int.
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
CNA is a NUMA-aware version of the MCS lock. Spinning threads are
organized in two queues, a main queue for threads running on the same
node as the current lock holder, and a secondary queue for threads
running on other nodes. Threads store the ID of the node on which
they are running in their queue nodes. After acquiring the MCS lock and
before acquiring the spinlock, the lock holder scans the main queue
looking for a thread running on the same node (pre-scan). If found (call
it thread T), all threads in the main queue between the current lock
holder and T are moved to the end of the secondary queue. If such T
is not found, we make another scan of the main queue after acquiring
the spinlock when unlocking the MCS lock (post-scan), starting at the
node where pre-scan stopped. If both scans fail to find such T, the
MCS lock is passed to the first thread in the secondary queue. If the
secondary queue is empty, the MCS lock is passed to the next thread in the
main queue. To avoid starvation of threads in the secondary queue, those
threads are moved back to the head of the main queue after a certain
number of intra-node lock hand-offs.
More details are available at https://arxiv.org/abs/1810.05600.
The series applies on top of v5.5.0-rc6, commit b3a987b026.
Performance numbers are available in previous revisions
of the series.
Further comments are welcome and appreciated.
I ran this on a large system with a version of locktorture that was
modified to print out the maximum and minimum per-CPU lock-acquisition
counts, and with CPU hotplug disabled. I also modified the LOCK01 and
LOCK04 scenarios to use 220 hardware threads.
Here is what the test ended up with at the end of a one-hour run:
LOCK01 (exclusive):
Writes: Total: 1241107333 Max/Min: 9206962/60902 ??? Fail: 0
LOCK04 (rwlock):
Writes: Total: 232991963 Max/Min: 2631574/74582 ??? Fail: 0
Reads : Total: 216935386 Max/Min: 2735939/28665 ??? Fail: 0
The "???" strings are printed because the ratio of maximum to minimum exceeds
a factor of two.
I also ran 30-minute runs on my laptop, which has 12 hardware threads:
LOCK01 (exclusive):
Writes: Total: 3992072782 Max/Min: 259368782/97231961 ??? Fail: 0
LOCK04 (rwlock):
Writes: Total: 131063892 Max/Min: 13136206/5876157 ??? Fail: 0
Reads : Total: 144876801 Max/Min: 19999535/4873442 ??? Fail: 0
These also exceed the factor-of-two cutoff, but not as dramatically.
The readers for the reader-writer lock fared worst, with a 4-to-1 ratio.
These tests did run within guest OSes. Is that configuration out of
scope for this locking algorithm? In addition (as might well also have
been the case for the locktorture runs in your paper), these tests run
a pair of stress-test tasks for each hardware thread.
Is this expected behavior?
Thanx, Paul
Alex Kogan (5):
locking/qspinlock: Rename mcs lock/unlock macros and make them more
generic
locking/qspinlock: Refactor the qspinlock slow path
locking/qspinlock: Introduce CNA into the slow path of qspinlock
locking/qspinlock: Introduce starvation avoidance into CNA
locking/qspinlock: Introduce the shuffle reduction optimization into
CNA
.../admin-guide/kernel-parameters.txt | 18 +
arch/arm/include/asm/mcs_spinlock.h | 6 +-
arch/x86/Kconfig | 20 +
arch/x86/include/asm/qspinlock.h | 4 +
arch/x86/kernel/alternative.c | 4 +
include/asm-generic/mcs_spinlock.h | 4 +-
kernel/locking/mcs_spinlock.h | 20 +-
kernel/locking/qspinlock.c | 82 +++-
kernel/locking/qspinlock_cna.h | 399 ++++++++++++++++++
kernel/locking/qspinlock_paravirt.h | 2 +-
10 files changed, 536 insertions(+), 23 deletions(-)
create mode 100644 kernel/locking/qspinlock_cna.h
--
2.21.0 (Apple Git-122.2)
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Hi Alex and Waiman,
Thanks a lot for your swift response and clarification.
On Wed, Jan 22, 2020 at 7:30 PM Alex Kogan [off-list ref] wrote:
Hi, Lihao.
quoted
On Jan 22, 2020, at 6:45 AM, Lihao Liang [off-list ref] wrote:
Hi Alex,
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune.
This has been the case in the first versions of the series, but is not true anymore.
That is, the long-term fairness is achieved deterministically (and you are correct
that it is done through the numa_spinlock_threshold parameter).
quoted
As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock?
The SHUFFLE_REDUCTION_PROB_ARG parameter is intended for performance
optimization only, and *does not* affect the long-term fairness (or, at the
very least, does not make it any worse). As Longman correctly pointed out in
his response to this email, the shuffle reduction optimization is relevant only
when the secondary queue is empty. In that case, CNA hands-off the lock
exactly as MCS does, i.e., in the FIFO order. Note that when the secondary
queue is not empty, we do not call probably().
quoted
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
You're right that the latest version of the patch handles long-term fairness
deterministically.
As I understand it, the n-th thread in the main queue is guaranteed to
acquire the lock after N lock handovers, where N is bounded by
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
I'm not sure what role the variable nr_cpus_per_node plays in your analysis.
Do I miss anything?
Many thanks,
Lihao.
Hopefully, it addresses your concern. Let me know if you have any further
questions.
Best regards,
— Alex
On Sun, Jan 26, 2020 at 12:32 AM Lihao Liang [off-list ref] wrote:
Hi Alex and Waiman,
Thanks a lot for your swift response and clarification.
On Wed, Jan 22, 2020 at 7:30 PM Alex Kogan [off-list ref] wrote:
quoted
Hi, Lihao.
quoted
On Jan 22, 2020, at 6:45 AM, Lihao Liang [off-list ref] wrote:
Hi Alex,
On Wed, Jan 22, 2020 at 10:28 AM Alex Kogan [off-list ref] wrote:
quoted
Summary
-------
Lock throughput can be increased by handing a lock to a waiter on the
same NUMA node as the lock holder, provided care is taken to avoid
starvation of waiters on other NUMA nodes. This patch introduces CNA
(compact NUMA-aware lock) as the slow path for qspinlock. It is
enabled through a configuration option (NUMA_AWARE_SPINLOCKS).
Thanks for your patches. The experimental results look promising!
I understand that the new CNA qspinlock uses randomization to achieve
long-term fairness, and provides the numa_spinlock_threshold parameter
for users to tune.
This has been the case in the first versions of the series, but is not true anymore.
That is, the long-term fairness is achieved deterministically (and you are correct
that it is done through the numa_spinlock_threshold parameter).
quoted
As Linux runs extremely diverse workloads, it is not
clear how randomization affects its fairness, and how users with
different requirements are supposed to tune this parameter.
To this end, Will and I consider it beneficial to be able to answer the
following question:
With different values of numa_spinlock_threshold and
SHUFFLE_REDUCTION_PROB_ARG, how long do threads running on different
sockets have to wait to acquire the lock?
The SHUFFLE_REDUCTION_PROB_ARG parameter is intended for performance
optimization only, and *does not* affect the long-term fairness (or, at the
very least, does not make it any worse). As Longman correctly pointed out in
his response to this email, the shuffle reduction optimization is relevant only
when the secondary queue is empty. In that case, CNA hands-off the lock
exactly as MCS does, i.e., in the FIFO order. Note that when the secondary
queue is not empty, we do not call probably().
quoted
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
You're right that the latest version of the patch handles long-term fairness
deterministically.
As I understand it, the n-th thread in the main queue is guaranteed to
acquire the lock after N lock handovers, where N is bounded by
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
I'm not sure what role the variable nr_cpus_per_node plays in your analysis.
Do I miss anything?
If I understand correctly, there are two phases in the algorithm:
MCS phase: when the secondary queue is empty, as explained in your emails,
the algorithm hands the lock to threads in the main queue in an FIFO order.
When probably(SHUFFLE_REDUCTION_PROB_ARG) returns false (with default
probability 1%), if the algorithm finds the first thread running on the same
socket as the lock holder in cna_scan_main_queue(), it enters the following
CNA phase.
CNA phase: when the secondary queue is not empty, the algorithm keeps
handing the lock to threads in the main queue that run on the same socket as
the lock holder. When 2^numa_spinlock_threshold is reached, it splices
the secondary queue to the front of the main queue. And we are back to the
MCS phase above.
For the n-th thread T in the main queue, the MCS phase handles threads that
arrived in the main queue before T. In high contention situations, the CNA
phase handles two kinds of threads:
1. Threads ahead of T that run on the same socket as the lock holder when
a transition from the MCS to CNA phase was made. Assume there are m such
threads.
2. Threads that keep arriving on the same socket as the lock holder. There
are at most 2^numa_spinlock_threshold of them.
Then the number of lock handovers in the CNA phase is max(m,
2^numa_spinlock_threshold). So the total number of lock handovers before T
acquires the lock is at most
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
Please let me know if I misunderstand anything.
Many thanks,
Lihao.
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-27 06:17:14
quoted
quoted
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
You're right that the latest version of the patch handles long-term fairness
deterministically.
As I understand it, the n-th thread in the main queue is guaranteed to
acquire the lock after N lock handovers, where N is bounded by
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
I'm not sure what role the variable nr_cpus_per_node plays in your analysis.
Yeah, that’s a minor point, but let me try to clarify.
The "n-th thread in the main queue” is (at most) the nr_cpus_per_node-th thread
for some node k. So when the node k gets the preference, that thread will
get the lock after at most nr_cpus_per_node-1 lock transitions. As we consider
the upper bound, your analysis is also correct; mine is just a bit tighter.
Makes sense?
Regards,
— Alex
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
From: Alex Kogan <hidden> Date: 2020-01-27 16:02:48
Hi, Lihao.
quoted
quoted
quoted
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
You're right that the latest version of the patch handles long-term fairness
deterministically.
As I understand it, the n-th thread in the main queue is guaranteed to
acquire the lock after N lock handovers, where N is bounded by
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
I'm not sure what role the variable nr_cpus_per_node plays in your analysis.
Do I miss anything?
If I understand correctly, there are two phases in the algorithm:
MCS phase: when the secondary queue is empty, as explained in your emails,
the algorithm hands the lock to threads in the main queue in an FIFO order.
When probably(SHUFFLE_REDUCTION_PROB_ARG) returns false (with default
probability 1%), if the algorithm finds the first thread running on the same
socket as the lock holder in cna_scan_main_queue(), it enters the following
CNA phase
Yep. When probably() returns false, we scan the main queue. If as the result of
this scan the secondary queue becomes not empty, we enter what you call
the CNA phase.
.
CNA phase: when the secondary queue is not empty, the algorithm keeps
handing the lock to threads in the main queue that run on the same socket as
the lock holder. When 2^numa_spinlock_threshold is reached, it splices
the secondary queue to the front of the main queue. And we are back to the
MCS phase above.
Correct.
For the n-th thread T in the main queue, the MCS phase handles threads that
arrived in the main queue before T. In high contention situations, the CNA
phase handles two kinds of threads:
1. Threads ahead of T that run on the same socket as the lock holder when
a transition from the MCS to CNA phase was made. Assume there are m such
threads.
2. Threads that keep arriving on the same socket as the lock holder. There
are at most 2^numa_spinlock_threshold of them.
Then the number of lock handovers in the CNA phase is max(m,
2^numa_spinlock_threshold). So the total number of lock handovers before T
acquires the lock is at most
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
Please let me know if I misunderstand anything.
I think you got it right (modulo nr_cpus_per_node instead of n, as mentioned in
my other response).
Regards,
— Alex
_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Hi Alex and Waiman,
On Mon, Jan 27, 2020 at 4:02 PM Alex Kogan [off-list ref] wrote:
Hi, Lihao.
quoted
quoted
quoted
quoted
This is particularly relevant
in high contention situations when new threads keep arriving on the same
socket as the lock holder.
In this case, the lock will stay on the same NUMA node/socket for
2^numa_spinlock_threshold times, which is the worst case scenario if we
consider the long-term fairness. And if we have multiple nodes, it will take
up to 2^numa_spinlock_threshold X (nr_nodes - 1) + nr_cpus_per_node
lock transitions until any given thread will acquire the lock
(assuming 2^numa_spinlock_threshold > nr_cpus_per_node).
You're right that the latest version of the patch handles long-term fairness
deterministically.
As I understand it, the n-th thread in the main queue is guaranteed to
acquire the lock after N lock handovers, where N is bounded by
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
I'm not sure what role the variable nr_cpus_per_node plays in your analysis.
Do I miss anything?
If I understand correctly, there are two phases in the algorithm:
MCS phase: when the secondary queue is empty, as explained in your emails,
the algorithm hands the lock to threads in the main queue in an FIFO order.
When probably(SHUFFLE_REDUCTION_PROB_ARG) returns false (with default
probability 1%), if the algorithm finds the first thread running on the same
socket as the lock holder in cna_scan_main_queue(), it enters the following
CNA phase
Yep. When probably() returns false, we scan the main queue. If as the result of
this scan the secondary queue becomes not empty, we enter what you call
the CNA phase.
As I understand it, the probability of making a transition from the
MCS to CNA phase
in less than N lock handovers is 1 - p^N, where p is the probability
that probably()
returns true (default 99%). So in high contention situations where N can become
quite large in a relatively short period of time, the probability of
getting into the CNA
phase is high, e.g. 95% when N = 300.
I was wondering whether it would be possible to detect contention and make a
phase transition deterministically, maybe by reusing the intra_count
variable to keep
track of the processing rate in the MCS phase?
As Will pointed out earlier, this would make formal analysis and
verification of the
CNA qspinlock much more feasible.
quoted
.
CNA phase: when the secondary queue is not empty, the algorithm keeps
handing the lock to threads in the main queue that run on the same socket as
the lock holder. When 2^numa_spinlock_threshold is reached, it splices
the secondary queue to the front of the main queue. And we are back to the
MCS phase above.
Correct.
quoted
For the n-th thread T in the main queue, the MCS phase handles threads that
arrived in the main queue before T. In high contention situations, the CNA
phase handles two kinds of threads:
1. Threads ahead of T that run on the same socket as the lock holder when
a transition from the MCS to CNA phase was made. Assume there are m such
threads.
2. Threads that keep arriving on the same socket as the lock holder. There
are at most 2^numa_spinlock_threshold of them.
Then the number of lock handovers in the CNA phase is max(m,
2^numa_spinlock_threshold). So the total number of lock handovers before T
acquires the lock is at most
n - 1 + 2^numa_spinlock_threshold * (nr_nodes - 1)
Please let me know if I misunderstand anything.
I think you got it right (modulo nr_cpus_per_node instead of n, as mentioned in
my other response).