Mutable Locks: Combining the Best of Spin and Sleep Locks

06/02/2019
by   Romolo Marotta, et al.
0

In this article we present Mutable Locks, a synchronization construct with the same execution semantic of traditional locks (such as spin locks or sleep locks), but with a self-tuned optimized trade off between responsiveness---in the access to a just released critical section---and CPU-time usage during threads' wait phases. It tackles the need for modern synchronization supports, in the era of multi-core machines, whose runtime behavior should be optimized along multiple dimensions (performance vs resource consumption) with no intervention by the application programmer. Our proposal is intended for exploitation in generic concurrent applications where scarce or none knowledge is available about the underlying software/hardware stack and the actual workload, an adverse scenario for static choices between spinning and sleeping faced by mutable locks just thanks to their hybrid waiting phases and self-tuning capabilities.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 7

07/30/2017

Adaptive Performance Optimization under Power Constraint in Multi-thread Applications with Diverse Scalability

In modern data centers, energy usage represents one of the major factors...
03/30/2021

The Matter of Time – A General and Efficient System for Precise Sensor Synchronization in Robotic Computing

Time synchronization is a critical task in robotic computing such as aut...
05/02/2018

Decoupling GPU Programming Models from Resource Management for Enhanced Programming Ease, Portability, and Performance

The application resource specification--a static specification of severa...
09/14/2018

Lightweight Synchronization Algorithm with Self-Calibration for Industrial LORA Sensor Networks

Wireless sensor and actuator networks are gaining momentum in the era of...
11/12/2018

Transkernel: An Executor for Commodity Kernels on Peripheral Cores

Modern mobile and embedded platforms see a large number of ephemeral tas...
02/07/2018

Zorua: Enhancing Programming Ease, Portability, and Performance in GPUs by Decoupling Programming Models from Resource Management

The application resource specification--a static specification of severa...
04/29/2020

Compilation of Coordinated Choice

Recently, we have proposed coordinated choices, which are nondeterminist...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Modern multi-core chipsets and the ever-growing adoption of concurrent programming in daily-usage software are posing new synchronization challenges. Non-coherent cache architectures—such as Intel SCC (Single-chip Cloud Computer) [13]—look to be a way for reducing the complexity of classical cache-coherency protocols. However, these solutions significantly impact software design, since hardware-level coherency becomes fully demanded from software—which needs to rely on explicit message passing across cores to let updated data values flow in the caching hierarchy.

Hardware-Transactional-Memory (HTM) allows atomic and isolated accesses to slices of shared data in multi-core machines. This solution is however not viable in many scenarios because of limitations in the HTM firmware, like the impossibility to successfully finalize (commit) data manipulations in face of hardware events such as interrupts. For this reason, HTM is often used in combination with traditional locking primitives, which enable isolated shared-data accesses otherwise not sustainable via HTM.

The Software Transactional Memory (STM) counterpart avoids HTM-related limitations. However, STM internal mechanisms (e.g. [3]) still rely on locks to enable atomic and isolated management of the metadata that the STM layer exploits to assess the correctness (e.g. the isolation) of data accesses performed by threads. Furthermore, locking is still exploited as a core mechanism in software-based shared-data management approaches for multi-core machines like Read-Copy-Update (RCU) [10], where readers are allowed to concurrently access shared data with respect to writers, but concurrent writers are anyhow serialized via the explicit usage of locks.

Despite the rising trend towards differentiated synchronization supports, locking still stands as a core synchronization mechanism. Therefore, optimizing the runtime behavior of locking primitives is a core achievement for software operations carried out on nowadays multi-core hardware.

Actually, we can distinguish among two main categories of locks: 1) spin locks, which are based on threads actively waiting for the ownership in the access to the targeted shared resource; 2) sleep locks, which make threads not run on any CPU-core until they can (retry to) acquire the access ownership. As well known, the first category can operate in user-space, while the latter requires the interaction with and the support of the underlying Operating System (OS) kernel.

Spin locks are often preferred in HPC applications—where low or none time-sharing interference in spinning phases (or more generally in the usage of CPU by threads) is expected—since they ensure the lowest latency while acquiring the ownership of the lock. However, they do not care about metrics such as CPU usage. In fact, CPU cycles wasted in spinning operations because of conflicting accesses to critical sections may result non-negligible at non-minimal thread counts. Moreover, this might increase the impact of hardware contention on performance, since spin operations typically involve atomic instructions that trigger the cache-coherence firmware. Conversely, sleep locks save CPU cycles and reduce hardware contention, thus representing the obvious alternative to spin locks when resource usage is a concern. However, they might increase the latency in the access to a critical section because of delays introduced by the OS while awakening and scheduling threads.

In this scenario, developers might benefit of a lock supporting both active and passive waiting phases and able to determine the best choice between spinning and sleeping at run time. Such an approach can guarantee that non-predicted changes in the workload cannot hamper the overall system performance and can relieve developers from taking static choices between spinning and sleeping, which might be always inadequate with a dynamic workload, reducing the experimental evaluation and development time.

To tackle the limitations of spin and sleep locks, in this article we present a new synchronization support called mutable lock. Our solution is based on a non-trivial combination of spin and sleep primitives, which gives rise to a state machine driving the evolution of threads in such a way that sleep-to-spin transitions are envisaged as a means to always guarantee that some thread is already awaken when the critical section is released. Hence, it can access the critical section with no additional delay caused by the OS awakening phase. On the other hand, the sleep phase is retained as a means for controlling the waste of resources that would otherwise be experienced with pure spin locking.

Our mutable locks ship with the support for the autonomic tuning of the transitions between sleep and spin phases—or the choice of one of the two upon the initial attempt to access the critical section—which is implemented as a control algorithm encapsulated into the locking primitives. Furthermore, our solution is fully transparent, and can be exploited by simply redirecting the API of the locking primitive originally used by the programmer to our mutable locks library111Code available at https://github.com/HPDCS/libmutlock.

The remainder of this article is structured as follows. In Section 2 we discuss related work. Section 3 presents the design of our mutable locks. Experimental results for a comparison with other conventional lock implementations are reported in Section 4. Section 5 gives the conclusions.

2 Related Work

Spin locks have been originally implemented by only relying on atomic read/write instructions [4, 9]. However, this solution had limited applicability to scenarios where the number of threads to synchronize was known at compile/initialization time and could not change at runtime. Such a limitation was overcome by recurring to atomic Read-Modify-Write (RMW) instructions, like Compare&Swap (CAS) in modern processors. The main idea behind RMW-based spin locks is the one of repeatedly trying to atomically switch a variable from a value to another value—the so-called test-and-set operation. If a thread succeeds in this operation, it can proceed and execute the critical section, otherwise it has to continuosly retry the operation—this is the spin phase. Spin locks are greedy in terms of clock cycles usage, thus leading to non-minimal waste of resources in scenarios with non-negligible likelihood of thread conflict in the access to critical sections. This problem is further exacerbated by the fact that RMW instructions make intensive usage of state transitions in the hardware-level cache coherency protocol. This in turn can impact the cache access latency by other threads, including the one that is currently owning the critical section. Clearly, the more threads spin at the same time, the worse the scenario becomes.

The test-and-test-and-set spin lock [14] makes challenging threads continuously check the lock variable until it is released and, only in this case, they try to acquire it via RMW instructions. This allows threads to spin (read the actual value of the lock variable) in cache without disturbing others, thus generating cache/memory traffic only when strictly needed.

The authors of [1] introduce a simple back-off time before attempting to re-acquire the lock. Anyhow, such a strategy requires some variables to be set up, such as the maximum and minimum back off time, that cannot be universal across any hardware architecture and/or workload [15].

Spin locks might lead to starvation since there is no assurance that a given thread wins the challenge eventually. The authors of [11] introduce the queued spin lock to resolve this issue. It is a linked list where the first connected node is owned by the thread holding the lock, while others are inserted in a FIFO order by threads trying to access the critical section. Such threads spin on a boolean variable encapsulated in their individual nodes. This guarantees that each spinning thread repeatedly reads a memory cell different from other threads and a releasing thread updates a cacheline owned by a unique CPU-core, which significantly reduce the pressure on the cache management firmware.

In all the above solutions, there is no direct attempt to control the number of threads spinning at each time instant, as instead we do in our mutable locks thanks to the smart combination of spin/sleep phases and sleep-to-spin transitions. Clearly, such a limitation of the literature approaches can lead to catastrophic consequences on performance and resource usage when there is a relevant hardware contention, e.g. when applications are executed with more threads than cores. Furthermore, when recurring to FIFO locks an anti pattern emerges. The FIFO semantics imposes that, when a thread releases the lock, one specific thread has to acquire it—the one standing at the head of the FIFO queue. It follows that delays (for example a CPU descheduling) affecting a thread impact all its successors in the queue. This increases considerably the residence time (queue time plus critical section execution time) and consequently the overall system performance can be impaired. As overall considerations, spin locks are typically avoided with long critical sections just because of the above motivations, but they do anyhow suffer from the problem of waste of resources in face of conflicting accesses to the critical section. Our mutable locks cope with both these problems, since the smart combination between spin and sleep phases avoids the antipattern where a thread running a long critical section is descheduled in favor of one simply spinning for the access to the critical section.

As hinted, sleep locks—based on OS blocking services—represent the opposite solution to synchronization, and are aimed at avoiding usage of resources (that would take place with spin locks) during wait phases preceding the access to the critical section. OS implementations offer sleep locks since their very beginning, and various improvements in these synchronization constructs have been devised in order to enable flexible synchronization schemes, involving awake conditions resulting as the combination of the state of multiple sleep locks. Examples are the System V semaphores offered by Posix [8] or the wait-for-multiple-object primitive offered by WinAPI [12]. In any case, all the sleep locks based on blocking OS services share the common drawback that, as soon as a critical section is released, there is no guarantee that a thread willing to access the critical section is already CPU-dispatchable (or already dispatched). In fact, it might have gone sleeping, thus needing to undergo a wake-up phase bringing it back onto the OS run-queue. Overall, we may experience a delay in the access by this thread to the critical section, which in turn may hamper performance, especially when the critical section is short—a problem exacerbated at higher concurrency.

Critical Section

Spin

Critical Section

Spin

Spin

Critical Section

Critical Section

Critical Section

Critical Section

a) Only spinning

Critical Section

Wake Up

Critical Section

Wake Up

Critical Section

Critical Section

Critical Section

Critical Section

b) Only sleeping

Critical Section

Spin

Critical Section

Wake Up

Critical Section

Critical Section

Critical Section

Critical Section

c) Desideratum
Fig. 1: Different timelines related to different lock specifications

A lock implementation which copes with the issue of choosing at runtime between spinning and sleeping is the mutex offered by the glibc pthread library[7]. This lock can work with two different behaviors: default and adaptive. In the default configuration, a thread tries to acquire the lock by initially performing an atomic test-and-set operation. If this operation fails, the thread goes to sleep. Conversely, the adaptive behavior is based on the idea of attempting to spin for a while before going to sleep. The limitation of this approach is that it does not offer any support for making a thread transiting from the sleep phase to the spin phase before the critical section is released. Hence, like for the case of pure sleep locks, the access to the critical section might be delayed because of latencies associated with OS-level awakening of a waiting thread upon mutex release. In other words, the adaptive mutex attempts to tackle the problem of reducing the waste of resources caused by excessive spin operations, but does not jointly copes with the optimization of the latency for accessing the critical section when sleeps occur, an issue that is instead tackled by our mutable locks.

3 Mutable Locks

Let us slide towards the description of our mutable locks through the help of an example scenario where 3 threads compete for the access to a critical section. For simplicity, but with no loss of generality, we consider the case where the critical section duration is equal to the time required by a thread to be awaken and CPU-rescheduled—if originally sleeping because of lock occupancy by another thread—and where each thread runs on a different CPU-core. Figure 1 shows different timelines resulting from the abovementioned workload running on different lock specifications. The timeline at the bottom shows the projection of each critical section on the real time axis.

Figure 1a) represents a possible execution resulting by adopting a spin lock (e.g. a test-and-test-and-set spin lock). As we can see, threads that have loosen the challenge are always ready to participate to a new challenge. This makes critical section executions immediately consecutive along the real time axis, requiring 3 slots for executing critical sections (CSes) and 3 slots for spinning. It follows that the 50% of the clock cycles dedicated to the execution of those three CSes are “wasted” in spin operations for ensuring minimum latency.

Figure 1b) shows the effects of always going to sleep if the lock is already taken by some other thread—this is the same strategy adopted by the pthread mutex in the default configuration. During the release phase, each thread wakes only one thread at a time. Consequently, we have to pay some awakening latency for accessing the critical section. In this scenario, 5 slots (each lasting the duration of the critical section) are required to complete 3 critical sections. It follows that the overall throughput is 40% worse than the one achieved by the spin lock-based approach, but 2 slots instead of 3 are wasted for awakening and CPU-reschedule operations.

Finally, Figure 1c) shows an optimized behavior with the same amount (2) of wasted slots as for the classical sleep-based approach, but where the latency for accessing the critical section is the same as the one of the spin-lock approach. In this scenario the lock is able to decide which thread has to go to sleep and which one to spin during the challenge for acquiring it, or to transit out of the sleep state before attempting to reacquire the lock. In particular, it makes the latency of awakening a thread () be masked by the critical section execution of the spinning thread (). This optimized behavior is the target of our mutable lock algorithm, which encapsulates the ability to mix spin and sleep phases in an optimized manner, with the inclusion of sleep to spin transitions.

0

1

2

3

4

5

6

7

8

9

10

X

X

X

X

X

X

X

X

……..

Threadin critical section

Spinning Window Size

Spinning Window

Thread Count

Fig. 2: Logical representation of a lock with a spinning window

3.1 The Notion of Spinning Window

A baseline concept the mutable lock relies on is the spinning window (SW). SW allows identifying a set of threads allowed to spin—among those contending for the critical section— while the others (if any) need to undergo a sleep phase. The maximum cardinality of this set is bounded by an integer value called spinning window size (SWS). A logical representation of the effects of using SW is shown in Figure 2. Here we have an array where each slot is occupied by a waiting thread except for the first one (with index equal to 0) that represents the thread holding the lock. The next SWS cells form the SW and are occupied by threads allowed to spin, while others are outside the SW and are sleeping. A new arriving thread takes the first available slot and, according to its index , it choses if it has to spin or to sleep. In particular:

The lock release operation consists in making one random spinning thread access the critical section and one random sleeping thread wake up and occupy the just freed slot of the SW. The latter is, essentially, the sleep to spin transition we include in our mutable lock logic. Then, the array cell that is left empty outside the spinning window by the woke up thread will be occupied by shifting the threads associated with larger indexes. This complies with a specification that does not ensure a FIFO policy while serving threads—which can be instead obtained by left-shifting all threads in the array exactly by one position. It follows that this approach allows to control the exact number of threads allowed to spin—including those transiting from the sleep to spin state—by manipulating the value of SWS.

Since we are interested in pursuing two goals, maximizing performance and reducing the waste of clock cycles caused by spin operations, the SWS should adapt to the workload peculiarities and changes—e.g. to the duration of the critical section and the actual incidence of conflicts in its access. The larger SWS, the lower the access latency—although if too many threads spin, we get CPU-interference on the one running the critical section–but, at the same time, more computational power is wasted due to spinning cycles. Conversely, a lower value of SWS tends to reduce clock cycles usage, but increases the probability that threads experience late wake ups, stretching the critical section access latency. Overall, a suited value for SWS is the one ensuring that the number of spinning threads is low and the latency of awakening threads is masked by critical section executions by other threads.

Dynamically adapting the value of SWS at runtime is not a trivial task since the newly chosen value must be correctly reflected onto the actual state of the threads (sleeping or spinning). More in detail, increasing the value of SWS with no other action could make one or more threads to be considered as falling within SW (thus spinning) even if they are currently sleeping (case 1). Consequently, no one will ever try to wake them up and they will sleep unboundedly unless the SWS is eventually restored to the original value. On the other hand, reducing the value of SWS, which might make some spinning thread be outside SW (case 2), does not hamper progress. However, it makes a number of threads larger than SWS spin for an unknown period of time, diverging from the desidered behavior.

Case 1 and 2 can occur only under specific conditions. Let be the variation of the value of SWS to be applied at runtime, the value of SWS before the variation is applied, and (thread count) be the number of threads waiting to access the lock. Case 1 can occur if and only if (C1) while case 2 can occur if and only if (C2).

The SW specification tackle case 1 by waking up a number of sleeping threads equal to instead of 1 as in normal release operations. Case 2 is tackled by assigning to a number of spinning threads higher priority in the access to SW, with respect to threads that transit from the sleep to the spin state. This can be obtained by simply waking no thread up for a number of release phases equal to .

This mutable lock algorithm, described in Section 3.2, is de-facto a new thread synchronization algorithm grounded on the notion of locking primitive. At the same time, the change of SWS, in order to adapt its value to the workload, needs to be actuated via some other algorithm, which implements a kind of oracle for optimizing the runtime dynamics under mutable lock-based synchronization. Clearly, different oracles for adapting the SWS value at runtime could be devised, and we provide one of them in Section 3.2. In any case, the mutable lock algorithm is independent of the actually selected SWS adaptation oracle. This opens to the possibility of studying further variations of thread synchronization dynamics built around the notion of mutable lock.

3.2 The Mutable Lock Algorithm

A mutable lock is a spin lock, denoted as spn_obj, plus other five variables: sws, which stores the current spinning window size; thc, which keeps the thread count, i.e. the number of threads currently waiting for accessing the lock plus one (that holds the lock); wuc, which keeps the wake-up count, i.e. number of threads to be woken up during a mutable lock release phase; slp_obj, which is a blocking synchronization object used to access the sleep/wake-up API of the underlying Operating System; max is the maximum SWS set to the number of cores.

In our design, sws and thc are 32 bits long and are stored in a unique 64-bits word, denoted as lstate (lock state), such that . This arrangement allows threads to update one field, get its old value and retrieve the actual value of the other field in once by using an atomic Fetch&Add (FAD) machine instruction, commonly supported by off-the-shelf processors.

A1:procedure Acquire(mutlock m) A2:      A3:      A4:      FAD(m.lstate, +1) A5:      A6:      A7:     if   then A8:           A9:          .sleep() A10:     end if A11:      .lock() A12:      EvalSWS(, , ) A13:     if   then A14:          return A15:     end if A16:      ? : A17:      .max ? .max : A18:     if  then A19:           ( << 32); A20:           FAD(m.lstate, ) A21:           A22:           A23:           A24:           A25:          if   then A26:                A27:          else if   then A28:                A29:          else A30:                A31:          end if A32:           A33:           + A34:     end if A35:end procedure R1:procedure Release(mutlock m) R2:     if  then R3:           R4:           R5:     else R6:           R7:           R8:     end if R9:      FAD(m.lstate, -1) R10:     .unlock() R11:     if   then R12:          return R13:     end if R14:      R15:      R16:     if   then R17:           R18:     end if R19:     while  do R20:           .wake_up() R21:           R22:     end while R23:end procedure   E1:procedure EvalSWS(bool , bool , mutlock ) E2:     .cnt .cnt E3:      E4:     if   then E5:           .sws E6:          .cnt E7:     else if .cnt  then E8:           E9:          .cnt E10:     end if E11:     return E12:end procedure
Algorithm 1 Mutable Lock Operations

The operations used to acquire or release the mutable lock are shown in Algorithm 1. Let be an atomic register (a variable) supporting atomic FAD operations, in our notation and are the values of respectively before and after a FAD execution on it. During an acquire phase, a thread increasing thc via FAD (line A4) and checks whether there is space in SW. If the condition holds (line A7)—no room is available in SW—it goes to sleep on slp_obj (line A9). Otherwise it invokes the acquire API of spn_obj (line A11).

As soon as a thread owns the spn_obj, it determines if sws should be updated by invoking EvalSWS. This is the function implementing the SWS adaptation oracle. It returns the signed variation to be applied to sws. This update is performed via FAD on the most 32 significant bits of the lstate field (line A20). Based on the values of , thc, and , we know that some countermeasure has to be taken in order to ensure progress of each thread and that the number of spinning threads will be eventually bounded by sws. In particular, if condition C1 occurs, we set the variable wuc to the number of additional threads to be woken up. Conversely, when condition C2 holds, wuc will be set to the number of threads in a spinning state which are outside the SW multiplied by -1. Finally, if none of the above conditions holds, no countermeasure is needed at all (line A30). At this point, the computed value will be simply added to wuc (line A33), and the lock acquire phase is completed.

Upon a lock release operation, if holds (line R6), its value is copied into a local variable and then is set to 0, otherwise (line R3) it is incremented by 1 and is set to -1. Now, the thc can be decremented by 1 via FAD and the spn_obj release API allows another thread to get the lock (lines R9 and R10). In order to complete the release operation, we have to ensure that the number of non-sleeping threads is compliant with the current value of sws. Thus, the releasing thread first check if is lower than 0. In this case, it can simply return since a previous reduction of sws has made some thread spinning outside the SW and, consequently, no additional wake up is required. This is because sws updates and decrements of thc are performed in mutual exclusion (via FAD) and it is ensured that more than sws threads are spinning. If is greater than or equal to 0, we need to check if an additional thread should be awakened in order to keep a number of spinning threads equal to the current value of sws. In this case (line R16), is incremented by 1. Finally, the thread can awake threads by relying on the slp_obj API (line R20). Thanks to these algorithms, shared variables () used to keep the state of the lock are updated consistently without resorting to additional locks that could lead to other challenges such as choosing the proper lock implementation for protecting them.

The oracle (shown in the routine EvalSWS of Algorithm 1) we present for dynamically varying the SWS is based on the following policy. If a thread wakes up and there are no spinning threads, it means that SWS is not larger enough to mask wake-up latency. In fact, we know that, when arrived to the lock, there were other threads in active wait () and those threads have consumed SWS critical sections. In this case, we double SWS. Conversely, if such an event does not occur for consecutive critical-section executions, the oracle tries to decrease SWS by 1. This should allow us to keep the SWS below the minimum value required to mask the wake up latency in cases. Clearly, choosing the proper value for depends on characteristics specific of the underlying hardware/software stack and its trade-off between the impact of hardware contention and latencies for waking threads up. However, finding the optimal oracle is beyond the scope of this work, which is focused on providing a new technique for combining spinning and sleeping waiting phases that, at the best of our knowledge, explores the usage of a state transition (sleep to spin) never adopted by previous approaches.

4 Experimental Study

Fig. 3:

Tests consist in repeatedly executing critical and non-critical sections, whose lengths are uniformly distributed in the interval

and respectively. Left charts show throughput (the higher the better), while ones in the middle column show CPU time spent in synchronization (the lower the better). Finally, charts on the right gives the ratio between the average throughput of a given lock and the average of the optimum obtained getting the lock with maximum throughtput for each thread count (the higher the better)—PT-EXP refers to the mean of PT-SPINLOCK and PT-MUTEX values.
Fig. 4: Results for PHOLD run on top of a share-everything PDES platform.

We have evaluated our mutable lock (denoted as MUTLOCK) against the pthread spin lock (PT-SPINLOCK), an implementation of a queue lock (MCS) and the pthread mutex in both default and adaptive configuration (PT-MUTEX and PT-ADAPTIVE-MUTEX). MUTLOCK adopts a classical test-and-test-and-set spin lock as spn_obj and a semaphore as sleeping object. The parameter of the oracle has been set to 10 in order to keep the probability of paying the latency for waking threads up below the 10%, making the mutable lock biased in avoiding late wake ups instead of reducing hardware contetion. Each implementation has been evaluated by resorting to one synthetic benchmark and one real-world application. The first one, called lockbench222Available at https://github.com/HPDCS, makes a given number of threads repeatedly access a critical section and then execute a non-critical section, whose lengths and are uniformly distributed within a given interval equal to and respectively. Our performance metric is the throughput intended as number of executed critical sections per unit time, while we have adopted the CPU time spent in synchronization to evaluate CPU usage savings. The tests were executed on a ThinkMate GPX XT10-2260V4-4GPU equipped with 2 Intel Xeon E5-2640 v4 for a total of 20 cores equipped with 64GB of memory arranged in 2 NUMA nodes.

Figure 3 show all the results of the synthetic benchmark. In the left column we can find the throughputs in terms of critical section executed per second, while the center column shows CPU time wasted for synchronizing threads. Finally, the right column reports the ratio between the average throughtput of a given lock and the average of the optimal solution, namely the one that for each thread count has the maximum throughput achieved with the evaluated locks. The column denoted as PT-EXP is the mean between the values of PT-SPINLOCK and PT-MUTEX, representing the expected behaviour when a static choice (with uniform probability) between PT-SPINLOCK and PT-MUTEX has been made without knowing the actual workload and concurrency level. If PT-EXP is higher than MUTLOCK, it is convinient taking the risk of an a-priori choice between the two pthread locks, otherwise MUTLOCK represent a better choice capable of ensuring an expected higher throughput.

Figure 3 shows the throughput while executing both critical sections and non-critical sections uniformly distributed between and . As expected, MCS has the highest throughput because it best fits the NUMA arrangement of memory when the thread count is lower than (or equal to) the number of cores. In fact, each thread spins on its own cache line and the thread holding the lock touches a single line (the one owned by the next in the FIFO order) for signaling the release. MUTLOCK has slightly lower throughput than PT-SPINLOCK, showing an overhead up to the 8% for its management. Conversely, PT-MUTEX (PT-ADAPTIVE-MUTEX) has a 25% (12%) drop of performance w.r.t. spin locks and shows its benefit only in case of time sharing, where going to sleep is a smart choice to reduce hardware contention. As expected, MCS has a drop of performance in this scenario due to its FIFO semantics. The CPU usage devoted to synchronization is shown in Figure 3. Here, we can see that our MUTLOCK consumes the same amount of CPU w.r.t. PT-SPINLOCK (and much less than MCS which is in trashing), conversely both mutexes reduce by one order of magnitude the CPU time. This shows that mutexes are very efficient in terms of cpu usage, but this come with a price in terms of performance. Figure 3 confirms that spin locking is the best option with no-timesharing and PT-SPINLOCK has comparable performance w.r.t. PT-MUTEX in timesharing. However, our MUTLOCK guarantees an higher average throughput than PT-EXP and almost optimal in case of timesharing.

In a second set of experiments we consider critical-section length uniformly distributed in (Figure 3). Here, we can see that spinning for a very long time is convenient only for low thread counts (up to 4). Conversely, mutexes show their advantages having a maximum and stable throughput with thread counts higher than 4. It follows that we are in a scenario where the hardware contention has a relevant impact on performance. While pure spin locking is fated to worse while increasing the thread count, MUTLOCK maintains a stable throughput, a bit lower than mutexes since it continues to keep a few threads spinning to mask wake up latency. However, since critical sections are very long, such latencies are negligible and going to sleep allows to reduce hardware contention. Finally, MUTLOCK reduces the CPU time spent while synchronizing by an order of magnitude w.r.t. to spin locks for high thread counts (above 10) as shown in Figure 3. Figure 3 shows that this is a worst case scenario for our approach, since it has an average performance slightly lower than PT-EXP without timesharing. This is reasonable since one of the main limitation of our approach is not considering the length of the critical section while sizing the spinning window. However, the proposed hybrid approach guarantees a loss bounded by the 8% of the optimum, a threashold already crossed by pure spin locking approaches when running with 8 threads.

Short critical sections and non-critical section uniformly distributed in lead to very low lock contention. In such a scenario all locks have similar performance (Figure 3) and CPU times (Figure 3), except for pure spin locks in case of time sharing. Consequently, there is almost no loss in adopting our MUTLOCK (Figure 3).

The last case, namely the one with both CS and NCS uniformly distributed in , resembles a scenario where critical sections are very long, but scarcely accessed. On the one hand, this reduces the differences between pure spin locks and MUTLOCK as shown in Figure 3 and, on the other hand, it exacerbates the benefits of making a controlled number of threads spinning in order to save CPU time and not paying wake up latency. Clearly, the higher throughput than mutexes comes with a price in terms of CPU times (Figure 3). In fact, our MUTLOCK consumes up to one order of magnitude of additional CPU time w.r.t mutexes in order to guarantee performance almost optimal.

To summarize, our approach allows to achieve the highest average performance with low contention and higher throughput than the expetation when a static choice between pthread spin lock and mutex has been made. This makes our MUTLOCK a good candidate when operating in uncertain conditions because of an unpredictable (or difficult to be reasoned) workload and/or of a virtualized hardware.

We also tested our solution within the open source share-everything Parallel Discrete Event Simulator (PDES) project333Available at https://github.com/HPDCS. In this framework the simulation model is partitioned in Logical Processes (LPs) and its execution is guided by the occurance of discrete events handled by working threads mapped to specific CPU-cores.

As test-bed application we used the classical PHOLD benchmark [6] configured with 1024 simulation objects and 16 or 20 worker threads. As usual for PHOLD, event processing leads to spending some CPU time, via a configurable busy loop emulating a given event granularity. In our experiments we initially set the loop to give rise to events with granularity varying in 25,50 and 100 microseconds, which can be considered from mid- to very-high-weight values. In our PHOLD configuration we included 32 (greater than the number of cores) hot-spot simulation objects, towards which a given percentage of events are routed. This percentage has been set to 50%. Figure 4 shows the speedups w.r.t the sequential execution and CPU times wasted for synchronization with the different lock implementations when running with 16 and 20 threads (with lower thread count results are very similar for different locks). The MUTLOCK allows the simulator to achieve the highest speed up showing that a static decision between spinning and sleeping is suboptimal for performance. This gain is observed in combinatijn with a significant reduction of the CPU time spent for synchronizaiton compared to PT-SPINLOCK.

5 Conclusions

In this article, we have introduced the Mutable Locks, a locking mechanism based on the concept of spinning windowthat can control the number of threads enabled to spin in order to save CPU usage and to guarantee responsiveness while synchronizing threads. Finally, we have demostrated the validity of our approach thanks to an extensive experimental evaluation in both synthetic and

real-world scenarios, showing that is capable of ensuring either higher performance or lower loss than evaluated adversaries. As future work, we plan to study other approaches to resize the spinning window and to extend the states of waiting threads besides the classical spin/sleep ones, for example by introducing additional states where threads adopt a backoff time before attempting to acquire the lock or where CPU-cores are allowed to spin with a given frequency set by exploiting the Dynamic Voltage and Frequency Scaling capabilities of modern processors. This should allow to further increase the saving of computing power due to active waiting phases and to reduce hardware contention without sacrificing performance.

References

  • [1] Anderson, T.E.: The performance of spin lock alternatives for shared-memory multiprocessors. IEEE Trans. Parallel Distrib. Syst. 1(1), 6–16 (1990).
  • [2] Barroso, L.A., Hölzle, U.: The case for energy-proportional computing. Computer 40(12), 33–37 (2007).
  • [3] Dice, D., Shalev, O., Shavit, N.: Transactional locking ii. In: Proceedings of the 20th International Conference on Distributed Computing, DISC’06, pp. 194–208. Springer-Verlag, Berlin, Heidelberg (2006).
  • [4] Dijkstra, E.W.: Solution of a problem in concurrent programming control. Commun. ACM 8(9), 569– (1965).
  • [5] Esmaeilzadeh, H., Blem, E., Amant, R.S., Sankaralingam, K., Burger, D.: Dark silicon and the end of multicore scaling. In: 2011 38th Annual International Symposium on Computer Architecture (ISCA), pp. 365–376 (2011)
  • [6] Fujimoto, R.M.: Performance of Time Warp Under Synthetic Workloads. In: Proceedings of the Multiconference on Distributed Simulation, pp. 23–28. Society for Computer Simulation (1990)
  • [7] GNU.org: GNU C Library
  • [8] IEEE: Posix standard
  • [9] Lamport, L.: A new solution of dijkstra’s concurrent programming problem. Commun. ACM 17(8), 453–455 (1974).
  • [10] McKenney, P.E., Slingwine, J.D.: Read-copy update: Using execution history to solve concurrency problems. In: Parallel and Distributed Computing and Systems, pp. 509–518 (1998)
  • [11] Mellor-Crummey, J.M., Scott, M.L.: Algorithms for scalable synchronization on shared-memory multiprocessors. ACM Transactions on Computer Systems 9(1), 21–65 (1991).
  • [12] Microsoft: Windows API index
  • [13] Research, I.: Single-chip cloud computer. URL http://techresearch.intel.com/ProjectDetails.aspx?Id=1
  • [14] Rudolph, L., Segall, Z.: Dynamic decentralized cache schemes for mimd parallel processors. Proceedings of the 11th annual international symposium on Computer architecture - ISCA ’84 pp. 340–347 (1984).
  • [15] Scott, M.L.: Shared-Memory Synchronization. Synthesis Lectures on Computer Architecture 8(2), 1–221 (2013).